Nintendo Entertainment System (NES)#
Open 8bitworkshop IDE
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.
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 |
---|---|---|
|
|
RAM, zero-page |
|
|
RAM, CPU stack |
|
|
RAM, general-purpose |
|
|
(mirror of |
|
|
PPU registers |
|
|
(mirror of |
|
|
APU registers |
|
|
DMC, joystick, APU registers |
|
|
Cartridge (maybe mapper registers) |
|
|
Cartridge RAM (maybe battery-backed) |
|
|
PRG ROM (maybe bank switched) |
|
|
NMI vector |
|
|
Reset vector |
|
|
BRK vector |
PPU Registers#
Address |
Name |
R/W |
Description |
---|---|---|---|
|
|
write |
PPU Control 1 |
|
|
write |
PPU Control 2 |
|
|
read |
PPU Status |
|
|
write |
OAM Address |
|
|
read/write |
OAM Data |
|
|
write 2x |
Background Scroll Position \newline (write X then Y) |
|
|
write 2x |
PPU Address \newline (write upper then lower) |
|
|
read/write |
PPU Data |
|
|
write |
Sprite Page DMA Transfer |
PPU_CTRL Bits#
Bit |
Mask |
Description |
---|---|---|
0-1 |
0x3 |
Nametable page select \newline ( |
2 |
0x4 |
VRAM address increment (0=1, 1=32) |
3 |
0x8 |
8x8 sprite pattern address ( |
4 |
0x10 |
Background pattern address ( |
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 |
---|---|---|
|
|
Screen color |
|
|
Background palette 0 |
|
|
Background palette 1 |
|
|
Background palette 2 |
|
|
Background palette 3 |
|
|
Sprite palette 0 |
|
|
Sprite palette 1 |
|
|
Sprite palette 2 |
|
|
Sprite palette 3 |
APU Registers#
Address |
Name |
Bits |
Description |
---|---|---|---|
|
|
|
Square wave 1, duty and volume |
|
|
|
Square wave 1, sweep |
|
|
|
Square wave 1, period (LSB) |
|
|
|
Square wave 1, period (MSB) and counter load |
|
|
|
Square wave 2, duty and volume |
|
|
|
Square wave 2, sweep |
|
|
|
Square wave 2, period (LSB) |
|
|
|
Square wave 2, period (MSB) and counter load |
|
|
|
Triangle wave, control and counter load |
|
|
|
Triangle wave, period (LSB) |
|
|
|
Triangle wave, period (MSB) and counter load |
|
|
|
Noise generator, flags and volume |
|
|
|
Noise generator, tone and period |
|
|
|
Noise generator, counter load |
|
|
|
DMC: IRQ, flags, and rate |
|
|
|
DMC: direct load |
|
|
|
DMC, waveform start address |
|
|
|
DMC, waveform length |
|
|
|
Sound channel enable |
|
|
|
Frame counter mode and IRQ |
|
|
|
DMC/frame interrupt and status (read) |
|
|
|
Joystick 1 (read) |
|
|
|
Joystick 2 (read) |
Assembler Reference#
nesdefs.dasm#
NES_HEADER
mapper prgbanks chrbanks mirroringEmits a iNES cartridge header before the start of program. The
mirroring
parameter can beNES_MIRR_HORIZ
,NES_MIRR_VERT
, orNES_MIRR_QUAD
.NES_INIT
Standardized start-up code.
NES_VECTORS
Emits 6 bytes of CPU vectors at address
$FFFA
.SLEEP
nSleep n cycles.
PPU_SETADDR
addrSet the 16-bit
PPU_ADDR
address register.PPU_SETVALUE
dataSend 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 writeMSB|NT_UPD_HORZ, LSB, LEN, [bytes]
for a horizontal sequenceMSB|NT_UPD_VERT, LSB, LEN, [bytes]
for a vertical sequenceNT_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
Helpful Links#
8bitworkshop fork of NESLib – Use this library for building your project outside of 8bitworkshop.