Compare commits
10 Commits
85c6a414f9
...
e14776a473
Author | SHA1 | Date | |
---|---|---|---|
e14776a473 | |||
2084552c53 | |||
f591b4adf2 | |||
279478f16f | |||
c0679b404d | |||
27b9d4c120 | |||
97332c8a38 | |||
362341eac3 | |||
7d148924e2 | |||
2df7df5f5b |
4
Makefile
4
Makefile
@ -1,5 +1,7 @@
|
||||
CFLAGS = -O2 -Wall -Wextra -pedantic -Wno-unused-parameter
|
||||
|
||||
nes:
|
||||
${CC} -o nes cpu.c rom.c
|
||||
${CC} ${CFLAGS} -o nes cpu.c rom.c ppu.c apu.c
|
||||
|
||||
test: nes
|
||||
./nes ${HOME}/src/other/nes-test-roms/other/nestest.nes
|
||||
|
29
README.md
29
README.md
@ -1,6 +1,29 @@
|
||||
emu_nes
|
||||
=======
|
||||
# emu_nes
|
||||
A Nintendo Entertainment System (NES) emulator written for the purpose of
|
||||
understanding how the NES and similar 8-bit computers of that era worked.
|
||||
|
||||
Makes heavy use of https://www.nesdev.org/.
|
||||
The NES used a variant of the MOS 6502 8-bit microcontroller called a
|
||||
Ricoh 2A03 that contained the 6502 without the BCD mode for arithmetic,
|
||||
an audio signal generator (APU), controlling the gamepads, and for DMA.
|
||||
([Source: NESdev](https://www.nesdev.org/wiki/DMA))
|
||||
|
||||
A major goal for the emulator is finishing the software implementation
|
||||
in C and later implement this in hardware with either an FPGA or another
|
||||
CPU like a RISC-V. Later, I want to generalize the 2A03 simulator into
|
||||
a full 6502 for use with a Commodore 64 and Apple II.
|
||||
|
||||
Makes heavy use of the [NESdev wiki](https://www.nesdev.org/wiki).
|
||||
|
||||
## Current State
|
||||
- [x] CPU (Ricoh 2A03, variant of MOS 6502)
|
||||
- [ ] PPU (Graphics)
|
||||
- [ ] APU (Audio)
|
||||
|
||||
Currently, running the emulator with nestest.nes results in incorrect
|
||||
results in the memory space of the APU registers. This is due to the
|
||||
APU not being implemented yet.
|
||||
|
||||
Also, the only ROM mapper that is implemented is Mapper 0 to
|
||||
run the test ROM, but it also supports many other ROMs listed
|
||||
[here](https://nesdir.github.io/mapper0.html) such as Donkey Kong and
|
||||
Super Mario Bros.
|
||||
|
55
apu.c
Normal file
55
apu.c
Normal file
@ -0,0 +1,55 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "apu.h"
|
||||
|
||||
struct apu apu = {0};
|
||||
|
||||
void
|
||||
apu_init(void)
|
||||
{
|
||||
memset(&apu, 0, sizeof(apu));
|
||||
}
|
||||
|
||||
uint8_t
|
||||
apu_read(uint16_t addr)
|
||||
{
|
||||
switch (addr) {
|
||||
/* pulse1 */
|
||||
case 0x4000:
|
||||
return (apu.pulse1.duty << 6) | (apu.pulse1.envelope << 5)
|
||||
| (apu.pulse1.const_vol << 4) | apu.pulse1.envelope_vol;
|
||||
case 0x4001:
|
||||
return (apu.pulse1.sweep_enable << 7) | (apu.pulse1.sweep_period << 4)
|
||||
| (apu.pulse1.sweep_negative << 3) | apu.pulse1.sweep_shift_count;
|
||||
case 0x4002:
|
||||
return apu.pulse1.timer_low;
|
||||
case 0x4003:
|
||||
return (apu.pulse1.length_counter_load << 3) | apu.pulse1.timer_high;
|
||||
/* pulse2 */
|
||||
case 0x4004:
|
||||
return (apu.pulse2.duty << 6) | (apu.pulse2.envelope << 5)
|
||||
| (apu.pulse2.const_vol << 4) | apu.pulse2.envelope_vol;
|
||||
case 0x4005:
|
||||
return (apu.pulse1.sweep_enable << 7) | (apu.pulse1.sweep_period << 4)
|
||||
| (apu.pulse1.sweep_negative << 3) | apu.pulse1.sweep_shift_count;
|
||||
case 0x4006:
|
||||
return apu.pulse1.timer_low;
|
||||
case 0x4007:
|
||||
return (apu.pulse1.length_counter_load << 3) | apu.pulse1.timer_high;
|
||||
/* status */
|
||||
case 0x4015:
|
||||
return (apu.status.dmc_int << 7) | (apu.status.frame_int << 6)
|
||||
| (apu.status.dmc_active << 4) | (apu.status.lc_noise << 3)
|
||||
| (apu.status.lc_triangle << 2) | (apu.status.lc_pulse2 << 1)
|
||||
| apu.status.lc_pulse1;
|
||||
default:
|
||||
fprintf(stderr, "Invalid APU read at $%04X!\n", addr);
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
void
|
||||
apu_write(uint16_t addr, uint8_t byte)
|
||||
{
|
||||
}
|
105
apu.h
Normal file
105
apu.h
Normal file
@ -0,0 +1,105 @@
|
||||
#ifndef APU_H
|
||||
#define APU_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct pulse {
|
||||
/* $4000/$4004 */
|
||||
uint8_t duty : 2;
|
||||
uint8_t envelope : 1;
|
||||
uint8_t const_vol : 1;
|
||||
uint8_t envelope_vol : 4;
|
||||
/* $4001/$4005 */
|
||||
uint8_t sweep_enable : 1;
|
||||
uint8_t sweep_period : 3;
|
||||
uint8_t sweep_negative : 1;
|
||||
uint8_t sweep_shift_count : 3;
|
||||
/* $4002/$4006 */
|
||||
uint8_t timer_low;
|
||||
/* $4003/$4007 */
|
||||
uint8_t length_counter_load : 5;
|
||||
uint8_t timer_high : 3;
|
||||
};
|
||||
|
||||
struct triangle {
|
||||
/* $4008 */
|
||||
uint8_t counter_disable : 1;
|
||||
uint8_t counter_reload : 7;
|
||||
/* $400A */
|
||||
uint8_t timer_low;
|
||||
/* $400B */
|
||||
uint8_t counter_load : 5;
|
||||
uint8_t timer_high : 3;
|
||||
};
|
||||
|
||||
struct noise {
|
||||
/* $400C */
|
||||
uint8_t padding1 : 2;
|
||||
uint8_t loop_envelope : 1;
|
||||
uint8_t constant_volume : 1;
|
||||
uint8_t envelope_volume : 4;
|
||||
/* $400E */
|
||||
uint8_t loop_noise : 1;
|
||||
uint8_t padding2 : 3;
|
||||
uint8_t noise_period : 4;
|
||||
/* $400F */
|
||||
uint8_t length_counter_load : 5;
|
||||
uint8_t padding3 : 3;
|
||||
};
|
||||
|
||||
struct dmc {
|
||||
/* $4010 */
|
||||
uint8_t irq_enable : 1;
|
||||
uint8_t loop : 1;
|
||||
uint8_t padding1 : 2;
|
||||
uint8_t frequency : 4;
|
||||
/* $4011 */
|
||||
uint8_t padding2 : 1;
|
||||
uint8_t load_counter : 7;
|
||||
/* $4012 */
|
||||
uint8_t sample_addr;
|
||||
/* $4013 */
|
||||
uint8_t sample_length;
|
||||
};
|
||||
|
||||
struct apu {
|
||||
/* $4000-$4013 (write) */
|
||||
struct pulse pulse1;
|
||||
struct pulse pulse2;
|
||||
struct triangle triangle;
|
||||
struct noise noise;
|
||||
struct dmc dmc;
|
||||
/* $4015 (write) */
|
||||
struct control {
|
||||
uint8_t padding : 3;
|
||||
uint8_t dmc_enable : 1;
|
||||
uint8_t noise_enable : 1;
|
||||
uint8_t triangle_enable : 1;
|
||||
uint8_t pulse2_enable : 1;
|
||||
uint8_t pulse1_enable : 1;
|
||||
} control;
|
||||
/* $4015 (read) */
|
||||
struct status {
|
||||
uint8_t dmc_int : 1;
|
||||
uint8_t frame_int : 1;
|
||||
uint8_t padding : 1;
|
||||
uint8_t dmc_active : 1;
|
||||
uint8_t lc_noise : 1;
|
||||
uint8_t lc_triangle : 1;
|
||||
uint8_t lc_pulse2 : 1;
|
||||
uint8_t lc_pulse1 : 1;
|
||||
} status;
|
||||
/* $4017 (write) */
|
||||
struct frame_counter {
|
||||
uint8_t mode : 1;
|
||||
uint8_t irq_inhibit : 1;
|
||||
uint8_t padding : 6;
|
||||
} frame_counter;
|
||||
};
|
||||
extern struct apu apu;
|
||||
|
||||
void apu_tick(void);
|
||||
uint8_t apu_read(uint16_t addr);
|
||||
void apu_write(uint16_t addr, uint8_t byte);
|
||||
|
||||
#endif /* APU_H */
|
45
cpu.c
45
cpu.c
@ -5,8 +5,10 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "apu.h"
|
||||
#include "cpu.h"
|
||||
#include "opcodes.h"
|
||||
#include "ppu.h"
|
||||
#include "rom.h"
|
||||
|
||||
#define MAX(a, b) ((a > b) ? a : b)
|
||||
@ -26,16 +28,14 @@
|
||||
|
||||
#define MEMORY_MIRROR(addr) \
|
||||
if (addr < 0x2000) \
|
||||
addr &= 0x07FF; \
|
||||
else if (addr < 0x4000) \
|
||||
addr &= 0x2007;
|
||||
addr &= 0x07FF;
|
||||
|
||||
#define PUSH(b) \
|
||||
(memwrite(0x0100 + regs.sp--, b))
|
||||
#define PULL() \
|
||||
(peek(0x0100 + ++regs.sp))
|
||||
|
||||
struct Rom rom = {0};
|
||||
struct rom rom = {0};
|
||||
uint32_t cycles = 0;
|
||||
|
||||
bool page_crossed = false;
|
||||
@ -52,9 +52,10 @@ peek(uint16_t addr)
|
||||
return rom.prg_rom[addr - 0x8000];
|
||||
else
|
||||
fprintf(stderr, "PRG ROG size is not 0x4000 nor 0x8000\n"), exit(1);
|
||||
} else {
|
||||
} else if ((addr >= 0x4000 && addr <= 0x4013) || addr == 0x4015 || addr == 0x4017)
|
||||
return apu_read(addr);
|
||||
else
|
||||
return memory[addr];
|
||||
}
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
@ -67,6 +68,11 @@ peek16(uint16_t addr)
|
||||
static void
|
||||
memwrite(uint16_t addr, uint8_t byte)
|
||||
{
|
||||
if (addr >= 0x4000 && addr <= 0x4017) {
|
||||
apu_write(addr, byte);
|
||||
return;
|
||||
}
|
||||
|
||||
MEMORY_MIRROR(addr);
|
||||
|
||||
memory[addr] = byte;
|
||||
@ -145,15 +151,24 @@ opcode_mem(enum addressing_mode mode)
|
||||
return val;
|
||||
}
|
||||
|
||||
static void
|
||||
tick(void)
|
||||
{
|
||||
cycles++;
|
||||
ppu_tick();
|
||||
ppu_tick();
|
||||
ppu_tick();
|
||||
}
|
||||
|
||||
static void
|
||||
branch(uint16_t addr, bool cond)
|
||||
{
|
||||
if (!cond)
|
||||
return;
|
||||
cycles++;
|
||||
tick();
|
||||
|
||||
if (((regs.pc + 1) & 0xFF00) != (addr & 0xFF00))
|
||||
cycles++;
|
||||
tick();
|
||||
|
||||
regs.pc = addr;
|
||||
}
|
||||
@ -946,18 +961,21 @@ interpret(void)
|
||||
|
||||
while (spaces--) putchar(' ');
|
||||
|
||||
printf("A:%02X X:%02X Y:%02X P:%02X SP:%02X CYC:%d\n",
|
||||
regs.a, regs.x, regs.y, STATUS_TO_INT(), regs.sp, cycles);
|
||||
printf("A:%02X X:%02X Y:%02X P:%02X SP:%02X PPU:%3d,%3d CYC:%d\n",
|
||||
regs.a, regs.x, regs.y, STATUS_TO_INT(), regs.sp,
|
||||
ppu.scanlines, ppu.cycles, cycles);
|
||||
|
||||
if (opcodes[op].memread)
|
||||
opcodes[op].instr(peek(arg));
|
||||
else
|
||||
opcodes[op].instr(arg);
|
||||
|
||||
cycles += opcodes[op].cycles;
|
||||
for (uint8_t i = 0; i < opcodes[op].cycles; i++)
|
||||
tick();
|
||||
|
||||
if (page_crossed) {
|
||||
if (opcodes[op].page_cross)
|
||||
cycles++;
|
||||
tick();
|
||||
page_crossed = false;
|
||||
}
|
||||
}
|
||||
@ -976,6 +994,8 @@ cpu_init(void)
|
||||
regs.status.unused = 1;
|
||||
|
||||
cycles += 7;
|
||||
for (uint8_t i = 0; i < 7 * 3; i++)
|
||||
ppu_tick();
|
||||
}
|
||||
|
||||
int
|
||||
@ -1005,6 +1025,7 @@ main(int argc, char *argv[])
|
||||
|
||||
parse_rom(buf, buflen, &rom);
|
||||
free(buf);
|
||||
ppu.rom = &rom;
|
||||
|
||||
cpu_init();
|
||||
|
||||
|
156
ppu.c
Normal file
156
ppu.c
Normal file
@ -0,0 +1,156 @@
|
||||
#include "ppu.h"
|
||||
|
||||
struct ppu ppu = {0};
|
||||
|
||||
static void
|
||||
vram_addr_inc(void)
|
||||
{
|
||||
ppu.regs.v += (ppu.ctrl.inc_mode) ? 32 : 1;
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
vram_addr_mirror(uint16_t addr)
|
||||
{
|
||||
uint8_t nametable;
|
||||
|
||||
addr &= 0x2fff;
|
||||
addr -= 0x2000;
|
||||
nametable = addr / 0x400;
|
||||
|
||||
switch (ppu.rom->mirror) {
|
||||
case M_HORIZONTAL:
|
||||
if (nametable == 1 || nametable == 2)
|
||||
return addr - 0x400;
|
||||
else if (nametable == 3)
|
||||
return addr - 0x800;
|
||||
break;
|
||||
case M_VERTICAL:
|
||||
if (nametable == 2 || nametable == 3)
|
||||
return addr - 0x800;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
void
|
||||
ppu_tick(void)
|
||||
{
|
||||
ppu.cycles++;
|
||||
|
||||
if (ppu.cycles > 256 && ppu.cycles <= 320)
|
||||
ppu.oam_addr = 0;
|
||||
|
||||
if (ppu.cycles >= 341) {
|
||||
ppu.cycles -= 341;
|
||||
ppu.scanlines++;
|
||||
}
|
||||
if (ppu.scanlines >= 262)
|
||||
ppu.scanlines -= 262;
|
||||
}
|
||||
|
||||
uint8_t
|
||||
ppu_read(uint16_t addr)
|
||||
{
|
||||
uint8_t tmp;
|
||||
|
||||
switch (addr) {
|
||||
case 0x2002:
|
||||
tmp = (ppu.status.vblank << 7)
|
||||
| (ppu.status.sprite0_hit << 6)
|
||||
| (ppu.status.sprite_overflow << 5);
|
||||
|
||||
ppu.status.vblank = 0;
|
||||
ppu.regs.write_toggle = 0;
|
||||
|
||||
return tmp;
|
||||
case 0x2004:
|
||||
return ppu.oam[ppu.oam_addr];
|
||||
case 0x2007:
|
||||
tmp = ppu.last_read;
|
||||
|
||||
if (addr <= 0x1fff)
|
||||
ppu.last_read = ppu.rom->chr_rom[ppu.regs.v];
|
||||
else if (addr <= 0x2fff)
|
||||
ppu.last_read = ppu.vram[vram_addr_mirror(addr)];
|
||||
|
||||
vram_addr_inc();
|
||||
|
||||
return tmp;
|
||||
default:
|
||||
fprintf(stderr, "Invalid PPU read at address $%04\n", addr);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ppu_write(uint16_t addr, uint8_t byte)
|
||||
{
|
||||
switch (addr) {
|
||||
case 0x2000:
|
||||
ppu.ctrl.nametable_base = byte & 3;
|
||||
ppu.ctrl.inc_mode = byte & (1 << 2);
|
||||
ppu.ctrl.sprite_tile_sel = byte & (1 << 3);
|
||||
ppu.ctrl.bg_tile_sel = byte & (1 << 4);
|
||||
ppu.ctrl.sprite_height = byte & (1 << 5);
|
||||
ppu.ctrl.master_slave = byte & (1 << 6);
|
||||
ppu.ctrl.nmi_enable = byte & (1 << 7);
|
||||
|
||||
if (ppu.ctrl.nametable_base & 1)
|
||||
ppu.regs.scroll_x += 256;
|
||||
if (ppu.ctrl.nametable_base & 2)
|
||||
ppu.regs.scroll_y += 240;
|
||||
|
||||
/*
|
||||
* TODO: send NMI if vblank flag still set and nmi_enable
|
||||
* changes from 0 to 1
|
||||
*/
|
||||
break;
|
||||
case 0x2001:
|
||||
ppu.mask.grayscale = byte & 1;
|
||||
ppu.mask.bg_left_enable = byte & (1 << 1);
|
||||
ppu.mask.sprite_left_enable = byte & (1 << 2);
|
||||
ppu.mask.bg_enable = byte & (1 << 3);
|
||||
ppu.mask.sprite_enable = byte & (1 << 4);
|
||||
ppu.mask.colour_emphasis = byte & (1 << 5);
|
||||
|
||||
break;
|
||||
case 0x2003:
|
||||
ppu.oam_addr = byte;
|
||||
|
||||
break;
|
||||
case 0x2004:
|
||||
ppu.oam[ppu.oam_addr++] = byte;
|
||||
|
||||
break;
|
||||
case 0x2005:
|
||||
if (ppu.regs.write_toggle == 0)
|
||||
ppu.regs.scroll_x = byte;
|
||||
else
|
||||
ppu.regs.scroll_y = byte;
|
||||
ppu.regs.write_toggle = !ppu.regs.write_toggle;
|
||||
|
||||
break;
|
||||
case 0x2006:
|
||||
if (ppu.regs.write_toggle == 0)
|
||||
ppu.regs.v = (byte << 8);
|
||||
else {
|
||||
ppu.regs.v |= byte;
|
||||
ppu.regs.v &= 0x3fff;
|
||||
}
|
||||
ppu.regs.write_toggle = !ppu.regs.write_toggle;
|
||||
|
||||
break;
|
||||
case 0x2007:
|
||||
ppu.vram[ppu.regs.v] = byte;
|
||||
vram_addr_inc();
|
||||
|
||||
break;
|
||||
case 0x4014:
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Invalid PPU write at address $%04X\n", addr);
|
||||
return;
|
||||
}
|
||||
}
|
55
ppu.h
Normal file
55
ppu.h
Normal file
@ -0,0 +1,55 @@
|
||||
#ifndef PPU_H
|
||||
#define PPU_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "rom.h"
|
||||
|
||||
struct ppu {
|
||||
uint8_t vram[2048];
|
||||
uint8_t oam[64*4];
|
||||
uint8_t oam_addr; /* $2003 */
|
||||
uint8_t palette[16*2];
|
||||
uint16_t cycles, scanlines;
|
||||
struct rom *rom;
|
||||
|
||||
uint8_t last_read;
|
||||
|
||||
struct ppu_regs {
|
||||
uint16_t v : 15; /* current vram address */
|
||||
uint16_t t : 15; /* temporary vram address */
|
||||
uint16_t scroll_x : 9; /* x scroll position */
|
||||
uint16_t scroll_y : 9; /* y scroll position */
|
||||
uint8_t write_toggle : 1; /* first/second write toggle */
|
||||
} regs;
|
||||
|
||||
struct ppu_ctrl { /* $2000 */
|
||||
uint8_t nmi_enable : 1;
|
||||
uint8_t master_slave : 1;
|
||||
uint8_t sprite_height : 1;
|
||||
uint8_t bg_tile_sel : 1;
|
||||
uint8_t sprite_tile_sel : 1;
|
||||
uint8_t inc_mode : 1;
|
||||
uint8_t nametable_base : 2;
|
||||
} ctrl;
|
||||
struct ppu_mask { /* $2001 */
|
||||
uint8_t colour_emphasis : 3;
|
||||
uint8_t sprite_enable : 1;
|
||||
uint8_t bg_enable : 1;
|
||||
uint8_t sprite_left_enable : 1;
|
||||
uint8_t bg_left_enable : 1;
|
||||
uint8_t grayscale : 1;
|
||||
} mask;
|
||||
struct ppu_status { /* $2002 */
|
||||
uint8_t vblank : 1;
|
||||
uint8_t sprite0_hit : 1;
|
||||
uint8_t sprite_overflow : 1;
|
||||
uint8_t pad : 5;
|
||||
} status;
|
||||
};
|
||||
extern struct ppu ppu;
|
||||
|
||||
void ppu_tick(void);
|
||||
|
||||
#endif /* PPU_H */
|
4
rom.c
4
rom.c
@ -6,7 +6,7 @@
|
||||
#include "rom.h"
|
||||
|
||||
void
|
||||
parse_rom(const uint8_t *data, size_t data_len, struct Rom *rom)
|
||||
parse_rom(const uint8_t *data, size_t data_len, struct rom *rom)
|
||||
{
|
||||
size_t prg_rom_offset = 16;
|
||||
size_t chr_rom_offset = 0;
|
||||
@ -48,7 +48,7 @@ parse_rom(const uint8_t *data, size_t data_len, struct Rom *rom)
|
||||
}
|
||||
|
||||
void
|
||||
free_rom(struct Rom *rom)
|
||||
free_rom(struct rom *rom)
|
||||
{
|
||||
free(rom->prg_rom);
|
||||
if (rom->chr_rom_size > 0)
|
||||
|
11
rom.h
11
rom.h
@ -1,10 +1,13 @@
|
||||
#ifndef ROM_H
|
||||
#define ROM_H
|
||||
|
||||
enum screen_mirroring {
|
||||
M_HORIZONTAL,
|
||||
M_VERTICAL,
|
||||
M_FOUR,
|
||||
};
|
||||
|
||||
struct Rom {
|
||||
struct rom {
|
||||
char *prg_rom;
|
||||
char *chr_rom;
|
||||
size_t prg_rom_size;
|
||||
@ -13,5 +16,7 @@ struct Rom {
|
||||
enum screen_mirroring mirror;
|
||||
};
|
||||
|
||||
void parse_rom(const uint8_t *data, size_t data_len, struct Rom *rom);
|
||||
void free_rom(struct Rom *rom);
|
||||
void parse_rom(const uint8_t *data, size_t data_len, struct rom *rom);
|
||||
void free_rom(struct rom *rom);
|
||||
|
||||
#endif /* ROM_H */
|
||||
|
Loading…
Reference in New Issue
Block a user