Compare commits

...

10 Commits

10 changed files with 449 additions and 21 deletions

View File

@ -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

View File

@ -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
View 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
View 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
View File

@ -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,10 +52,11 @@ 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
peek16(uint16_t addr)
@ -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
View 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
View 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
View File

@ -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
View File

@ -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 */

6
shell.nix Normal file
View File

@ -0,0 +1,6 @@
with import <nixpkgs> {}; mkShellNoCC {
nativeBuildInputs = [
bmake
clang
];
}