Nintendo Entertainment System (NES)#

NES Specifications#

Lifespan

1985-1995 (NES), 1983-2003 (Famicom)

Media

ROM cartridge (Game Pak)

CPU 8-bit

Ricoh 2A03 8-bit processor (MOS Technology 6502 core) @ 1.79 MHz (NTSC) / 1.66 MHz (PAL)

Memory

2K work RAM, 2K video RAM (both can be expanded by cartridge), 256 bytes sprite RAM

Controllers

2 game pad controllers, “Zapper”

Best-selling game

Super Mario Bros.

NES (by Evan-Amos, CC-BY-SA 3.0)

History#

Following the success of their Game & Watch series and the Donkey Kong arcade hit, Nintendo made plans to create their own home game console with interchangable cartridges. Internally dubbed GAMECOM, it was renamed Family Computer (Famicom) based on a suggestion from the wife of Masayuki Uemura, head of Nintendo’s R&D2 group.

Nintendo almost licensed the ColecoVision wholesale for the Japanese market, due to the success of its Donkey Kong port, and their familiarity with the Z80 CPU. But instead they forged ahead with their own design.

The personal computer boom had led to a shortage in microprocessors, and few suppliers wanted to take on the risky prospect of a new game console. The manufacturer Ricoh had lots of spare capacity and personal connections with Nintendo engineers. They agreed to help produce custom chips for the new game console, with Nintendo guaranteeing them a three-million chip order.

Ricoh suggested the 6502 CPU, which took up less chip space than the Z80. Nintendo was unfamiliar with the 6502, but they surmised that its obscurity would stall competitors hoping to reverse-engineer the console. The Nintendo chip omits the BCD (Binary Coded Decimal) logic for legal reasons, but adds a complex sound generator and a DMA controller.

Nintendo approached Atari, who was already prototyping its MARIA console (later sold as the Atari 7800) and looking for an alternate console should its plans fall though. But the video game crash of 1983 would keep the Famicom in Japan for the time being.

The Famicom was released on July 15, 1983 for ¥14,800 (equivalent to ¥18,400 in 2019) alongside three ports of Nintendo’s successful arcade games Donkey Kong, Donkey Kong Jr. and Popeye. Its “plus controller”, now known as a game pad, was lifted straight from the Game & Watch.

At June 1985’s Consumer Electronics Show (CES), Nintendo unveiled the American version of its Famicom, the NES. It featured a new case and a “zero insertion force” cartridge slot.

Nintendo released 17 launch games: 10-Yard Fight, Baseball, Clu Clu Land, Duck Hunt, Excitebike, Golf, Gyromite, Hogan’s Alley, Ice Climber, Kung Fu, Pinball, Soccer, Stack-Up, Super Mario Bros., Tennis, Wild Gunman and Wrecking Crew.

For the nationwide release, Nintendo offered the “Action Set” which replaced the gimmicky Robotic Operating Buddy with Super Mario Bros.

Programming#

Almost all NES commercial titles were written in 6502 assembly language. This allowed developers to fine-tune each and every byte of their programs. This was critical when fitting games into the small but expensive in the game cartridge. It also allowed for clever optimizations which squeeze the most performance possible out of each frame of animation.

Nowadays, homebrew NES developers usually write games in either assembler or C, the latter using the cc65 compiler toolchain. Writing in C gives you more functionality per line of code. While it has lower performance and greater code size than a well-written assembly program, you can still write a pretty good game in C.

NES games come in cartridges, and inside of those cartridges are various circuits and hardware. Different games use different circuits and hardware, and the configuration and capabilities of such cartridges is commonly called their mapper. Mappers are designed to extend the system and bypass its limitations, such as by adding RAM to the cartridge or even extra sound channels. More commonly though, mappers are designed to allow games larger than 40K to be made. 8bitworkshop’s built-in JSNES emulator simulates many of these circuits.

Memory Map#

Start

End

Description

$0000

$00FF

RAM, zero-page

$0100

$01FF

RAM, CPU stack

$0200

$07FF

RAM, general-purpose

$0800

$1FFF

(mirror of $0000-$07FF)

$2000

$2007

PPU registers

$2008

$3FFF

(mirror of $2000-$2007)

$4000

$400F

APU registers

$4010

$4017

DMC, joystick, APU registers

$4020

$5FFF

Cartridge (maybe mapper registers)

$6000

$7FFF

Cartridge RAM (maybe battery-backed)

$8000

$FFFF

PRG ROM (maybe bank switched)

$FFFA

$FFFB

NMI vector

$FFFC

$FFFD

Reset vector

$FFFE

$FFFF

BRK vector

PPU Registers#

Address

Name

R/W

Description

$2000

PPU_CTRL

write

PPU Control 1

$2001

PPU_MASK

write

