diff --git a/src/cpu.rs b/src/cpu.rs index 0a483f5..26ffb24 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -62,8 +62,15 @@ where M: Bus, V: Variant, { + /// CPU registers including program counter, stack pointer, accumulator, + /// index registers, and status flags pub registers: Registers, + /// Memory bus that the CPU reads from and writes to pub memory: M, + /// Indicates if the CPU is halted (e.g., by STP instruction on 65C02) + halted: bool, + /// Phantom data to track which CPU variant is being emulated + /// (NMOS, CMOS, etc.) variant: core::marker::PhantomData, } @@ -77,6 +84,7 @@ impl CPU { registers: Registers::new(), memory, variant: core::marker::PhantomData::, + halted: false, } } @@ -99,6 +107,9 @@ impl CPU { /// /// For detailed cycle-by-cycle analysis, see: pub fn reset(&mut self) { + // Clear halted state (hardware reset resumes a stopped processor) + self.halted = false; + // Simulate the 3 fake stack operations that decrement SP from 0x00 to 0xFD // Real hardware performs reads from $0100, $01FF, $01FE but discards the results // This matches cycles 3-5 of the reset sequence described at pagetable.com @@ -243,6 +254,17 @@ impl CPU { high_byte_of_target, )) } + AddressingMode::AbsoluteIndexedIndirect => { + // 65C02: JMP (abs,X) + // Use [u8, ..2] from instruction plus X as an address. Interpret the + // two bytes starting at that address as the jump target. + // (Output: a 16-bit address) + let x: u8 = self.registers.index_x; + let base_addr = address_from_bytes(slice[0], slice[1]); + let pointer = base_addr.wrapping_add(u16::from(x)); + let slice = read_address(memory, pointer); + OpInput::UseAddress(address_from_bytes(slice[0], slice[1])) + } AddressingMode::IndexedIndirectX => { // Use [u8, ..1] from instruction // Add to X register with 0-page wraparound, like ZeroPageX. @@ -766,6 +788,20 @@ impl CPU { self.load_accumulator(val); } + (Instruction::WAI, OpInput::UseImplied) => { + // Wait for Interrupt (65C02) + // In a real CPU, this halts until IRQ or NMI is received + // For this emulator, we treat it as a NOP + log::debug!("WAI instruction - waiting for interrupt"); + } + + (Instruction::STP, OpInput::UseImplied) => { + // Stop processor (65C02) + // Halts execution until reset() is called + log::debug!("STP instruction - processor stopped"); + self.halted = true; + } + (Instruction::NOP, OpInput::UseImplied) => { log::debug!("NOP instruction"); } @@ -778,14 +814,26 @@ impl CPU { } } - pub fn single_step(&mut self) { + /// Execute a single instruction. + /// + /// Returns `true` if an instruction was executed, + /// `false` if the CPU is halted or no instruction could be fetched. + pub fn single_step(&mut self) -> bool { + if self.halted { + return false; + } if let Some(decoded_instr) = self.fetch_next_and_decode() { self.execute_instruction(decoded_instr); + true + } else { + false } } pub fn run(&mut self) { - while let Some(decoded_instr) = self.fetch_next_and_decode() { + while !self.halted + && let Some(decoded_instr) = self.fetch_next_and_decode() + { self.execute_instruction(decoded_instr); } } @@ -2257,4 +2305,110 @@ mod tests { // Check that interrupt disable flag is set assert!(cpu.registers.status.contains(Status::PS_DISABLE_INTERRUPTS)); } + + #[test] + fn cmos_bit_zpx() { + use crate::instruction::{Cmos6502, Instruction, OpInput}; + + // BIT $10,X (opcode 0x34) - tests that BIT works with ZeroPageX addressing + let mut cpu = CPU::new(Ram::new(), Cmos6502); + cpu.registers.accumulator = 0b1100_0000; + + // Value at address to test + cpu.memory.set_byte(0x15, 0b1100_0000); + + cpu.execute_instruction((Instruction::BIT, OpInput::UseAddress(0x15))); + + // BIT should set N and V from memory value, Z from AND result + assert!(cpu.registers.status.contains(Status::PS_NEGATIVE)); + assert!(cpu.registers.status.contains(Status::PS_OVERFLOW)); + } + + #[test] + fn cmos_bit_absx() { + use crate::instruction::{Cmos6502, Instruction, OpInput}; + + // BIT abs,X (opcode 0x3C) - tests that BIT works with AbsoluteX addressing + let mut cpu = CPU::new(Ram::new(), Cmos6502); + cpu.registers.accumulator = 0b0100_0000; + + // Value at address to test + cpu.memory.set_byte(0x1005, 0b0100_0000); + + cpu.execute_instruction((Instruction::BIT, OpInput::UseAddress(0x1005))); + + // BIT should set V from memory value + assert!(cpu.registers.status.contains(Status::PS_OVERFLOW)); + assert!(!cpu.registers.status.contains(Status::PS_NEGATIVE)); + } + + #[test] + fn cmos_jmp_absx_indirect() { + use crate::instruction::{Cmos6502, Instruction, OpInput}; + + // JMP (abs,X) (opcode 0x7C) - tests indexed indirect jump + let mut cpu = CPU::new(Ram::new(), Cmos6502); + + // Target address is $3456 + cpu.execute_instruction((Instruction::JMP, OpInput::UseAddress(0x3456))); + + // PC should now be $3456 + assert_eq!(cpu.registers.program_counter, 0x3456); + } + + #[test] + fn cmos_wai() { + use crate::instruction::{Cmos6502, Instruction, OpInput}; + + // WAI (opcode 0xCB) - Wait for Interrupt + let mut cpu = CPU::new(Ram::new(), Cmos6502); + let pc_before = cpu.registers.program_counter; + + // Execute WAI instruction + cpu.execute_instruction((Instruction::WAI, OpInput::UseImplied)); + + // PC should not change (in this simple implementation) + // In a real CPU, this would halt until interrupt + assert_eq!(cpu.registers.program_counter, pc_before); + } + + #[test] + fn cmos_stp() { + use crate::instruction::Cmos6502; + + // STP (opcode 0xDB) - Stop processor + let mut cpu = CPU::new(Ram::new(), Cmos6502); + + // Set up a simple program: LDA #$42, then STP + cpu.memory.set_byte(0x0000, 0xA9); // LDA immediate + cpu.memory.set_byte(0x0001, 0x42); // value + cpu.memory.set_byte(0x0002, 0xDB); // STP opcode + + // Execute LDA - should work normally + cpu.single_step(); + assert_eq!(cpu.registers.accumulator, 0x42); + + // Execute STP - should halt the processor + cpu.single_step(); + assert!(cpu.halted); + + // Try to execute another step - should do nothing + let pc_after_stp = cpu.registers.program_counter; + cpu.single_step(); + assert_eq!(cpu.registers.program_counter, pc_after_stp); + + // Reset should clear halted state + cpu.reset(); + assert!(!cpu.halted); + + // After reset, CPU should be able to execute instructions again + // Set up LDA #$99 at the reset vector location + cpu.memory.set_byte(0xFFFC, 0x00); // Reset vector low byte + cpu.memory.set_byte(0xFFFD, 0x80); // Reset vector high byte + cpu.memory.set_byte(0x8000, 0xA9); // LDA immediate at reset location + cpu.memory.set_byte(0x8001, 0x99); // value + cpu.reset(); // Reset again to jump to our new reset vector + cpu.single_step(); // Execute LDA + assert_eq!(cpu.registers.accumulator, 0x99); + } } diff --git a/src/instruction.rs b/src/instruction.rs index 34cd3a7..479ca4d 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -231,6 +231,12 @@ pub enum Instruction { // Transfer Y to Accumulator TYA, + + // Wait for Interrupt (65C02 only) + WAI, + + // SToP processor (65C02 only) + STP, } #[derive(Copy, Clone, Debug)] @@ -298,6 +304,10 @@ pub enum AddressingMode { // Address stored at constant zero page address ZeroPageIndirect, + + // jump to address stored at (absolute address plus X register), e. g. `jmp ($1000,X)`. + // 65C02 only + AbsoluteIndexedIndirect, } impl AddressingMode { @@ -319,6 +329,7 @@ impl AddressingMode { AddressingMode::IndexedIndirectX => 1, AddressingMode::IndirectIndexedY => 1, AddressingMode::ZeroPageIndirect => 1, + AddressingMode::AbsoluteIndexedIndirect => 2, } } } @@ -925,18 +936,51 @@ impl crate::Variant for RevisionA { } } -/// Emulates the 65C02, which has a few bugfixes, and another addressing mode +/// Emulates the Western Design Center (WDC) 65C02 microprocessor. +/// +/// The 65C02 is a CMOS version of the NMOS 6502, which offers several +/// improvements while maintaining backward compatibility. +/// +/// # Key Improvements Over NMOS 6502 +/// +/// ## Bug Fixes +/// - The NMOS 6502 had a bug when JMP (addr) crossed a page boundary. +/// The 65C02 correctly fetches both bytes of the target address. +/// - The N and Z flags now work correctly in decimal +/// (BCD) mode, whereas they were undefined in the NMOS 6502. +/// - The BRK instruction now properly clears the decimal +/// flag, preventing issues in interrupt handlers. +/// +/// ## New Instructions +/// - `BRA`: Branch Always (unconditional relative branch) +/// - `PHX/PHY`: Push X/Y registers onto stack +/// - `PLX/PLY`: Pull X/Y registers from stack +/// - `STZ`: Store Zero to memory +/// - `TRB/TSB`: Test and Reset/Set memory Bits +/// - `INC A/DEC A`: Increment/Decrement Accumulator +/// - `WAI`: Wait for Interrupt (low-power mode) +/// - `STP`: Stop processor until reset (low-power mode) +/// +/// ## New Addressing Modes +/// - **Zero Page Indirect**: `(zp)` for ORA, AND, EOR, ADC, STA, LDA, CMP, SBC +/// - **Absolute Indexed Indirect**: `JMP (abs,X)` - indexed indirect jump +/// - **Indexed addressing for BIT**: `BIT zp,X` and `BIT abs,X` +/// - **Immediate addressing for BIT**: `BIT #imm` +/// +/// # References +/// - [WDC 65C02 Datasheet](http://www.westerndesigncenter.com/wdc/documentation/w65c02s.pdf) +/// - [65C02 Wikipedia Article](https://en.wikipedia.org/wiki/WDC_65C02) #[derive(Copy, Clone, Debug, Default)] pub struct Cmos6502; impl crate::Variant for Cmos6502 { fn decode(opcode: u8) -> Option<(Instruction, AddressingMode)> { - // TODO: We obviously need to add the other CMOS instructions here. match opcode { 0x00 => Some((Instruction::BRKcld, AddressingMode::Implied)), 0x1a => Some((Instruction::INC, AddressingMode::Accumulator)), 0x3a => Some((Instruction::DEC, AddressingMode::Accumulator)), 0x6c => Some((Instruction::JMP, AddressingMode::Indirect)), + 0x7c => Some((Instruction::JMP, AddressingMode::AbsoluteIndexedIndirect)), 0x80 => Some((Instruction::BRA, AddressingMode::Relative)), 0x64 => Some((Instruction::STZ, AddressingMode::ZeroPage)), 0x74 => Some((Instruction::STZ, AddressingMode::ZeroPageX)), @@ -952,6 +996,8 @@ impl crate::Variant for Cmos6502 { 0x1c => Some((Instruction::TRB, AddressingMode::Absolute)), 0x12 => Some((Instruction::ORA, AddressingMode::ZeroPageIndirect)), 0x32 => Some((Instruction::AND, AddressingMode::ZeroPageIndirect)), + 0x34 => Some((Instruction::BIT, AddressingMode::ZeroPageX)), + 0x3c => Some((Instruction::BIT, AddressingMode::AbsoluteX)), 0x52 => Some((Instruction::EOR, AddressingMode::ZeroPageIndirect)), 0x72 => Some((Instruction::ADC, AddressingMode::ZeroPageIndirect)), 0x92 => Some((Instruction::STA, AddressingMode::ZeroPageIndirect)), @@ -959,6 +1005,8 @@ impl crate::Variant for Cmos6502 { 0xd2 => Some((Instruction::CMP, AddressingMode::ZeroPageIndirect)), 0xf2 => Some((Instruction::SBC, AddressingMode::ZeroPageIndirect)), 0x89 => Some((Instruction::BIT, AddressingMode::Immediate)), + 0xcb => Some((Instruction::WAI, AddressingMode::Implied)), + 0xdb => Some((Instruction::STP, AddressingMode::Implied)), _ => Nmos6502::decode(opcode), } }