From 01b63cab6391dd57632026d45e043c095f37302a Mon Sep 17 00:00:00 2001 From: David Isaksson Date: Sun, 7 Jul 2024 18:49:27 +0200 Subject: [PATCH] emu: Refactor memory to consist of a bus with traits --- src/emulator.rs | 6 ++-- src/emulator/bus.rs | 77 ++++++++++++++++++++++++++++++++++++++++ src/emulator/bus/ram.rs | 39 +++++++++++++++++++++ src/emulator/cpu.rs | 46 ++++++++++++------------ src/emulator/memory.rs | 78 ----------------------------------------- src/emulator/tui/app.rs | 9 ++--- tests/emulator_test.rs | 7 ++-- 7 files changed, 150 insertions(+), 112 deletions(-) create mode 100644 src/emulator/bus.rs create mode 100644 src/emulator/bus/ram.rs delete mode 100644 src/emulator/memory.rs diff --git a/src/emulator.rs b/src/emulator.rs index e2dce8b..bdf82be 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -4,13 +4,13 @@ use clap::Args; use crate::{ assembler::assemble_code, emulator::{ + bus::{Bus, Writeable}, cpu::{Cpu, RunOption}, - memory::{Bus, Memory}, }, }; +pub mod bus; pub mod cpu; -pub mod memory; pub mod tui; #[derive(Args, Debug)] @@ -29,7 +29,7 @@ The program will run until it encounters a break instruction.")] /// Runs the emulator with the given program bytes and program start address. #[tracing::instrument] pub fn run_headless(program_bytes: &[u8], program_start: u16) -> Result<()> { - let mut memory = Memory::new(); + let mut memory = Bus::new(); memory.write_word(cpu::RESET_VECTOR, program_start); // TODO: Include in the program memory.load(0x0000, program_bytes); let mut cpu = Cpu::new(); diff --git a/src/emulator/bus.rs b/src/emulator/bus.rs new file mode 100644 index 0000000..9703d28 --- /dev/null +++ b/src/emulator/bus.rs @@ -0,0 +1,77 @@ +mod ram; + +pub use ram::Ram; + +pub trait Writeable { + fn data_mut(&mut self) -> &mut [u8]; + + fn load(&mut self, start_address: u16, data: &[u8]) { + let mut address = start_address; + for byte in data { + self.write_byte(address, *byte); + address += 1; + } + } + fn write_byte(&mut self, address: u16, data: u8) { + self.data_mut()[address as usize] = data; + } + fn write_word(&mut self, address: u16, data: u16) { + self.data_mut()[address as usize] = (data & 0xff) as u8; + self.data_mut()[(address + 1) as usize] = ((data >> 8) & 0xff) as u8; + } +} + +pub trait Readable { + fn data(&self) -> &[u8]; + fn dump(&self, start_address: u16, end_address: u16) { + for address in start_address..end_address { + eprintln!("{:#06x}: {:#04x}", address, self.read_byte(address)); + } + } + + fn read_byte(&self, address: u16) -> u8 { + self.data()[address as usize] + } + + /// Reads a 16-bit word from memory. + /// The 16-bit word is stored in little-endian format. + fn read_word(&self, address: u16) -> u16 { + self.data()[address as usize] as u16 | ((self.data()[(address + 1) as usize] as u16) << 8) + } +} + +#[derive(Debug)] +pub struct Bus { + ram: Ram, +} + +impl Default for Bus { + fn default() -> Self { + Self::new() + } +} + +impl Bus { + pub const BUS_SIZE: usize = 64 * 1024; + + #[tracing::instrument] + pub fn new() -> Self { + Self { ram: Ram::new() } + } + + pub fn load(&mut self, start_address: u16, data: &[u8]) { + self.ram.load(start_address, data); + } +} + +impl Readable for Bus { + fn data(&self) -> &[u8] { + self.ram.data() + } +} + +impl Writeable for Bus { + fn data_mut(&mut self) -> &mut [u8] { + self.ram.data_mut() + } +} diff --git a/src/emulator/bus/ram.rs b/src/emulator/bus/ram.rs new file mode 100644 index 0000000..6664124 --- /dev/null +++ b/src/emulator/bus/ram.rs @@ -0,0 +1,39 @@ +use super::{Bus, Readable, Writeable}; + +#[derive(Debug)] +pub struct Ram { + data: [u8; Bus::BUS_SIZE], + #[allow(dead_code)] // Not used at the moment, but would be useful for memory-mapped I/O + size: usize, +} + +impl Ram { + pub fn new() -> Self { + Self { + data: [0; Bus::BUS_SIZE], + size: Bus::BUS_SIZE, + } + } + + pub fn _size(&self) -> usize { + self.size + } +} + +impl Default for Ram { + fn default() -> Self { + Self::new() + } +} + +impl Readable for Ram { + fn data(&self) -> &[u8] { + &self.data + } +} + +impl Writeable for Ram { + fn data_mut(&mut self) -> &mut [u8] { + &mut self.data + } +} diff --git a/src/emulator/cpu.rs b/src/emulator/cpu.rs index bcdac54..ffad737 100644 --- a/src/emulator/cpu.rs +++ b/src/emulator/cpu.rs @@ -1,9 +1,9 @@ use self::registers::{Register, Registers, Status}; -use super::memory::Memory; + use crate::{ assembler::codegen::opcode::OPCODE_MAPPING, ast::{AddressingMode, Instruction, Mnemonic, Operand}, - emulator::memory::Bus, + emulator::bus::{Bus, Readable, Writeable}, }; pub mod registers; @@ -64,7 +64,7 @@ impl Cpu { /// Fetches and decodes the next instruction from memory. #[tracing::instrument] - fn fetch_and_decode(&mut self, memory: &mut Memory) -> Instruction { + fn fetch_and_decode(&mut self, memory: &mut Bus) -> Instruction { let opcode = memory.read_byte(self.regs.pc); let (mnemonic, addr_mode) = OPCODE_MAPPING .find_instruction(opcode) @@ -92,7 +92,7 @@ impl Cpu { } #[tracing::instrument] - fn execute_instruction(&mut self, ins: &Instruction, memory: &mut Memory) -> usize { + fn execute_instruction(&mut self, ins: &Instruction, memory: &mut Bus) -> usize { match (&ins.mnemonic, &ins.addr_mode, &ins.operand) { (Mnemonic::ADC, _, Operand::Immediate(value)) => { self.add_with_carry(*value); @@ -982,12 +982,12 @@ impl Cpu { } #[tracing::instrument] - fn indexed_indirect_x(&self, memory: &mut Memory, zp_addr: u8) -> u16 { + fn indexed_indirect_x(&self, memory: &mut Bus, zp_addr: u8) -> u16 { memory.read_word((zp_addr + self.regs.x) as u16) & 0xff } #[tracing::instrument] - fn indexed_indirect_y(&self, memory: &mut Memory, zp_addr: u8) -> (bool, u16) { + fn indexed_indirect_y(&self, memory: &mut Bus, zp_addr: u8) -> (bool, u16) { let indirect_addr = memory.read_word(zp_addr as u16); self.indexed_indirect(Register::Y, indirect_addr) } @@ -1109,7 +1109,7 @@ impl Cpu { } #[tracing::instrument] - fn store_register(&mut self, register: Register, addr: u16, memory: &mut Memory) { + fn store_register(&mut self, register: Register, addr: u16, memory: &mut Bus) { let value = match register { Register::A => self.regs.a, Register::X => self.regs.x, @@ -1119,13 +1119,13 @@ impl Cpu { } #[tracing::instrument] - fn push_to_stack(&mut self, memory: &mut Memory, value: u8) { + fn push_to_stack(&mut self, memory: &mut Bus, value: u8) { memory.write_byte(STACK_PAGE + self.regs.sp as u16, value); self.regs.sp = self.regs.sp.wrapping_sub(1); } #[tracing::instrument] - fn pop_from_stack(&mut self, memory: &mut Memory) -> u8 { + fn pop_from_stack(&mut self, memory: &mut Bus) -> u8 { self.regs.sp = self.regs.sp.wrapping_add(1); memory.read_byte(STACK_PAGE + self.regs.sp as u16) } @@ -1136,7 +1136,7 @@ impl Cpu { } #[tracing::instrument] - fn handle_interrupt(&mut self, memory: &mut Memory, vector: u16) -> usize { + fn handle_interrupt(&mut self, memory: &mut Bus, vector: u16) -> usize { let return_addr = self.regs.pc; self.push_to_stack(memory, (return_addr >> 8) as u8); self.push_to_stack(memory, return_addr as u8); @@ -1148,7 +1148,7 @@ impl Cpu { } #[tracing::instrument] - fn handle_reset(&mut self, memory: &mut Memory) -> usize { + fn handle_reset(&mut self, memory: &mut Bus) -> usize { self.regs.a = 0; self.regs.x = 0; self.regs.y = 0; @@ -1163,7 +1163,7 @@ impl Cpu { } #[tracing::instrument] - fn handle_interrupt_request(&mut self, memory: &mut Memory) { + fn handle_interrupt_request(&mut self, memory: &mut Bus) { if self.reset_interrupt_pending { self.reset_interrupt_pending = false; self.cycles_left_for_instruction = self.handle_reset(memory); @@ -1201,7 +1201,7 @@ impl Cpu { /// Ticks the clock of the CPU. #[tracing::instrument] - pub fn clock(&mut self, memory: &mut Memory) { + pub fn clock(&mut self, memory: &mut Bus) { if self.cycles_left_for_instruction > 0 { // Processing current instruction self.cycles_left_for_instruction -= 1; @@ -1225,7 +1225,7 @@ impl Cpu { /// Steps the CPU by one instruction. #[tracing::instrument] - pub fn step(&mut self, memory: &mut Memory) { + pub fn step(&mut self, memory: &mut Bus) { // Fetch and decode the instruction self.clock(memory); @@ -1237,7 +1237,7 @@ impl Cpu { /// Runs the CPU until the given run condition is met. #[tracing::instrument] - pub fn run(&mut self, memory: &mut Memory, run_option: RunOption) { + pub fn run(&mut self, memory: &mut Bus, run_option: RunOption) { match run_option { RunOption::UntilCycles(cycles_to_run) => { for _ in 0..cycles_to_run { @@ -1277,7 +1277,7 @@ impl Cpu { #[cfg(test)] mod tests { use super::*; - use crate::{assembler::assemble_code, emulator::memory::Memory}; + use crate::{assembler::assemble_code, emulator::bus::Bus}; use pretty_assertions::assert_eq; @@ -1299,9 +1299,9 @@ mod tests { /// Expected number of cpu cycles to run. expected_cycles: usize, /// Optional function to initialize the memory before running the test. - init_memory_fn: Option, + init_memory_fn: Option, /// Optional function to assert the memory after running the test. - expected_memory_fn: Option, + expected_memory_fn: Option, } impl Default for TestCase { @@ -1320,16 +1320,16 @@ mod tests { pub fn run_test(&self) { // Arrange let mut cpu = Cpu::new(); - let mut memory = Memory::new(); + let mut bus = Bus::new(); let bytes = assemble_code(self.code, PROGRAM_START).expect("Failed to compile code"); - memory.load(PROGRAM_START, &bytes); + bus.load(PROGRAM_START, &bytes); if let Some(init_memory_fn) = self.init_memory_fn { - init_memory_fn(&mut memory); + init_memory_fn(&mut bus); } cpu.set_program_counter(PROGRAM_START); // Act - cpu.run(&mut memory, RunOption::UntilCycles(self.expected_cycles)); + cpu.run(&mut bus, RunOption::UntilCycles(self.expected_cycles)); // Assert assert_eq!(cpu.regs.a, self.expected_cpu.regs.a); @@ -1340,7 +1340,7 @@ mod tests { assert_eq!(cpu.regs.status, self.expected_cpu.regs.status); if let Some(expected_memory_fn) = self.expected_memory_fn { - expected_memory_fn(&memory); + expected_memory_fn(&bus); } } } diff --git a/src/emulator/memory.rs b/src/emulator/memory.rs deleted file mode 100644 index f9c1651..0000000 --- a/src/emulator/memory.rs +++ /dev/null @@ -1,78 +0,0 @@ -pub trait Bus { - fn read_byte(&self, address: u16) -> u8; - fn read_word(&self, address: u16) -> u16; - fn write_byte(&mut self, address: u16, data: u8); - fn write_word(&mut self, address: u16, data: u16); -} - -#[derive(Debug)] -pub struct Memory { - data: [u8; Memory::MEMORY_SIZE], -} - -impl Memory { - pub const MEMORY_SIZE: usize = 64 * 1024; - - #[tracing::instrument] - pub fn new() -> Self { - Self { - data: [0; Memory::MEMORY_SIZE], - } - } - - /// Get a slice reference - #[tracing::instrument] - pub fn slice(&self, start_address: usize, end_address: usize) -> &[u8] { - &self.data[start_address..end_address] - } - - #[tracing::instrument] - pub fn dump(&self, start_address: u16, end_address: u16) { - for address in start_address..end_address { - eprintln!("{:#06x}: {:#04x}", address, self.read_byte(address)); - } - } - - #[tracing::instrument] - pub fn load(&mut self, start_address: u16, data: &[u8]) { - let mut address = start_address; - for byte in data { - self.write_byte(address, *byte); - address += 1; - } - } -} - -impl Default for Memory { - #[tracing::instrument] - fn default() -> Self { - Self::new() - } -} - -impl Bus for Memory { - /// Reads a byte from memory. - #[tracing::instrument] - fn read_byte(&self, address: u16) -> u8 { - self.data[address as usize] - } - - /// Reads a 16-bit word from memory. - /// The 16-bit word is stored in little-endian format. - #[tracing::instrument] - fn read_word(&self, address: u16) -> u16 { - self.data[address as usize] as u16 | ((self.data[(address + 1) as usize] as u16) << 8) - } - - /// Writes a byte to memory. - #[tracing::instrument] - fn write_byte(&mut self, address: u16, data: u8) { - self.data[address as usize] = data; - } - - #[tracing::instrument] - fn write_word(&mut self, address: u16, data: u16) { - self.data[address as usize] = (data & 0xff) as u8; - self.data[(address + 1) as usize] = ((data >> 8) & 0xff) as u8; - } -} diff --git a/src/emulator/tui/app.rs b/src/emulator/tui/app.rs index bb9203c..5277fb3 100644 --- a/src/emulator/tui/app.rs +++ b/src/emulator/tui/app.rs @@ -1,8 +1,8 @@ use crate::{ disassembler::{disassemble_code, listing}, emulator::{ + bus::{Bus, Readable, Writeable}, cpu::{self, Cpu, RunOption, STACK_BASE, STACK_PAGE}, - memory::{Bus, Memory}, }, }; @@ -19,7 +19,7 @@ pub struct App { /// Emulated CPU cpu: Cpu, /// Emulated memory - memory: Memory, + memory: Bus, /// The program to run. program: Vec, @@ -68,6 +68,7 @@ impl App { program_start, disassembled_program: disassembly, selected_widget: AppWidget::Disassembly, + memory: Bus::new(), ..Default::default() }; @@ -81,7 +82,7 @@ impl App { /// Resets the application with the provided program. pub fn reset(&mut self) { - self.memory = Memory::new(); + self.memory = Bus::new(); self.memory .write_word(cpu::RESET_VECTOR, self.program_start); // TODO: Include in the program self.memory.load(0x0000, &self.program); @@ -129,7 +130,7 @@ impl App { } pub fn memory_slice(&self, start: usize, end: usize) -> &[u8] { - self.memory.slice(start, end) + self.memory.data()[start..end].as_ref() } pub fn stack_memory(&self) -> &[u8] { diff --git a/tests/emulator_test.rs b/tests/emulator_test.rs index 1105124..75562cd 100644 --- a/tests/emulator_test.rs +++ b/tests/emulator_test.rs @@ -2,9 +2,8 @@ use mos6502::emulator::cpu::RunOption; use mos6502::{ assembler::assemble_code, emulator::{ - cpu, - cpu::Cpu, - memory::{Bus, Memory}, + bus::{Bus, Readable, Writeable}, + cpu::{self, Cpu}, }, }; @@ -16,7 +15,7 @@ fn test_loop_program() { let bytes = assemble_code(source, 0).unwrap(); let mut cpu = Cpu::new(); - let mut memory = Memory::new(); + let mut memory = Bus::new(); memory.load(0x0000, &bytes); memory.write_word(cpu::RESET_VECTOR, program_start); // TODO: Include in the program