PPU Control 2

$2002

PPU_STATUS

read

PPU Status

$2003

OAM_ADDR

write

OAM Address

$2004

OAM_DATA

read/write

OAM Data

$2005

PPU_SCROLL

write 2x

Background Scroll Position \newline (write X then Y)

$2006

PPU_ADDR

write 2x

PPU Address \newline (write upper then lower)

$2007

PPU_DATA

read/write

PPU Data

$4014

OAM_DMA

write

Sprite Page DMA Transfer

PPU_CTRL Bits#

Bit

Mask

Description

0-1

0x3

Nametable page select \newline (0=$2000, 1=$2400, 2=$2800, 3=$2c00)

2

0x4

VRAM address increment (0=1, 1=32)

3

0x8

8x8 sprite pattern address (0=$0, 1=$1000)

4

0x10

Background pattern address (0=$0, 1=$1000)

5

0x20

Sprite size (0=8x8, 1=8x16)

6

0x40

Master/slave mode (unused)

7

0x80

NMI enable at VBLANK (0=off, 1=on)

PPU_MASK Bits#

Bit

Mask

Description

0

0x1

Greyscale (0=color, 1=greyscale)

1

0x2

Show background in leftmost 8 pixels

2

0x4

Show sprites in leftmost 8 pixels

3

0x8

Show background

4

0x10

Show sprites

5

0x20

Red emphasis

6

0x40

Green emphasis

7

0x80

Blue emphasis

Palette RAM#

Start

End

Description

$3F00

$3F00

Screen color

$3F01

$3F03

Background palette 0

$3F05

$3F07

Background palette 1

$3F09

$3F0B

Background palette 2

$3F0D

$3F0F

Background palette 3

$3F11

$3F13

Sprite palette 0

$3F15

$3F17

Sprite palette 1

$3F19

$3F1B

Sprite palette 2

$3F1D

$3F1F

Sprite palette 3

APU Registers#

Address

Name

Bits

Description

$4000

SQ1_VOL

ddlcvvvv

Square wave 1, duty and volume

$4001

SQ1_SWEEP

epppnsss

Square wave 1, sweep

$4002

SQ1_LO

pppppppp

Square wave 1, period (LSB)

$4003

SQ1_HI

xxxxxppp

Square wave 1, period (MSB) and counter load

$4004

SQ2_VOL

dd..vvvv

Square wave 2, duty and volume

$4005

SQ2_SWEEP

epppnsss

Square wave 2, sweep

$4006

SQ2_LO

pppppppp

Square wave 2, period (LSB)

$4007

SQ2_HI

xxxxxppp

Square wave 2, period (MSB) and counter load

$4008

TRI_LINEAR

crrrrrrr

Triangle wave, control and counter load

$400A

TRI_LO

pppppppp

Triangle wave, period (LSB)

$400B

TRI_HI

xxxxxppp

Triangle wave, period (MSB) and counter load

$400C

NOISE_VOL

..lcvvvv

Noise generator, flags and volume

$400E

NOISE_CTRL

t...pppp

Noise generator, tone and period

$400F

NOISE_LEN

lllll...

Noise generator, counter load

$4010

DMC_FREQ

il..rrrr

DMC: IRQ, flags, and rate

$4011

DMC_RAW

.xxxxxxx

DMC: direct load

$4012

DMC_START

aaaaaaaa

DMC, waveform start address

$4013

DMC_LEN

llllllll

DMC, waveform length

$4015

SND_CHN

...dnt21

Sound channel enable

$4017

JOY2

mi......

Frame counter mode and IRQ

$4015

SND_CHN

if.dnt21

DMC/frame interrupt and status (read)

$4016

JOY1

...xxxxd

Joystick 1 (read)

$4017

JOY2

...xxxxd

Joystick 2 (read)

Assembler Reference#

nesdefs.dasm#

NES_HEADER mapper prgbanks chrbanks mirroring

Emits a iNES cartridge header before the start of program. The mirroring parameter can be NES_MIRR_HORIZ, NES_MIRR_VERT, or NES_MIRR_QUAD.

NES_INIT

Standardized start-up code.

NES_VECTORS

Emits 6 bytes of CPU vectors at address $FFFA.

SLEEP n

Sleep n cycles.

PPU_SETADDR addr

Set the 16-bit PPU_ADDR address register.

PPU_SETVALUE data

Send a constant byte to PPU_DATA.

SAVE_REGS

Push A, X, and Y to stack.

RESTORE_REGS

Pop A, X, and Y from stack.

nesppu.dasm#

ClearRAM

Clear all RAM, except for last 2 bytes of CPU stack.

WaitSync

Wait for vertical sync to start.

NextRandom

Return a random number in A register.

PrevRandom

Return a random number in A register.

ReadJoypadY

