├── .gitignore ├── README.md ├── part01 ├── README.md ├── ex01-c-example │ ├── README.md │ └── hello-nes.c └── ex02-asm-example │ ├── README.md │ ├── anton.chr │ ├── nesdefs.inc │ ├── nesfile.ini │ └── test.s └── part02 ├── README.md └── ex01-controller-test ├── README.md ├── anton.chr ├── anton2.chr ├── helpers.inc ├── nesdefs.inc ├── nesfile.ini ├── ram.inc ├── rodata.inc └── test.s /.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | *.swp 3 | *~ 4 | *.o 5 | *.lst 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /part01/ex02-asm-example/README.md: -------------------------------------------------------------------------------- 1 | # ex02-asm-example 2 | 3 | This is a simple example NES program, written by [Anton Maurovic](http://anton.maurovic.com), 4 | in 6502 assembly language. 5 | 6 | It writes a message to the screen with a basic "typewriter" sound effect per each 7 | character, then changes part of the message to an orange colour, before scrolling it 8 | off the screen and repeating the process. 9 | 10 | 11 | ## Features 12 | 13 | Primarily this demonstrates: 14 | 15 | 1. How to structure a 6502 assembly language program for cc65, such that it targets 16 | the NES architecture, and in particular the `.nes` 17 | ([INES](http://wiki.nesdev.com/w/index.php/INES)) file format. 18 | 19 | 2. Initialising and validating hardware in the NES, especially the PPU (Picture Processing Unit) 20 | for video, and the APU (Audio Processing Unit). 21 | 22 | 3. Writing to a nametable of the PPU, i.e. creating a "background image" using the "tile" layout 23 | -- in this case writing characters of a message to the screen. 24 | 25 | 4. Generating simple one-shot sound effects with the APU -- in this case a white noise 26 | effect with an envelope. 27 | 28 | 5. The basics of scrolling. 29 | 30 | 31 | ## Compiling 32 | 33 | 1. Assemble `test.s` to an object file, `test.o`, using the 34 | [`ca65` assembler utility](http://www.cc65.org/doc/ca65.html): 35 | 36 | ca65 test.s -o test.o 37 | 38 | 2. You can then link the object file to a usable file, `test.nes`, using the 39 | [`ld65` linker utility](http://www.cc65.org/doc/ld65.html): 40 | 41 | ld65 test.o -C nesfile.ini -o test.nes 42 | 43 | Note that this uses `nesfile.ini` as configuration to tell the linker the binary 44 | layout of the target file, such that it matches the 45 | [INES specifications](http://wiki.nesdev.com/w/index.php/INES). 46 | 47 | Note also that the intermediate file `test.o` can now be deleted, if you prefer. 48 | 49 | 3. You now have `test.nes` which can be run with an emulator... say, 50 | [FCEUX](http://www.fceux.com/web/download.html). 51 | 52 | 53 | ## Files 54 | 55 | The following files make up this program: 56 | 57 | * `test.s` -- The main 6502 assembly language source code, specifically in cc65's `ca65` format. 58 | 59 | * `nesdefs.inc` -- An assembly language "include file" that defines certain constants 60 | (e.g. key memory locations such as NES hardware registers) 61 | and macros that are likely to be used by all NES programs. 62 | 63 | * `anton.chr` -- Raw binary data for a single 64 | ["pattern table"](http://wiki.nesdev.com/w/index.php/PPU_pattern_tables) (4096 bytes) of 256 "tiles", that 65 | are used to generate pixel images on the NES. In this case, this is a character set 66 | that I drew and encoded in the format required for the 67 | [NES "Character ROM"](http://wiki.nesdev.com/w/index.php/CHR_ROM_vs._CHR_RAM). This file gets 68 | "included" into `test.s`, but as a raw binary blob instead of source code. 69 | 70 | * `nesfile.ini` -- The definition of the memory segments in this project, as they are fleshed out 71 | in the source code, and as they are to be laid out in the target `test.nes` file. 72 | 73 | 74 | ## Extra debugging information 75 | 76 | Note that, if you want, you can get extra information about the compiling and linking 77 | stages, as follows: 78 | 79 | * If you append `-l` to the `ca65` command-line, then upon successful assembly it 80 | will generate an extra `test.lst` "listing" file that shows what machine code was generated 81 | for each line of source code. For example: 82 | 83 | ca65 test.s -o test.o -l 84 | 85 | ...will give a `test.lst` file that looks something like: 86 | 87 | 000004r 1 ; MAIN PROGRAM START: The 'reset' address. 88 | 000004r 1 .proc reset 89 | 000004r 1 90 | 000004r 1 ; Disable interrupts: 91 | 000004r 1 78 sei 92 | 000005r 1 93 | 000005r 1 ; Basic init: 94 | 000005r 1 A2 00 ldx #0 95 | 000007r 1 8E 00 20 stx PPU_CTRL ; General init state; NMIs (bit 7) disabled. 96 | 00000Ar 1 8E 01 20 stx PPU_MASK ; Disable rendering, i.e. turn off background & sprites. 97 | 00000Dr 1 8E 10 40 stx APU_DMC_CTRL ; Disable DMC IRQ. 98 | 000010r 1 99 | 000010r 1 ; Set stack pointer: 100 | 000010r 1 A6 FF ldx $FF 101 | 000012r 1 9A txs ; Stack pointer = $FF 102 | 000013r 1 103 | ... 104 | 105 | * If you append `-m XXX` to the `ld65` command-line, then it will generate a memory 106 | map file called `XXX` that describes the starting address and number of bytes used 107 | for each segment. For example: 108 | 109 | ld65 test.o -C nesfile.ini -o test.nes -m map.txt 110 | 111 | ...will generate a `map.txt` file that contains, amongst other things: 112 | 113 | Segment list: 114 | ------------- 115 | Name Start End Size 116 | -------------------------------------------- 117 | INESHDR 000000 000005 000006 118 | PATTERN0 000000 000FFF 001000 119 | ZEROPAGE 000010 000012 000003 120 | PATTERN1 001000 001FFF 001000 121 | CODE 00C000 00C194 000195 122 | RODATA 00C200 00C29D 00009E 123 | VECTORS 00FFFA 00FFFF 000006 124 | 125 | Note that because of the architecture of the NES, some of these 126 | (namely `PATTERN0` and `PATTERN1`) will appear to overlap with others, due to the 127 | separate address buses (and hence separate memory spaces that are coincident). 128 | -------------------------------------------------------------------------------- /part01/ex02-asm-example/anton.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algofoogle/nes-gamedev-examples/befef9b9c8609d00efe0b4cdfabae1cd560b0854/part01/ex02-asm-example/anton.chr -------------------------------------------------------------------------------- /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 #=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 | -------------------------------------------------------------------------------- /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/README.md: -------------------------------------------------------------------------------- 1 | # ex02-asm-example 2 | 3 | This is a simple example NES program, written by [Anton Maurovic](http://anton.maurovic.com), 4 | in 6502 assembly language. 5 | 6 | It writes a message to the screen with a basic "typewriter" sound effect per each 7 | character, then changes part of the message to an orange colour, before scrolling it 8 | off the screen and repeating the process. 9 | 10 | 11 | ## Features 12 | 13 | Primarily this demonstrates: 14 | 15 | 1. How to structure a 6502 assembly language program for cc65, such that it targets 16 | the NES architecture, and in particular the `.nes` 17 | ([INES](http://wiki.nesdev.com/w/index.php/INES)) file format. 18 | 19 | 2. Initialising and validating hardware in the NES, especially the PPU (Picture Processing Unit) 20 | for video, and the APU (Audio Processing Unit). 21 | 22 | 3. Writing to a nametable of the PPU, i.e. creating a "background image" using the "tile" layout 23 | -- in this case writing characters of a message to the screen. 24 | 25 | 4. Generating simple one-shot sound effects with the APU -- in this case a white noise 26 | effect with an envelope. 27 | 28 | 5. The basics of scrolling. 29 | 30 | 31 | ## Compiling 32 | 33 | 1. Assemble `test.s` to an object file, `test.o`, using the 34 | [`ca65` assembler utility](http://www.cc65.org/doc/ca65.html): 35 | 36 | ca65 test.s -o test.o 37 | 38 | 2. You can then link the object file to a usable file, `test.nes`, using the 39 | [`ld65` linker utility](http://www.cc65.org/doc/ld65.html): 40 | 41 | ld65 test.o -C nesfile.ini -o test.nes 42 | 43 | Note that this uses `nesfile.ini` as configuration to tell the linker the binary 44 | layout of the target file, such that it matches the 45 | [INES specifications](http://wiki.nesdev.com/w/index.php/INES). 46 | 47 | Note also that the intermediate file `test.o` can now be deleted, if you prefer. 48 | 49 | 3. You now have `test.nes` which can be run with an emulator... say, 50 | [FCEUX](http://www.fceux.com/web/download.html). 51 | 52 | 53 | ## Files 54 | 55 | The following files make up this program: 56 | 57 | * `test.s` -- The main 6502 assembly language source code, specifically in cc65's `ca65` format. 58 | 59 | * `nesdefs.inc` -- An assembly language "include file" that defines certain constants 60 | (e.g. key memory locations such as NES hardware registers) 61 | and macros that are likely to be used by all NES programs. 62 | 63 | * `anton.chr` -- Raw binary data for a single 64 | ["pattern table"](http://wiki.nesdev.com/w/index.php/PPU_pattern_tables) (4096 bytes) of 256 "tiles", that 65 | are used to generate pixel images on the NES. In this case, this is a character set 66 | that I drew and encoded in the format required for the 67 | [NES "Character ROM"](http://wiki.nesdev.com/w/index.php/CHR_ROM_vs._CHR_RAM). This file gets 68 | "included" into `test.s`, but as a raw binary blob instead of source code. 69 | 70 | * `nesfile.ini` -- The definition of the memory segments in this project, as they are fleshed out 71 | in the source code, and as they are to be laid out in the target `test.nes` file. 72 | 73 | 74 | ## Extra debugging information 75 | 76 | Note that, if you want, you can get extra information about the compiling and linking 77 | stages, as follows: 78 | 79 | * If you append `-l` to the `ca65` command-line, then upon successful assembly it 80 | will generate an extra `test.lst` "listing" file that shows what machine code was generated 81 | for each line of source code. For example: 82 | 83 | ca65 test.s -o test.o -l 84 | 85 | ...will give a `test.lst` file that looks something like: 86 | 87 | 000004r 1 ; MAIN PROGRAM START: The 'reset' address. 88 | 000004r 1 .proc reset 89 | 000004r 1 90 | 000004r 1 ; Disable interrupts: 91 | 000004r 1 78 sei 92 | 000005r 1 93 | 000005r 1 ; Basic init: 94 | 000005r 1 A2 00 ldx #0 95 | 000007r 1 8E 00 20 stx PPU_CTRL ; General init state; NMIs (bit 7) disabled. 96 | 00000Ar 1 8E 01 20 stx PPU_MASK ; Disable rendering, i.e. turn off background & sprites. 97 | 00000Dr 1 8E 10 40 stx APU_DMC_CTRL ; Disable DMC IRQ. 98 | 000010r 1 99 | 000010r 1 ; Set stack pointer: 100 | 000010r 1 A6 FF ldx $FF 101 | 000012r 1 9A txs ; Stack pointer = $FF 102 | 000013r 1 103 | ... 104 | 105 | * If you append `-m XXX` to the `ld65` command-line, then it will generate a memory 106 | map file called `XXX` that describes the starting address and number of bytes used 107 | for each segment. For example: 108 | 109 | ld65 test.o -C nesfile.ini -o test.nes -m map.txt 110 | 111 | ...will generate a `map.txt` file that contains, amongst other things: 112 | 113 | Segment list: 114 | ------------- 115 | Name Start End Size 116 | -------------------------------------------- 117 | INESHDR 000000 000005 000006 118 | PATTERN0 000000 000FFF 001000 119 | ZEROPAGE 000010 000012 000003 120 | PATTERN1 001000 001FFF 001000 121 | CODE 00C000 00C194 000195 122 | RODATA 00C200 00C29D 00009E 123 | VECTORS 00FFFA 00FFFF 000006 124 | 125 | Note that because of the architecture of the NES, some of these 126 | (namely `PATTERN0` and `PATTERN1`) will appear to overlap with others, due to the 127 | separate address buses (and hence separate memory spaces that are coincident). 128 | -------------------------------------------------------------------------------- /part02/ex01-controller-test/anton.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algofoogle/nes-gamedev-examples/befef9b9c8609d00efe0b4cdfabae1cd560b0854/part02/ex01-controller-test/anton.chr -------------------------------------------------------------------------------- /part02/ex01-controller-test/anton2.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algofoogle/nes-gamedev-examples/befef9b9c8609d00efe0b4cdfabae1cd560b0854/part02/ex01-controller-test/anton2.chr -------------------------------------------------------------------------------- /part02/ex01-controller-test/helpers.inc: -------------------------------------------------------------------------------- 1 | ; This waits for a change in the value of the NMI counter. 2 | ; It destroys the A register. 3 | .macro wait_for_nmi 4 | lda nmi_counter 5 | : cmp nmi_counter 6 | beq :- ; Loop, so long as nmi_counter hasn't changed its value. 7 | .endmacro 8 | 9 | 10 | ; This waits for a given no. of NMIs to pass. It destroys the A register. 11 | ; Note that it relies on an NMI counter that decrements, rather than increments. 12 | .macro nmi_delay frames 13 | lda #frames 14 | sta nmi_counter ; Store the desired frame count. 15 | : lda nmi_counter ; In a loop, keep checking the frame count. 16 | bne :- ; Loop until it's decremented to 0. 17 | .endmacro 18 | 19 | 20 | .macro basic_init 21 | ; Disable interrupts: 22 | sei 23 | ; Disable 'decimal' mode (because the NES CPU doesn't support it): 24 | cld 25 | ; Basic init: 26 | ldx #0 27 | stx PPU_CTRL ; General init state; NMIs (bit 7) disabled. 28 | stx PPU_MASK ; Disable rendering, i.e. turn off background & sprites. 29 | stx APU_DMC_CTRL ; Disable DMC IRQ. 30 | 31 | ; Set stack pointer: 32 | dex ; X = $FF 33 | txs ; Stack pointer = $FF 34 | .endmacro 35 | 36 | 37 | .macro clear_wram 38 | ; Clear WRAM, including zeropage; probably not strictly necessary 39 | ; (and creates a false sense of security) but it DOES ensure a clean 40 | ; state at power-on and reset. 41 | ; WRAM ("Work RAM") is the only general-purpose RAM in the NES. 42 | ; It is 2KiB mapped to $0000-$07FF. 43 | ldx #0 44 | txa 45 | : sta $0000, X ; This line, in the loop, will clear zeropage. 46 | sta $0100, X 47 | sta $0200, X 48 | sta $0300, X 49 | sta $0400, X 50 | sta $0500, X 51 | sta $0600, X 52 | sta $0700, X 53 | inx 54 | bne :- 55 | .endmacro 56 | 57 | 58 | .macro ack_interrupts 59 | ; Clear lingering interrupts since before reset: 60 | bit PPU_STATUS ; Ack VBLANK NMI (if one was left over after reset); bit 7. 61 | bit APU_CHAN_CTRL ; Ack DMC IRQ; bit 7 62 | .endmacro 63 | 64 | 65 | .macro init_apu 66 | ; Init APU: 67 | lda #$40 68 | sta APU_FRAME ; Disable APU Frame IRQ 69 | lda #$0F 70 | sta APU_CHAN_CTRL ; Disable DMC, enable/init other channels. 71 | .endmacro 72 | 73 | 74 | .macro ppu_wakeup 75 | ; PPU warm-up: Wait 1 full frame for the PPU to become stable, by watching VBLANK. 76 | ; NOTE: There are 2 different ways to wait for VBLANK. This is one, recommended 77 | ; during early startup init. The other is by the NMI being triggered. 78 | ; For more information, see: http://wiki.nesdev.com/w/index.php/NMI#Caveats 79 | : bit PPU_STATUS ; P.V (overflow) <- bit 6 (S0 hit); P.N (negative) <- bit 7 (VBLANK). 80 | bpl :- ; Keep checking until bit 7 (VBLANK) is asserted. 81 | ; First PPU frame has reached VBLANK. 82 | 83 | ; NOTE: "bit PPU_STATUS" reads the bit, but actually clears it in the process too, 84 | ; so we can loop on checking it a second time: 85 | 86 | ; Wait for second VBLANK: 87 | : bit PPU_STATUS 88 | bpl :- 89 | ; VLBANK asserted: PPU is now fully stabilised. 90 | .endmacro 91 | 92 | 93 | .macro load_palettes pdata 94 | ; $3F00-$3F1F in the PPU address space is where palette data is kept, 95 | ; organised as 2 sets (background & sprite sets) of 4 palettes, each 96 | ; being 4 bytes long (but only the upper 3 bytes of each being used). 97 | ; That is 2(sets) x 4(palettes) x 3(colours). $3F00 itself is the 98 | ; "backdrop" colour, or the universal background colour. 99 | ppu_addr $3F00 ; Tell the PPU we want to access address $3F00 in its address space. 100 | ldx #0 101 | : lda pdata,x 102 | sta PPU_DATA 103 | inx 104 | cpx #32 ; P.C gets set if X>=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 | -------------------------------------------------------------------------------- /part02/ex01-controller-test/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 # 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 | --------------------------------------------------------------------------------