From faef20555b6bfcaf61bd8cdb2756d33ed70cf276 Mon Sep 17 00:00:00 2001 From: JakeBreath Date: Sat, 8 Aug 2026 13:21:31 -0500 Subject: [PATCH] first working CPU implementation with 3 op codes --- .gitignore | 1 + ours.log | 6 ++ src/bus.rs | 10 ++- src/cpu.rs | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 14 +++- 5 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 ours.log diff --git a/.gitignore b/.gitignore index cb1530c..2133345 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ roms/ AGENTS/ AGENTS.md +Cargo.lock diff --git a/ours.log b/ours.log new file mode 100644 index 0000000..9d1a90e --- /dev/null +++ b/ours.log @@ -0,0 +1,6 @@ +C000 4C F5 C5 A:00 X:00 Y:00 P:24 SP:FD CYC:7 +C5F5 A2 00 A:00 X:00 Y:00 P:24 SP:FD CYC:10 +C5F7 86 00 A:00 X:00 Y:00 P:26 SP:FD CYC:12 +C5F9 86 10 A:00 X:00 Y:00 P:26 SP:FD CYC:15 +C5FB 86 11 A:00 X:00 Y:00 P:26 SP:FD CYC:18 +C5FD 20 A:00 X:00 Y:00 P:26 SP:FD CYC:21 diff --git a/src/bus.rs b/src/bus.rs index 2c02f17..ee5072d 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -10,15 +10,21 @@ pub struct NesBus { cart: Cartridge, } +impl NesBus { + pub fn new(cart: Cartridge) -> Self { + Self { ram: [0; 0x800], cart } + } +} + impl Bus for NesBus { fn read(&mut self, address: u16) -> u8 { match address { 0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize], // Zero-Page my beloved 0x2000..=0x3FFF => 0, //PPU registers stub, temporal 0x4000..=0x401F => 0, //APU controller stub, temporal - 0x4020..=0x5FFF => 0, //Expansion ROM stub + 0x4020..=0x5FFF => 0, //Expansion ROM stub. temporal 0x6000..=0x7FFF => 0, //SRAM stub, no reading saves for you :3 - 0x8000..=0xFFFF => self.cart.read_prg(address), + 0x8000..=0xFFFF => self.cart.read_prg(address), // this is te space PRG Rom gets "loaded" to in real hardware } } diff --git a/src/cpu.rs b/src/cpu.rs index e69de29..f623181 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -0,0 +1,213 @@ +use crate::bus::Bus; + +// Flag bit constants +const FLAG_CARRY: u8 = 0b0000_0001; +const FLAG_ZERO: u8 = 0b0000_0010; +const FLAG_INTERRUPT: u8 = 0b0000_0100; +const FLAG_DECIMAL: u8 = 0b0000_1000; +const FLAG_BREAK: u8 = 0b0001_0000; +const FLAG_UNUSED: u8 = 0b0010_0000; +const FLAG_OVERFLOW: u8 = 0b0100_0000; +const FLAG_NEGATIVE: u8 = 0b1000_0000; + +pub struct Cpu { + a: u8, // Accumulator Register + x: u8, // X Register + y: u8, // Y Register + sp: u8, // Stack Pointer + pc: u16, // Program Counter + p: u8, // Status Register + cycles: u64, + irq_pending: bool, + nmi_pending: bool, +} + +impl Cpu { + // Testing related + pub fn set_test_start(&mut self, pc: u16, cycles: u64) { + self.pc = pc; + self.cycles = cycles; + } + + // Helpers for changing flags on the CPU + fn set_flag(&mut self, bit: u8) { self.p |= bit; } + fn clear_flag(&mut self, bit: u8) { self.p &= !bit; } + fn flag(&self, bit: u8) -> bool { self.p & bit != 0 } + + fn set_zp_flags(&mut self, value: u8) { + // Clear before setting + self.clear_flag(FLAG_ZERO); + self.clear_flag(FLAG_NEGATIVE); + + if value == 0 { self.set_flag(FLAG_ZERO); } // Set Zero if Zero + if value & 0x80 != 0 { self.set_flag(FLAG_NEGATIVE) } // Set if Negative + } + + // New and reset methods + pub fn new() -> Self { + Self { a: 0, x: 0, y: 0, sp: 0xFD, pc: 0, p: FLAG_UNUSED | FLAG_INTERRUPT, cycles: 0, irq_pending: false, nmi_pending: false } // kick stars a NES CPU + } + + pub fn reset(&mut self, bus: &mut impl Bus) { + self.sp = 0xFD; + self.p = FLAG_UNUSED | FLAG_INTERRUPT; + self.pc = self.read16(bus, 0xFFFC); + } + + // Memory helpers + fn fetch(&mut self, bus: &mut impl Bus) -> u8 { + let value: u8 = bus.read(self.pc); + self.pc = self.pc.wrapping_add(1); + value + } + + fn fetch16(&mut self, bus: &mut impl Bus) -> u16 { + let low = self.fetch(bus) as u16; + let high = self.fetch(bus) as u16; + high << 8 | low + } + + fn read(&self, bus: &mut impl Bus, address: u16) -> u8 { + bus.read(address) + } + + fn write(&mut self, bus: &mut impl Bus, address: u16, value: u8) { + bus.write(address, value); + } + + fn read16(&self, bus: &mut impl Bus, address: u16) -> u16 { + let low: u16 = bus.read(address) as u16; + let high: u16 = bus.read(address.wrapping_add(1)) as u16; + high << 8 | low // returt in little-endian format + } + + // Stack related Memory helpers + fn push(&mut self, bus: &mut impl Bus, value: u8) { + self.write(bus, 0x0100 + self.sp as u16, value); + self.sp = self.sp.wrapping_sub(1); + } + + fn pop(&mut self, bus: &mut impl Bus) -> u8 { + self.sp = self.sp.wrapping_add(1); + self.read(bus, 0x0100 + self.sp as u16) + } + + // Step shell + pub fn step(&mut self, bus: &mut impl Bus) -> u8 { + let opcode: u8 = self.fetch(bus); + let cycles = match opcode { + 0x4C => self.jmp_abs(bus), + 0xA2 => self.ldx_imm(bus), + 0x86 => self.stx_zp(bus), + _ => panic!("illegal/unkown opcode {opcode:#04X} at {:#06X}", self.pc.wrapping_sub(1)), + }; + self.cycles += cycles as u64; + cycles + } + + // Trace for testing the cpu + pub fn trace(&self, bus: &mut impl Bus) -> String { + let opcode: u8 = bus.read(self.pc); + let len: u8 = Self::opcode_len(opcode); + let mut bytes: String = String::new(); + for i in 0..len { + bytes.push_str(&format!("{:02X} ", bus.read(self.pc.wrapping_add(i as u16)))); + } + format!( + "{:04X} {:<8} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} CYC:{}", + self.pc, bytes, self.a, self.x, self.y, self.p, self.sp, self.cycles + ) + } + + // Address mode helpers + fn address_zp(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Zero Page + (self.fetch(bus) as u16, 0) + } + + fn address_zpx(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Zero Page, X mode + (self.fetch(bus).wrapping_add(self.x) as u16, 0) + } + + fn address_zpy(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Zero Page, Y mode + (self.fetch(bus).wrapping_add(self.y) as u16, 0) + } + + fn address_abs(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Absolute address + (self.fetch16(bus), 0) + } + + fn address_abx(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Absolute, X address + let base: u16 = self.fetch16(bus); + let address: u16 = base.wrapping_add(self.x as u16); + let extra: u8 = if (base & 0xFF00) != (address & 0xFF00) { 1 } else { 0 }; + (address, extra) + } + + fn address_aby(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Absolute, Y address + let base: u16 = self.fetch16(bus); + let address: u16 = base.wrapping_add(self.y as u16); + let extra: u8 = if (base & 0xFF00) != (address & 0xFF00) { 1 } else { 0 }; + (address, extra) + } + + fn address_izx(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Index-Indirect mode + X + let zp: u8 = self.fetch(bus); // Operand: Address on Zero-Page + let ptr_address: u8 = zp.wrapping_add(self.x); // We add x to it wrapped + let low: u16 = self.read(bus, ptr_address as u16) as u16; + let high: u16 = self.read(bus, ptr_address.wrapping_add(1) as u16) as u16; + (high << 8 | low, 0) // Return in little endian, 16-bit pointer + } + + fn address_izy(&mut self, bus: &mut impl Bus) -> (u16, u8) { // Index-Indirect mode + Y + let zp: u8 = self.fetch(bus); // Operand: Address on Zero-Page + let low: u16 = self.read(bus, zp as u16) as u16; + let high: u16 = self.read(bus, zp.wrapping_add(1) as u16) as u16; + let base: u16 = high << 8 | low; // 16-bit pointer to somewhere on WRAM, little-endian + let address: u16 = base.wrapping_add(self.y as u16); // Add Y to the 16-bit pointer + let extra: u8 = if (base & 0xFF00) != (address & 0xFF00) { 1 } else { 0 }; + (address, extra) + } + + fn address_ind(&mut self, bus: &mut impl Bus) -> (u16, u8) { // used by JMP instruction + hardware bug where the page wraps around + let ptr: u16 = self.fetch16(bus); // pointer itself + let low: u16 = self.read(bus, ptr) as u16; + let high_ptr: u16 = (ptr & 0xFF00) | (ptr.wrapping_add(1) & 0x00FF); // Hardware bug wrap here + let high = self.read(bus, high_ptr) as u16; + (high << 8 | low, 0) // little-endian Return + } + + fn address_rel(&mut self, bus: &mut impl Bus) -> u16 { // Branch relative + let offset: u8 = self.fetch(bus); // signed 8-bit offset from argument + self.pc.wrapping_add((offset as i8) as u16) // sign-extend + add, this allows to move the PC backwards + } + + // NES 6502 opcode -> functions + fn jmp_abs(&mut self, bus: &mut impl Bus) -> u8 { // 0x4C, 3 cycles + self.pc = self.fetch16(bus); + 3 + } + + fn ldx_imm(&mut self, bus: &mut impl Bus) -> u8 { // 0xA2, 2 cycles + self.x = self.fetch(bus); + self.set_zp_flags(self.x); + 2 + } + + fn stx_zp(&mut self, bus: &mut impl Bus) -> u8 { // 0x86, 3 cycles + let (addr, _) = self.address_zp(bus); + self.write(bus, addr, self.x); + 3 + } + + // NES 6502 opcode length table/matcher + fn opcode_len(opcode: u8) -> u8 { + match opcode { + 0x4C => 3, // JMP abs + 0xA2 => 2, // LDX imm + 0x86 => 2, // STX zp + _ => 1, // fallback, would show the invalid/unknown opcode only + } + } + +} + diff --git a/src/main.rs b/src/main.rs index 6b756fd..0b24567 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,18 @@ mod cartridge; mod bus; +mod cpu; + +use crate::bus::{Bus, NesBus}; +use crate::cartridge::Cartridge; +use crate::cpu::Cpu; fn main() { - println!("Hello, world!"); + let cart: Cartridge = Cartridge::from_file("roms/test/cpu/nestest/nestest.nes").unwrap(); + let mut bus: NesBus = NesBus::new(cart); + let mut cpu = Cpu::new(); + cpu.set_test_start(0xC000, 7); + loop { + println!("{}", cpu.trace(&mut bus)); + cpu.step(&mut bus); + } }