Return either joypad’s bits in A register. Y must be set to 0 or 1.

C Reference#

apu.h#

Macros and functions for programming the APU sound generator.

apu_init()

initialize APU with default state

Enable an audio channel:

APU_ENABLE(flags)

flags := ENABLE_PULSE0 | ENABLE_PULSE1 | ENABLE_TRIANGLE | ENABLE_NOISE | ENABLE_DMC

Pulse channels:

APU_PULSE_DECAY(channel,period,duty,decay,len)
APU_PULSE_SUSTAIN(channel,period,duty,vol)
APU_PULSE_SET_DECAY(channel,duty,decay)
APU_PULSE_SET_VOLUME(channel,duty,vol)
APU_PULSE_SWEEP(channel,period,shift,up)
APU_PULSE_SWEEP_DISABLE(channel)

channel := PULSE_CH0 | PULSE_CH1
duty := DUTY_75 | DUTY_50 | DUTY_25 | DUTY_12

Triangle channel:

APU_TRIANGLE_LENGTH(period,len)
APU_TRIANGLE_SUSTAIN(period)

Noise channel:

APU_NOISE_SUSTAIN(_period,vol)
APU_NOISE_DECAY(_period,_decay,_len)

bcd.h#

Binary-encoded decimal math.

unsigned int bcd_add(unsigned int a, unsigned int b)

add two BCD-encoded 16-bit numbers

neslib.h#

pal_all(const char *data)

set bg and spr palettes, data is 32 bytes array

pal_bg(const char *data)

set bg palette only, data is 16 bytes array

pal_spr(const char *data)

set spr palette only, data is 16 bytes array

pal_col(unsigned char index, unsigned char color)

set a palette entry, index is 0..31

pal_clear()

reset palette to $0f

pal_bright(unsigned char bright)

set virtual bright both for sprites and background, 0 is black, 4 is normal, 8 is white

pal_spr_bright(unsigned char bright)

set virtual bright for sprites only

pal_bg_bright(unsigned char bright)

set virtual bright for sprites background only

ppu_wait_nmi()

wait actual TV frame, 50hz for PAL, 60hz for NTSC

ppu_wait_frame()

wait virtual frame, it is always 50hz, frame-to-frame in PAL, frameskip in NTSC

ppu_off()

turn off rendering, nmi still enabled when rendering is disabled

ppu_on_all()

turn on bg, spr

ppu_on_bg()

turn on bg only

ppu_on_spr()

turn on spr only

ppu_mask(unsigned char mask)

set PPU_MASK directly

unsigned char ppu_system()

get current video system, 0 for PAL, not 0 for NTSC

unsigned char nesclock()

Return an 8-bit counter incremented at each vblank

unsigned char get_ppu_ctrl_var()

get/set the internal ppu ctrl cache var for manual writing

set_ppu_ctrl_var(unsigned char var)

set PPU control byte

oam_clear()

clear OAM buffer, all the sprites are hidden

oam_size(unsigned char size)

set sprite display mode, 0 for 8x8 sprites, 1 for 8x16 sprites

oam_hide_rest(unsigned char sprid)

hide all remaining sprites from given offset

oam_spr(x, y, chrnum, attr, sprid)

set sprite in OAM buffer – chrnum is tile, attr is attribute, sprid is offset in OAM in bytes returns sprid+4, which is offset for a next sprite

oam_meta_spr(x, y, sprid, data)

set metasprite in OAM buffer – meta sprite is a const unsigned char array, it contains four bytes per sprite in order x offset, y offset, tile, attribute x=128 is end of a meta sprite returns sprid+4, which is offset for a next sprite

famitone_init(void* music_data)

initialize the FamiTone system

sfx_init(void* sounds_data)

initialize the FamiTone SFX system

music_play(unsigned char song)

play a music in FamiTone format

music_stop()

stop music

music_pause(unsigned char pause)

pause and unpause music

sfx_play(unsigned char sound, unsigned char channel)

play FamiTone sound effect on channel 0..3

sample_play(unsigned char sample)

play a DPCM sample, 1..63

famitone_update()

call from NMI once per frame

unsigned char pad_poll(unsigned char pad)

poll controller and return flags like PAD_LEFT etc, input is pad number (0 or 1)

unsigned char pad_trigger(unsigned char pad)

poll controller in trigger mode, a flag is set only on button down, not hold if you need to poll the pad in both normal and trigger mode, poll it in the trigger mode for first, then use pad_state

unsigned char pad_state(unsigned char pad)

get previous pad state without polling ports

scroll(unsigned int x, unsigned int y)

set scroll, including the top bits it is always applied at beginning of a TV frame, not at the function call

split(unsigned int x, unsigned int y)

