first working CPU implementation with 3 op codes
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
roms/
|
||||
AGENTS/
|
||||
AGENTS.md
|
||||
Cargo.lock
|
||||
|
||||
@@ -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
|
||||
+8
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+213
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+13
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user