set scroll after screen split invoked by the sprite 0 hit warning: all CPU time between the function call and the actual split point will be wasted! warning: the program loop has to fit into the frame time, ppu_wait_frame should not be used otherwise empty frames without split will be inserted, resulting in jumpy screen warning: only X scroll could be changed in this version

splitxy(unsigned int x, unsigned int y)

set scroll after screen split invoked by the sprite 0 hit sets both X and Y, but timing might be iffy depending on exact sprite 0 position

bank_spr(unsigned char n)

select current chr bank for sprites, 0..1

bank_bg(unsigned char n)

select current chr bank for background, 0..1

set_rand(unsigned int seed)

get random number 0..255 or 0..65535 set random seed

vram_adr(unsigned int adr)

set vram pointer to write operations if you need to write some data to vram

vram_put(unsigned char n)

put a byte at current vram address, works only when rendering is turned off

vram_fill(unsigned char n, unsigned int len)

fill a block with a byte at current vram address, works only when rendering is turned off

vram_inc(unsigned char n)

set vram autoincrement, 0 for +1 and not 0 for +32

vram_read(unsigned char *dst, unsigned int size)

read a block from current address of vram, works only when rendering is turned off

vram_write(const unsigned char *src, unsigned int size)

write a block to current address of vram, works only when rendering is turned off

vram_unrle(const unsigned char *data)

unpack RLE data to current address of vram, mostly used for nametables

vram_unlz4(const unsigned char *in, unsigned char *out, const unsigned uncompressed_size)

unpack LZ4 data to this address

memfill(*dst, unsigned char value, unsigned int len)

like memset, but does not return anything

delay(unsigned char frames)

delay for N frames

oam_clear_fast()

clear OAM memory oam_meta_spr_pal(unsigned char x,unsigned char y,unsigned char pal,const unsigned char *metasprite)

set metasprite with palette color oam_meta_spr_clip(signed int x,unsigned char y,const unsigned char *metasprite)

set metasprite with clipping

nmi_set_callback(void (*callback)(void))

set NMI interrupt callback function

Gamepad flags:

PAD_A PAD_B PAD_SELECT PAD_START PAD_UP PAD_DOWN PAD_LEFT PAD_RIGHT

OAM flags:

OAM_FLIP_V OAM_FLIP_H OAM_BEHIND

MAX(x1,x2)

get maximum of two integers

MIN(x1,x2)

get minimum of two integers

Mask flags:

MASK_SPR MASK_BG MASK_EDGE_SPR MASK_EDGE_BG MASK_TINT_RED MASK_TINT_BLUE MASK_TINT_GREEN MASK_MONO

Nametable constants:

NAMETABLE_A NAMETABLE_B NAMETABLE_C NAMETABLE_D

NTADR_A(x,y)

macro to calculate nametable address from X,Y in compile time NTADR_B(x,y)

macro to calculate nametable address from X,Y in compile time NTADR_C(x,y)

macro to calculate nametable address from X,Y in compile time NTADR_D(x,y)

macro to calculate nametable address from X,Y in compile time

MSB(x)

macro to get MSB (high byte) LSB(x)

macro to get LSB (low byte)

OAMBUF

OAM buffer @ $200-$2FF

oam_off

internal OAM offset variable

VRAM Update Buffer#

set_vram_update(unsigned char *buf)

The function sets a pointer to the update buffer that contains data and addresses in a special format. It allows to write non-sequental bytes, as well as horizontal or vertical nametable sequences. Buffer pointer could be changed during rendering, but it only takes effect on a new frame. Number of transferred bytes is limited by vblank time. To disable updates, call this function with NULL pointer.

flush_vram_update(unsigned char *buf)

The same format as for set_vram_update, but writes done right away, instead of waiting for NMI interrupt.

The update data format:

  • MSB, LSB, byte for a non-sequental write

  • MSB|NT_UPD_HORZ, LSB, LEN, [bytes] for a horizontal sequence

  • MSB|NT_UPD_VERT, LSB, LEN, [bytes] for a vertical sequence

  • NT_UPD_EOF to mark end of the buffer

vrambuf.h#

VBUFSIZE

maximum update buffer bytes

updbuf

update buffer starts at $100 (stack page)

VRAMBUF_SET(byte)

set byte at end of buffer (do not advance pointer) VRAMBUF_ADD(byte)

add byte to end of buffer VRAMBUF_PUT(addr,len,flags)

macro to add a raw header (useful for single bytes) VRAMBUF_VERT

OR with address to put vertical run

vrambuf_end()

add EOF marker to buffer (but don’t increment pointer)

vrambuf_clear()

clear vram buffer and place EOF marker

vrambuf_flush()

wait for next frame, then clear buffer this assumes the NMI will call flush_vram_update()

vrambuf_put(word addr, const char* str, byte len)

add multiple characters to update buffer using horizontal increment

Comments