diff options
| -rw-r--r-- | Cargo.lock | 16 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | README.md | 9 | ||||
| -rw-r--r-- | crates/runrom/Cargo.toml | 10 | ||||
| -rw-r--r-- | crates/runrom/README.md | 32 | ||||
| -rw-r--r-- | crates/runrom/src/main.rs | 225 | ||||
| -rw-r--r-- | lib/vnf/Cargo.toml | 10 | ||||
| -rw-r--r-- | lib/vnf/src/lib.rs | 1178 | ||||
| -rw-r--r-- | lib/xixanta/src/opcodes.rs | 6 | ||||
| -rwxr-xr-x | scripts/test-e2e.sh | 9 | ||||
| -rw-r--r-- | tests/expected/runrom/arithlog.txt | 58 | ||||
| -rw-r--r-- | tests/runrom/arithlog.s | 68 |
12 files changed, 1622 insertions, 0 deletions
@@ -23,6 +23,22 @@ dependencies = [ ] [[package]] +name = "runrom" +version = "0.1.0" +dependencies = [ + "header", + "vnf", +] + +[[package]] +name = "vnf" +version = "0.1.0" +dependencies = [ + "header", + "xixanta", +] + +[[package]] name = "xa65" version = "0.1.0" @@ -11,6 +11,7 @@ authors = ["Miquel Sabaté Solà <mikisabate@gmail.com>"] [workspace.dependencies] xixanta = { path = "./lib/xixanta", version = "0.1.0" } header = { path = "./lib/header", version = "0.1.0" } +vnf = { path = "./lib/vnf", version = "0.1.0" } [profile.release] codegen-units = 1 @@ -26,6 +26,15 @@ will invoke `nasm` with more pedantic features like its address sanitizer. information about it. Read more about it in [./crates/readrom/README.md](./crates/readrom/README.md). +## `runrom` + +`runrom` is an NES/Famicom emulator that doesn't attempt to run a ROM +graphically. Instead, it just runs code and exposes the data on memory, +registers, etc. for a given run. Thus, `runrom` is a tool to run an NES/Famicom +programatically, so developers can use it to test their ROM files under certain +conditions. Read more about it in +[./crates/runrom/README.md](./crates/runrom/README.md). + ## License This repository holds two licenses, as you can also note on the `Cargo.toml` diff --git a/crates/runrom/Cargo.toml b/crates/runrom/Cargo.toml new file mode 100644 index 0000000..e1c70fc --- /dev/null +++ b/crates/runrom/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "runrom" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +header.workspace = true +vnf.workspace = true
\ No newline at end of file diff --git a/crates/runrom/README.md b/crates/runrom/README.md new file mode 100644 index 0000000..c5e7604 --- /dev/null +++ b/crates/runrom/README.md @@ -0,0 +1,32 @@ +This is yet another NES/Famicom emulator. Only this time around it's +specifically tailored to NES/Famicom developers, not players. + +First of all, the `vnf` library used for this binary exposes the virtual machine +with a proper interface. This way, you can run ROM files programmatically. Then, +`runrom` is just a wrapper on top of this library with a set of options that +toggle certain features from it. This is a nice thing to have if you don't need +to write very specific conditions with a tailored program. Second of all, +`runrom` runs with no graphics nor sound. That is, it runs headless. Thus, it +can be run on your testing infrastructure, so you can run continuous integration +on critical paths from your games. + +## Basic usage + +You can run a ROM file by simply: + +``` +$ runrom <your-game-path>/game.nes +``` + +This will display all of the instructions being run. By default it will run from +the reset vector. You can change that with the `-s/--start` option, which +accepts a 16-bit address from where to start execution. + +That being said, most of the times you want to test a specific function. For +that, you can toggle the `-f/--function` option, which tells `runrom` that the +address is just a function and, whenever a top-level `rts`/`rti` instruction is +found, then execution can be halted. + +Moreover, you may also find interesting the `-d/--dump-memory` option, which +will display a summary of memory addresses which have been updated along +execution, and some statistics about them. diff --git a/crates/runrom/src/main.rs b/crates/runrom/src/main.rs new file mode 100644 index 0000000..ab1ea83 --- /dev/null +++ b/crates/runrom/src/main.rs @@ -0,0 +1,225 @@ +use header::Header; +use std::fs::File; +use std::io::{ErrorKind, Read, Seek, SeekFrom}; +use vnf::{Machine, MemoryInitialValue, MemoryPolicy}; + +/// Version for this program. +const VERSION: &str = "0.1.0"; + +#[derive(Default)] +struct Args { + file: String, + start: Option<u16>, + assume_function: bool, + dump_memory: bool, +} + +fn print_help() { + println!("Run an NES/Famicom ROM to test its code under a set of conditions.\n"); + println!("usage: runrom [OPTIONS] <FILE>\n"); + println!("Options:"); + println!(" -d, --dump-memory\tShow the memory that has changed after a run."); + println!(" -f, --function\tRun the code by assuming it's a function."); + println!(" -h, --help\t\tPrint this message and quit."); + println!(" -s, --start\t\tAddress from where to start (default: reset vector)."); + println!(" -v, --version\t\tPrint version information."); + std::process::exit(0); +} + +// Print the given `message` and exit(1). +fn die(message: String) { + eprintln!("error: {message}"); + std::process::exit(1); +} + +fn parse_hex_digit(c: char) -> Result<u16, String> { + match c.to_digit(16) { + Some(val) => Ok(val as u16), + None => Err("cannot convert digit to hexadecimal".to_string()), + } +} + +fn parse_hex_argument(given: &str) -> Result<u16, String> { + // Skip a leading '$' character. + let arg = if given.starts_with('$') { + given.get(1..).unwrap_or("") + } else { + given + }; + let mut chars = arg.chars(); + + match arg.len() { + 0 => Err("you need to provide an address".to_string()), + 1 => Ok(parse_hex_digit(chars.next().unwrap())?), + 2 => Ok((parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + 3 => Ok((parse_hex_digit(chars.next().unwrap())? << 8) + + (parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + 4 => Ok((parse_hex_digit(chars.next().unwrap())? << 12) + + (parse_hex_digit(chars.next().unwrap())? << 8) + + (parse_hex_digit(chars.next().unwrap())? << 4) + + (parse_hex_digit(chars.next().unwrap())?)), + _ => Err("hex literal is too big".to_string()), + } +} + +fn parse_arguments() -> Args { + let mut args = std::env::args(); + let mut res = Args::default(); + + // Skip command name. + args.next(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "-h" | "--help" => print_help(), + "-s" | "--start" => { + if res.start.is_some() { + die("do not specify the '-s/--start' flag twice".to_string()); + } + let Some(val) = args.next() else { + die("you need to specify a value for the -s/--start flag!".to_string()); + return res; + }; + match parse_hex_argument(&val) { + Ok(n) => res.start = Some(n), + Err(e) => die(e), + } + } + "-d" | "--dump-memory" => { + res.dump_memory = true; + } + "-f" | "--function" => { + res.assume_function = true; + } + "-v" | "--version" => { + println!("runrom {VERSION}"); + std::process::exit(0); + } + _ => { + if arg.starts_with('-') { + die(format!("don't know how to handle the '{arg}' flag")); + } + if !res.file.is_empty() { + die("cannot have multiple source files".to_string()); + } + res.file = arg; + } + } + } + + if res.file.is_empty() { + die("you need to specify the file to be run".to_string()); + } + + res +} + +// Given a ROM file identified by the `file` parameter, fetch the 16-bit address +// as pointed out by the reset vector. +fn start_from_reset_vector(file: &String) -> u16 { + // 1. Read the ROM header so we fetch the size of PRG ROM. + + let Ok(mut input) = File::open(file) else { + die(format!("failed to open the given file '{file}'")); + return 0; + }; + + let mut buf = vec![0u8; 0x10]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => die("malformed ROM file".to_string()), + _ => die(e.to_string()), + } + } + + let header = match Header::try_from(buf.as_slice()) { + Ok(h) => h, + Err(e) => { + die(e.to_string()); + return 0; + } + }; + + // 2. With a known PRG ROM size, fetch the two bytes pertaining to the reset + // vector. + + // The two bytes of the reset address are located as follows: + // 1. Skip the ROM header, guaranteed to be exactly 0x10 bytes long. + // 2. Go to the end of PRG ROM. + // 3. -6: NMI addres; -4: reset addres; -2: IRQ address. + let offset: u64 = (0x10 + (header.prg_rom_size * 16 * 1024) - 4) + .try_into() + .unwrap(); + + if input.seek(SeekFrom::Start(offset)).is_err() { + die("cannot peek into the ROM's reset address".to_string()); + return 0; + }; + let mut buf = [0u8; 0x02]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => die("malformed ROM file".to_string()), + _ => die(e.to_string()), + } + } + + ((buf[1] as u16) << 8) + buf[0] as u16 +} + +fn run(file: &String, start: u16, assume_function: bool, dump_memory: bool) -> Result<(), String> { + let mut machine = Machine::from( + file, + start, + #[allow(clippy::single_range_in_vec_init)] + MemoryPolicy { + initial_value: MemoryInitialValue::Fixed(0), + allowed_reads: vec![(0..0x800)], + allowed_writes: vec![(0..0x800)], + minimum_stack_value: 0, + }, + )?; + machine.verbose = true; + + if assume_function { + machine.run_function()?; + } else { + machine.until_address(0xFFFF)?; + } + + if dump_memory { + let mut title = false; + + for (idx, cell) in machine.ram.iter().enumerate() { + if cell.reads > 0 || cell.writes > 0 { + if !title { + println!("\n== Memory dump ==\n"); + title = true; + } + + println!( + "[${:X}] = ${:02X} [reads={}, writes={}]", + idx, cell.value, cell.reads, cell.writes + ); + } + } + } + + Ok(()) +} + +fn main() { + let args = parse_arguments(); + let start = match args.start { + Some(s) => s, + None => start_from_reset_vector(&args.file), + }; + + match run(&args.file, start, args.assume_function, args.dump_memory) { + Ok(m) => m, + Err(e) => { + die(e); + } + } +} diff --git a/lib/vnf/Cargo.toml b/lib/vnf/Cargo.toml new file mode 100644 index 0000000..66bed81 --- /dev/null +++ b/lib/vnf/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "vnf" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +header.workspace = true +xixanta.workspace = true diff --git a/lib/vnf/src/lib.rs b/lib/vnf/src/lib.rs new file mode 100644 index 0000000..d118157 --- /dev/null +++ b/lib/vnf/src/lib.rs @@ -0,0 +1,1178 @@ +use header::Header; +use std::assert_matches; +use std::collections::HashMap; +use std::fs::File; +use std::io::{ErrorKind, Read}; +use std::ops::Range; +use xixanta::opcodes::AddressingMode; +use xixanta::opcodes::{Instruction, InstructionIdentifier, OPCODES}; + +/// Values on the 'status' register converted to bools for easier use. +#[derive(Debug)] +pub struct StatusRegister { + pub negative: bool, + pub overflow: bool, + pub brk: bool, + pub decimal: bool, + pub interrupt: bool, + pub zero: bool, + pub carry: bool, +} + +impl Default for StatusRegister { + fn default() -> Self { + Self { + negative: false, + overflow: false, + brk: false, + decimal: false, + interrupt: true, + zero: false, + carry: false, + } + } +} + +impl StatusRegister { + /// Returns a string with the initial letter for each status bit that is + /// set. Otherwise, for unset bits, a '-' is given. + fn humanize(&self) -> String { + let mut res = String::from(""); + + if self.negative { + res.push('N'); + } else { + res.push('-'); + } + if self.overflow { + res.push('V'); + } else { + res.push('-'); + } + if self.brk { + res.push('B'); + } else { + res.push('-'); + } + if self.decimal { + res.push('D'); + } else { + res.push('-'); + } + if self.interrupt { + res.push('I'); + } else { + res.push('-'); + } + if self.zero { + res.push('Z'); + } else { + res.push('-'); + } + if self.carry { + res.push('C'); + } else { + res.push('-'); + } + + res.to_string() + } +} + +/// Registers from the APU chip. +#[derive(Debug, Default)] +pub struct APU { + pub dmc: u8, + pub frame_counter: u8, +} + +/// Registers from the PPU chip. +#[derive(Debug, Default)] +pub struct PPU { + pub control: u8, + pub mask: u8, + pub status: u8, + pub scroll: u8, + pub address: u8, + pub data: u8, + pub oam_address: u8, + pub oam_dma: u8, +} + +/// A byte from the memory, which other than the actual value, also contains +/// different stats for it. +#[derive(Clone, Copy, Debug, Default)] +pub struct MemoryCell { + /// The current value. + pub value: u8, + + /// Are writes allowed? + pub write_allowed: bool, + + /// Are reads allowed? + pub read_allowed: bool, + + /// How many writes have happened? + pub writes: usize, + + /// How many reads have happened? + pub reads: usize, +} + +/// The initial value to be set for memory cells on a given execution. +#[derive(Debug)] +pub enum MemoryInitialValue { + Fixed(u8), + Random, +} + +/// Allows users to define a policy for how the memory should be initialized for +/// the given Machine. +#[derive(Debug)] +pub struct MemoryPolicy { + /// The initial value to be given for each cell. + pub initial_value: MemoryInitialValue, + + /// The ranges for which reads are allowed to happen. + pub allowed_reads: Vec<Range<usize>>, + + /// The ranges for which writes are allowed to happen. + pub allowed_writes: Vec<Range<usize>>, + + /// How low can the stack go. Use this to control stack underflows. + pub minimum_stack_value: u8, +} + +/// The state of the Joypad handshake process. +#[derive(Copy, Clone, Debug, Default)] +pub enum JoypadState { + #[default] + Waiting, + Received, + Sending, +} + +/// The state of a Joypad. +#[derive(Copy, Clone, Debug, Default)] +pub struct Joypad { + pub state: JoypadState, + pub value: u8, + pub shift: u8, + pub reads: u8, +} + +impl Joypad { + /// Initialize the Joypad so it's ready to accept reads. + pub fn prepare_for_reads(&mut self) { + // TODO: I still have to prepare a proper interface to interact with + // joypads. + self.value = 0; + self.shift = self.value; + self.reads = 0; + } +} + +/// The core structure for the virtual machine. Use this structure to read and +/// run a ROM file. At each step it will hold the current status of the machine +/// so it can be inspected programatically in full detail. +#[derive(Debug)] +pub struct Machine { + /// Is the machine active at all? + active: bool, + + /// Whether the machine is supposed to be running just a function (while + /// also going into inner calls). Hence, it will stop whenever an 'rts' or + /// 'rti' instruction is found at the top level. + run_function_mode: bool, + + /// The PRG ROM pool of bytes. + pub prg_rom: Vec<u8>, + + /// The advertised size of PRG ROM. That is, regardless of the length the + /// ->prg_rom, what's the supposed size of PRG ROM. + pub prg_rom_size: usize, + + /// What is the instruction that is to be run. + pub current_instruction: Instruction, + + /// The number of cycles that the CPU has consumed. + pub cycles: usize, + + /// The extra cycles to be added to the ones inherent of the current + /// instruction. + extra_cycles: usize, + + /// The extra cycles to be added as part of a page penalty. + pub page_penalty: usize, + + /// Number of instructions that have been run so far. + pub instructions: usize, + + /// The program counter register. + pub pc: usize, + + /// Whether the program counter should be left untouched as the current + /// instruction already messed with it. + skip_pc: bool, + + /// The status register. + pub status_register: StatusRegister, + + /// The RAM for this machine. Each memory cell contains a value, but also + /// read/write policies and statistics. + pub ram: Vec<MemoryCell>, + + /// The 'a' register. + pub a: u8, + + /// The 'x' register. + pub x: u8, + + /// The 'y' register. + pub y: u8, + + /// The 's' register. + pub s: u8, + + /// Status from the APU chip. + pub apu: APU, + + /// Status from the PPU chip. + pub ppu: PPU, + + /// Whether the run should be verbose, sending to standard output statistics + /// for each instruction being run, the stack, etc. + pub verbose: bool, + + /// When 'verbose' is true, whether the APU should also be included into the + /// output. This is enabled/disabled automatically whenever the machine + /// detects a change on the APU. + should_report_apu: bool, + + /// When 'verbose' is true, whether the PPU should also be included into the + /// output. This is enabled/disabled automatically whenever the machine + /// detects a change on the PPU. + should_report_ppu: bool, + + /// The initial value for the stack register. Used to detect stack + /// under/over flows. + initial_stack_value: u8, + + /// The memory policy for this machine. That is, the value to be used as the + /// default for each cell, which regions are allowed for read/writes, etc. + policy: MemoryPolicy, + + /// The status of both Joypads. + joypads: [Joypad; 2], +} + +// Returns a vector of MemoryCell representing the RAM for a Machine, which +// follows the memory policy as defined in 'policy'. +fn init_memory(policy: &MemoryPolicy) -> Vec<MemoryCell> { + let mut vec = Vec::with_capacity(0x800); + + for i in 0..0x800 { + let read_allowed = policy.allowed_reads.iter().any(|range| range.contains(&i)); + let write_allowed = policy.allowed_writes.iter().any(|range| range.contains(&i)); + + vec.push(MemoryCell { + value: match policy.initial_value { + MemoryInitialValue::Fixed(n) => n, + MemoryInitialValue::Random => todo!(), + }, + write_allowed, + read_allowed, + reads: 0, + writes: 0, + }); + } + + vec +} + +/// For a given u16 expression, return a tuple formatted like so: +/// .0: The lower byte as u8. +/// .1: Whether the expression is larger than 8 bits. +macro_rules! u16_to_u8_with_carry { + ($val:expr) => {{ + let low_byte = ($val & 0x00FF) as u8; + (low_byte, ($val & 0xFF00) != 0) + }}; +} + +impl Machine { + /// Initialize a Machine object by reading the ROM file located at + /// 'file'. The machine should be initialized to start from the 'start' + /// address, and the memory should be initialized with the given 'policy'. + pub fn from(file: &String, start: u16, policy: MemoryPolicy) -> Result<Self, String> { + let Ok(mut input) = File::open(file) else { + return Err(format!("failed to open the given file '{}'", file)); + }; + + // Read the header in order to detect the PRG ROM size. + let mut buf = vec![0u8; 0x10]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => return Err("malformed ROM file".to_string()), + _ => return Err(e.to_string()), + } + } + let header = match Header::try_from(buf.as_slice()) { + Ok(h) => h, + Err(e) => return Err(e.to_string()), + }; + + let mut prg_rom = vec![0u8; header.prg_rom_size * 16 * 1024]; + if let Err(e) = input.read_exact(&mut prg_rom) { + match e.kind() { + ErrorKind::UnexpectedEof => { + return Err("could not read advertised PRG ROM space".to_string()); + } + _ => return Err(e.to_string()), + } + } + + // TODO: allow for randomized initialization. + Ok(Self { + active: true, + run_function_mode: false, + prg_rom, + prg_rom_size: header.prg_rom_size, + pc: start as usize, + skip_pc: false, + cycles: 7, // NOTE: as per 6502 initialization process. + extra_cycles: 0, + page_penalty: 0, + instructions: 0, + current_instruction: Instruction { + identifier: InstructionIdentifier::Start, + addressing_mode: AddressingMode::Implied, + cycles: 0, + opcode: 0, + size: 0, + affected_on_page: false, + bytes: [0, 0], + }, + a: 0, + x: 0, + y: 0, + s: 0xFD, // NOTE: as per 6502 initialization process. + initial_stack_value: 0xFD, + ram: init_memory(&policy), + status_register: StatusRegister::default(), + apu: APU::default(), + ppu: PPU::default(), + verbose: false, + should_report_apu: false, + should_report_ppu: false, + policy, + joypads: [Joypad::default(); 2], + }) + } + + // Report to the standard output the current status of the machine. + fn report(&mut self) { + let space = if matches!( + self.current_instruction.identifier, + InstructionIdentifier::Unknown + ) { + "\t" + } else if matches!( + self.current_instruction.addressing_mode, + AddressingMode::Implied | AddressingMode::RelativeOrZeropage + ) { + "\t\t" + } else { + "\t" + }; + + let empty = HashMap::new(); + println!( + "{}{}PC: ${:04X}, cycles: {}, registers: [a: ${:02X}, x: ${:02X}, y: ${:02X}, sp: ${:02X}], status: {}", + self.current_instruction + .to_human(self.pc, None, &empty, &empty), + space, + self.pc, + self.cycles, + self.a, + self.x, + self.y, + self.s, + self.status_register.humanize(), + ); + + if self.should_report_apu { + println!( + "\t\t[APU] DMC ${:X}, Frame counter ${:X}\n", + self.apu.dmc, self.apu.frame_counter + ); + self.should_report_apu = false; + } else if self.should_report_ppu { + println!( + "\t\t[PPU] Control: ${:02X}, Mask: ${:02X}, Status: ${:02X}, Scroll: ${:02X}, Address: ${:02X}, Data: ${:02X}, OAM addr: ${:02X}, OAM DMA: ${:02X}\n", + self.ppu.control, + self.ppu.mask, + self.ppu.status, + self.ppu.scroll, + self.ppu.address, + self.ppu.data, + self.ppu.oam_address, + self.ppu.oam_dma, + ); + self.should_report_ppu = false; + } + + if !self.active { + println!("<end>"); + } + } + + // Read the joypad identified by 'id' (0 or 1). + fn joypad_read(&mut self, id: usize) -> Result<u8, String> { + assert_matches!(id, 0 | 1); + let jp = self.joypads.get_mut(id).unwrap(); + + match jp.state { + JoypadState::Waiting | JoypadState::Received => { + Err("joypad is not ready to send data!".to_string()) + } + JoypadState::Sending => { + jp.reads += 1; + if jp.reads > 7 { + Err("too many reads for the joypad state".to_string()) + } else { + let val = jp.shift & 0x01; // TODO: actually more bits are to be sent + jp.shift >>= 1; + Ok(val) + } + } + } + } + + // Write to the joypad identified by 'id' (0 or 1) with the given 'value'. + fn joypad_write(&mut self, id: usize, value: u8) -> Result<(), String> { + assert_matches!(id, 0 | 1); + let jp = self.joypads.get_mut(id).unwrap(); + + match jp.state { + JoypadState::Waiting => { + if value != 1 { + // NOTE: if we are writing on joypad 2, then there might + // be a conflict with the APU frame counter. If that's + // the case, then ignore this "error" and just return + // early. In any other case, a value != 1 is an error. + if id == 0 { + return Err(format!("expecting exacly a '1', '{}' received", value)); + } + return Ok(()); + } + jp.state = JoypadState::Received; + Ok(()) + } + JoypadState::Received => { + if value != 0 { + return Err(format!("expecting exacly a '0', '{}' received", value)); + } + jp.prepare_for_reads(); + jp.state = JoypadState::Sending; + Ok(()) + } + JoypadState::Sending => { + Err("writing into a controller while it's sending data".to_string()) + } + } + } + + // Tick the PPU after an instruction has been run. + fn next_ppu(&mut self) -> Result<(), String> { + self.ppu.status = 0x80; + + Ok(()) + } + + /// Step the execution of the machine by one instruction. + pub fn next_iteration(&mut self) -> Result<(), String> { + // Perform a new iteration of the PPU and the CPU. + self.next_ppu()?; + self.execute()?; + + // Move the PC automatically unless the current instruction explicitely + // did so already. + if self.skip_pc { + self.skip_pc = false; + } else { + self.pc += self.current_instruction.size as usize; + } + + // Sum up cycles and instructions. + self.instructions += 1; + self.cycles += self.current_instruction.cycles as usize; + if self.extra_cycles > 0 { + self.cycles += self.extra_cycles; + self.extra_cycles = 0; + } + + // At this point we can already send a report of the current status of + // the machine. + if self.verbose { + self.report(); + } + + // After moving the PC, is it out of bounds? + if self.pc < 0x8000 { + return Err("out of bounds: program counter is pointing below ROM space".to_string()); + } + + // Fetch the next instruction. + let address = self.pc - 0x8000; + let opcode = self.prg_rom.get(address).unwrap(); + self.current_instruction = match OPCODES.get(opcode) { + Some(instr) => instr.clone(), + None => { + return Err(format!( + "could not find instruction with opcode <{:02X}>", + opcode + )); + } + }; + + // Fetch the bytes for the current instruction. + match self.current_instruction.size { + 2 => { + self.current_instruction.bytes[0] = *self.prg_rom.get(address + 1).unwrap(); + self.current_instruction.bytes[1] = 0; + } + 3 => { + self.current_instruction.bytes[0] = *self.prg_rom.get(address + 1).unwrap(); + self.current_instruction.bytes[1] = *self.prg_rom.get(address + 2).unwrap(); + } + _ => { + self.current_instruction.bytes = [0, 0]; + } + }; + + Ok(()) + } + + /// Run a top-level function. That is, assume that the current 'start' + /// address is the start of a function, and keep on iterating the machine + /// until an 'rts'/'rti' instruction is found at the top-level (we still + /// allow inner calls). + pub fn run_function(&mut self) -> Result<(), String> { + self.run_function_mode = true; + + while self.active { + self.next_iteration()?; + } + + Ok(()) + } + + /// Run until the program counter reaches the given 'address'. + pub fn until_address(&mut self, address: u16) -> Result<(), String> { + while self.pc != address as usize { + self.next_iteration()?; + } + + Ok(()) + } + + // Perform a read of the given memory 'address'. + fn read_memory(&mut self, address: u16) -> Result<u8, String> { + let cell = self.ram.get_mut(address as usize).unwrap(); + + if !cell.read_allowed { + return Err(format!( + "reading was not allowed on address '${:04X}'", + address + )); + } + cell.reads += 1; + + Ok(cell.value) + } + + // Perform a write to the given memory 'address' with the given 'value'. + fn write_memory(&mut self, address: u16, value: u8) -> Result<(), String> { + let cell = self.ram.get_mut(address as usize).unwrap(); + + if !cell.write_allowed { + return Err(format!( + "writing was not allowed on address '${:04X}'", + address + )); + } + cell.writes += 1; + cell.value = value; + + Ok(()) + } + + // Print the current status of the stack. + fn put_stack(&mut self) { + print!("\t\t[STACK]: "); + if self.s == 0xFF { + println!("<empty>"); + return; + } + + for i in self.s + 1..=0xFF { + let addr = 0x200 + i as usize; + print!("{:02X} ", self.ram[addr].value); + } + println!(); + } + + // Push the given 'value' to the stack. + fn push_stack(&mut self, value: u8) -> Result<(), String> { + // Write the given value onto the stack. + let address = 0x200 + self.s as u16; + self.write_memory(address, value)?; + + // And update the stack pointer if possible. + self.s -= 1; + if self.s == self.policy.minimum_stack_value { + return Err("stack underflow!".to_string()); + } + + if self.verbose { + self.put_stack(); + } + + Ok(()) + } + + // Pop the stack once and return the value that was found. + fn pop_stack(&mut self) -> Result<u8, String> { + if self.s == self.initial_stack_value { + return Err("stack overflow!".to_string()); + } + + self.s += 1; + + if self.verbose { + self.put_stack(); + } + + let address = 0x200 + self.s as u16; + self.read_memory(address) + } + + // Returns true of the stack is empty, false otherwise. Note that this + // just means that the value of the 's' register is the one set as its + // initial value. + fn is_stack_empty(&mut self) -> bool { + self.s == self.initial_stack_value + } + + // Compare the given 'value' with the one from the current instruction. Then + // set the proper bits from the status register. + fn compare(&mut self, value: i16) -> Result<(), String> { + let res = value - self.current_instruction.value() as i16; + + self.status_register.zero = res == 0; + self.status_register.negative = (res as u8 & 0x80) == 0x80; + self.status_register.carry = (res as u16 & 0xFF00) != 0; + + Ok(()) + } + + /// Execute the current instruction. + pub fn execute(&mut self) -> Result<(), String> { + self.status_register.overflow = false; + + match self.current_instruction.identifier { + // TODO + InstructionIdentifier::Brk => todo!(), + InstructionIdentifier::Bvc => todo!(), + InstructionIdentifier::Bvs => todo!(), + InstructionIdentifier::Pha => todo!(), + InstructionIdentifier::Pla => todo!(), + InstructionIdentifier::Php => todo!(), + InstructionIdentifier::Plp => todo!(), + InstructionIdentifier::Rti => todo!(), + + // Flag instructions. + InstructionIdentifier::Sec => self.status_register.carry = true, + InstructionIdentifier::Clc => self.status_register.carry = false, + InstructionIdentifier::Sei => self.status_register.interrupt = true, + InstructionIdentifier::Cli => self.status_register.interrupt = false, + InstructionIdentifier::Sed => self.status_register.decimal = true, + InstructionIdentifier::Cld => self.status_register.decimal = false, + InstructionIdentifier::Clv => self.status_register.overflow = false, + + // Arithmetic and logic. + InstructionIdentifier::Adc => { + let mut val = (self.load()? as u16) + self.a as u16; + if self.status_register.carry { + val += 1; + } + (self.a, self.status_register.carry) = u16_to_u8_with_carry!(val); + + self.status_register.zero = self.a == 0; + self.status_register.negative = (self.a & 0x80) == 0x80; + } + InstructionIdentifier::Sbc => { + let mut val = self.a as i16 - self.load()? as i16; + if !self.status_register.carry { + val -= 1; + } + (self.a, self.status_register.carry) = u16_to_u8_with_carry!(val as u16); + + // The carry flag is set as an inverted borrow. Hence, whatever + // we got from the operation as a "regular 'adc'", then we + // invert it. + self.status_register.carry = !self.status_register.carry; + + self.status_register.zero = self.a == 0; + self.status_register.negative = (self.a & 0x80) == 0x80; + } + InstructionIdentifier::And => { + let val = self.load()?; + self.a &= val; + self.status_register.zero = self.a == 0; + self.status_register.negative = (self.a & 0x80) == 0x80; + } + InstructionIdentifier::Ora => { + let val = self.load()?; + self.a |= val; + self.status_register.zero = self.a == 0; + self.status_register.negative = (self.a & 0x80) == 0x80; + } + InstructionIdentifier::Eor => { + let val = self.load()?; + self.a ^= val; + self.status_register.zero = self.a == 0; + self.status_register.negative = (self.a & 0x80) == 0x80; + } + InstructionIdentifier::Inc => { + let val = ((self.load()? as u16 + 1) & 0x00FF) as u8; + + self.store(val)?; + self.status_register.zero = val == 0; + self.status_register.negative = (val & 0x80) == 0x80; + } + InstructionIdentifier::Inx => { + let val = ((self.x as u16 + 1) & 0x00FF) as u8; + + self.x = val; + self.status_register.zero = val == 0; + self.status_register.negative = (val & 0x80) == 0x80; + } + InstructionIdentifier::Iny => { + let val = ((self.y as u16 + 1) & 0x00FF) as u8; + + self.y = val; + self.status_register.zero = val == 0; + self.status_register.negative = (val & 0x80) == 0x80; + } + InstructionIdentifier::Dec => { + let mut val = self.load()?; + if val == 0x00 { + self.store(0xFF)?; + + self.status_register.zero = false; + self.status_register.negative = false; + } else { + val -= 1; + self.store(val)?; + self.status_register.zero = val == 0; + self.status_register.negative = (val & 0x80) == 0x80; + } + } + InstructionIdentifier::Dex => { + if self.x == 0x00 { + self.x = 0xFF; + + self.status_register.zero = false; + self.status_register.negative = false; + } else { + self.x -= 1; + self.status_register.zero = self.x == 0; + self.status_register.negative = (self.x & 0x80) == 0x80; + } + } + InstructionIdentifier::Dey => { + if self.y == 0x00 { + self.y = 0xFF; + + self.status_register.zero = false; + self.status_register.negative = false; + } else { + self.y -= 1; + self.status_register.zero = self.y == 0; + self.status_register.negative = (self.y & 0x80) == 0x80; + } + } + InstructionIdentifier::Asl => { + match self.current_instruction.addressing_mode { + AddressingMode::Implied => { + let val = (self.a as u16) << 1; + (self.a, self.status_register.carry) = u16_to_u8_with_carry!(val); + self.status_register.zero = self.a == 0; + self.status_register.negative = (val & 0x0080) == 0x0080; + } + _ => { + let val = (self.load()? as u16) << 1; + self.status_register.carry = (val & 0xFF00) != 0; + self.status_register.zero = val == 0; + self.store((val & 0x00FF) as u8)?; + self.status_register.negative = (val & 0x0080) == 0x0080; + } + }; + } + InstructionIdentifier::Lsr => { + match self.current_instruction.addressing_mode { + AddressingMode::Implied => { + self.status_register.carry = (self.a & 0x1) == 0x1; + self.a >>= 1; + self.status_register.zero = self.a == 0; + } + _ => { + let mut val = self.load()? as u16; + self.status_register.carry = (val & 0x1) == 0x1; + val >>= 1; + self.status_register.zero = self.a == 0; + self.store(val as u8)?; + } + }; + self.status_register.negative = false; + } + InstructionIdentifier::Ror => { + match self.current_instruction.addressing_mode { + AddressingMode::Implied => { + let carry = self.status_register.carry; + self.status_register.carry = (self.a & 0x1) == 0x1; + self.a >>= 1; + if carry { + self.a |= 0x80; + } + self.status_register.zero = self.a == 0; + } + _ => { + let mut val = self.load()? as usize; + let carry = self.status_register.carry; + self.status_register.carry = (val & 0x1) == 0x1; + val >>= 1; + if carry { + val |= 0x80; + } + self.status_register.zero = self.a == 0; + self.store(val as u8)?; + } + }; + self.status_register.negative = false; + } + InstructionIdentifier::Rol => { + match self.current_instruction.addressing_mode { + AddressingMode::Implied => { + let carry = self.status_register.carry; + self.status_register.carry = (self.a & 0x80) == 0x80; + self.a <<= 1; + if carry { + self.a |= 0x01; + } + self.status_register.zero = self.a == 0; + } + _ => { + let mut val = self.load()? as usize; + let carry = self.status_register.carry; + self.status_register.carry = (val & 0x80) == 0x80; + val <<= 1; + if carry { + val |= 0x01; + } + self.status_register.zero = self.a == 0; + self.store(val as u8)?; + } + }; + self.status_register.negative = false; + } + + // Compare + InstructionIdentifier::Cmp => self.compare(self.a as i16)?, + InstructionIdentifier::Cpx => self.compare(self.x as i16)?, + InstructionIdentifier::Cpy => self.compare(self.y as i16)?, + + // Load and Store + InstructionIdentifier::Lda => self.a = self.load()?, + InstructionIdentifier::Ldx => self.x = self.load()?, + InstructionIdentifier::Ldy => self.y = self.load()?, + InstructionIdentifier::Sta => self.store(self.a)?, + InstructionIdentifier::Stx => self.store(self.x)?, + InstructionIdentifier::Sty => self.store(self.y)?, + + // Jump and branching. + InstructionIdentifier::Jsr => { + let address = self.current_instruction.value(); + if !(0x8000..=0xFFFF).contains(&address) { + return Err("invalid jump!".to_string()); + } + + let next_address = self.pc + self.current_instruction.size as usize; + let low = (next_address as u16 & 0x00FF) as u8; + let high = ((next_address as u16 & 0xFF00) >> 8) as u8; + + self.push_stack(high)?; + self.push_stack(low)?; + + self.pc = address; + self.skip_pc = true; + } + InstructionIdentifier::Jmp => { + let address = self.current_instruction.value(); + if !(0x8000..=0xFFFF).contains(&address) { + return Err("invalid jump!".to_string()); + } + + self.pc = address; + self.skip_pc = true; + } + InstructionIdentifier::Bcs => { + if self.status_register.carry { + self.branch(); + } + } + InstructionIdentifier::Bcc => { + if !self.status_register.carry { + self.branch(); + } + } + InstructionIdentifier::Beq => { + if self.status_register.zero { + self.branch(); + } + } + InstructionIdentifier::Bne => { + if !self.status_register.zero { + self.branch(); + } + } + InstructionIdentifier::Bpl => { + if !self.status_register.negative { + self.branch(); + } + } + InstructionIdentifier::Bmi => { + if self.status_register.negative { + self.branch(); + } + } + InstructionIdentifier::Rts => { + // If the stack is empty but we were just running a function, + // then assume that the machine is done. + if self.is_stack_empty() && self.run_function_mode { + if self.active { + self.active = false; + } + return Ok(()); + } + + // Pull the previous address from the stack and jump there. Note + // that we have to subtract the current instruction's size + // because it will be re-added after the call to `execute`. + let low = self.pop_stack()? as u16; + let high = (self.pop_stack()? as u16) << 8; + self.pc = (high + low) as usize; + self.skip_pc = true; + } + + // transfer + InstructionIdentifier::Tax => self.x = self.a, + InstructionIdentifier::Tay => self.y = self.a, + InstructionIdentifier::Tsx => self.x = self.s, + InstructionIdentifier::Txa => self.a = self.x, + InstructionIdentifier::Txs => { + self.s = self.x; + self.initial_stack_value = self.x; + } + InstructionIdentifier::Tya => self.a = self.y, + + // other + InstructionIdentifier::Bit => { + let val = self.load()?; + self.status_register.zero = (val & self.a) == 0; + self.status_register.negative = (val & 0x80) == 0x80; + self.status_register.overflow = (val & 0x40) == 0x40; + } + + InstructionIdentifier::Start | InstructionIdentifier::Nop => {} + InstructionIdentifier::Unknown => { + return Err("found an unknown instruction!".to_string()); + } + } + + Ok(()) + } + + // Perform a branch instruction. + fn branch(&mut self) { + let val = self.current_instruction.value() as i8; + let next = if val > 0 { + self.pc + val as usize + } else { + self.pc - val.wrapping_neg() as usize + }; + + if (next & 0xFF00) == (self.pc & 0xFF00) { + self.extra_cycles += 2; + self.page_penalty += 1; + } else { + self.extra_cycles += 1; + } + self.pc = next; + // TODO + // self.skip_pc = true; + } + + // Perform a load instruction and return the read value. + fn load(&mut self) -> Result<u8, String> { + let val = self.current_instruction.value(); + let byte = if matches!( + self.current_instruction.addressing_mode, + AddressingMode::Immediate + ) { + val as u8 + } else { + let address = self.target_address()?; + + match address { + 0x2000 => self.ppu.control, + 0x2001 => self.ppu.mask, + 0x2002 => { + // TODO: also clear the address latch + let val = self.ppu.status; + self.ppu.status = 0x00; + val + } + 0x2003 => self.ppu.oam_address, // TODO: maybe read fault + 0x2005 => self.ppu.scroll, + 0x2006 => self.ppu.address, + 0x2007 => self.ppu.data, + 0x4010 => self.apu.dmc, + 0x4014 => self.ppu.oam_dma, // TODO: maybe read fault + 0x00..0x2000 => { + // NOTE: 0x0800 until 0x2000 are simply mirrors of the first + // 2KB. Let's mask out the upper bits. + let real = address & 0x07FF; + self.read_memory(real as u16)? + } + 0x8000..=0xFFFF => { + let real = address - 0x8000; + *self.prg_rom.get(real).unwrap() + } + 0x4016 => self.joypad_read(0)?, + // NOTE: the joypad 2 and the APU frame counter share the same + // address, but read is only reserved for joypad 2. + 0x4017 => self.joypad_read(1)?, + _ => todo!(), + } + }; + + // Set proper flags from the final value. + self.status_register.zero = byte == 0; + self.status_register.negative = (byte & 0x80) == 0x80; + + Ok(byte) + } + + // Returns the effective address which the current instruction is + // targetting. + fn target_address(&mut self) -> Result<usize, String> { + match self.current_instruction.addressing_mode { + AddressingMode::Absolute | AddressingMode::RelativeOrZeropage => { + Ok(self.current_instruction.value()) + } + AddressingMode::ZeropageIndexedX | AddressingMode::IndexedX => { + Ok(self.current_instruction.value() + self.x as usize) + } + AddressingMode::ZeropageIndexedY | AddressingMode::IndexedY => { + Ok(self.current_instruction.value() + self.y as usize) + } + AddressingMode::IndirectY => { + let ptr = self.current_instruction.value() as u16; + let value = + self.read_memory(ptr)? as u16 + ((self.read_memory(ptr + 1)? as u16) << 8); + Ok(value as usize + self.y as usize) + } + _ => { + self.report(); + todo!(); + // Err("bad addressing mode".to_string()) + } + } + } + + // Perform a store instruction with the given 'value'. + fn store(&mut self, value: u8) -> Result<(), String> { + let address = self.target_address()?; + + match address { + 0x2000 => { + self.ppu.control = value; + self.should_report_ppu = self.verbose; + } + 0x2001 => { + self.ppu.mask = value; + self.should_report_ppu = self.verbose; + } + 0x2002 => { + self.ppu.status = value; + self.should_report_ppu = self.verbose; + } + 0x2003 => { + self.ppu.oam_address = value; + self.should_report_ppu = self.verbose; + } + 0x2005 => { + self.ppu.scroll = value; + self.should_report_ppu = self.verbose; + } + 0x2006 => { + self.ppu.address = value; + self.should_report_ppu = self.verbose; + } + 0x2007 => { + self.ppu.data = value; + self.should_report_ppu = self.verbose; + } + 0x4010 => { + self.apu.dmc = value; + self.should_report_apu = self.verbose; + } + 0x4014 => { + self.ppu.oam_dma = value; + self.should_report_ppu = self.verbose; + } + 0x4016 => self.joypad_write(0, value)?, + 0x4017 => { + // NOTE: a write on $4017 affects both the APU frame counter and + // the joypad 2 read sequence. + + self.apu.frame_counter = value; + self.should_report_apu = self.verbose; + + self.joypad_write(1, value)?; + } + 0x00..0x2000 => { + // NOTE: 0x0800 until 0x2000 are simply mirrors of the first + // 2KB. Let's mask out the upper bits. + let real = address & 0x07FF; + self.write_memory(real as u16, value)?; + } + _ => { + self.report(); + todo!() + } + }; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + // use super::*; + + // TODO +} diff --git a/lib/xixanta/src/opcodes.rs b/lib/xixanta/src/opcodes.rs index d5bb97a..a296aa5 100644 --- a/lib/xixanta/src/opcodes.rs +++ b/lib/xixanta/src/opcodes.rs @@ -96,6 +96,10 @@ pub enum InstructionIdentifier { Txa, Txs, Tya, + + // Pseudo-instructions used by 'vnf'. + Start, + Unknown, } /// An entry to the 'INSTRUCTIONS' map, which holds some values relevant for @@ -215,6 +219,8 @@ impl Instruction { InstructionIdentifier::Txa => "txa", InstructionIdentifier::Txs => "txs", InstructionIdentifier::Tya => "tya", + InstructionIdentifier::Start => "<start>", + InstructionIdentifier::Unknown => "<unknown>", }; // On most addressing modes, if the current value is actually found on diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh index 7546666..0620f25 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test-e2e.sh @@ -267,6 +267,15 @@ rm tests/out/jetpac.NTSC.nes rm -f tests/jetpac.nes/.nasm/addresses.txt ## +# runrom + +echo "test: runrom => arithlog.nes" +./target/debug/nasm -Werror -o tests/out/arithlog.nes tests/runrom/arithlog.s +./target/debug/runrom --function --dump-memory tests/out/arithlog.nes > tests/out/arithlog.txt +diff tests/out/arithlog.txt tests/expected/runrom/arithlog.txt +exit_code=$((exit_code + $?)) + +## # Done! exit $exit_code diff --git a/tests/expected/runrom/arithlog.txt b/tests/expected/runrom/arithlog.txt new file mode 100644 index 0000000..aa31569 --- /dev/null +++ b/tests/expected/runrom/arithlog.txt @@ -0,0 +1,58 @@ +<start> PC: $8000, cycles: 7, registers: [a: $00, x: $00, y: $00, sp: $FD], status: ----I-- +lda #$01 PC: $8002, cycles: 9, registers: [a: $01, x: $00, y: $00, sp: $FD], status: ----I-- +sta $00 PC: $8004, cycles: 12, registers: [a: $01, x: $00, y: $00, sp: $FD], status: ----I-- +sec PC: $8005, cycles: 14, registers: [a: $01, x: $00, y: $00, sp: $FD], status: ----I-C +adc $00 PC: $8007, cycles: 17, registers: [a: $03, x: $00, y: $00, sp: $FD], status: ----I-- +ldx #$FF PC: $8009, cycles: 19, registers: [a: $03, x: $FF, y: $00, sp: $FD], status: N---I-- +inx PC: $800A, cycles: 21, registers: [a: $03, x: $00, y: $00, sp: $FD], status: ----IZ- +inx PC: $800B, cycles: 23, registers: [a: $03, x: $01, y: $00, sp: $FD], status: ----I-- +sta $00, x PC: $800D, cycles: 27, registers: [a: $03, x: $01, y: $00, sp: $FD], status: ----I-- +clc PC: $800E, cycles: 29, registers: [a: $03, x: $01, y: $00, sp: $FD], status: ----I-- +adc $00 PC: $8010, cycles: 32, registers: [a: $04, x: $01, y: $00, sp: $FD], status: ----I-- +sta $02 PC: $8012, cycles: 35, registers: [a: $04, x: $01, y: $00, sp: $FD], status: ----I-- +txa PC: $8013, cycles: 37, registers: [a: $01, x: $01, y: $00, sp: $FD], status: ----I-- +sec PC: $8014, cycles: 39, registers: [a: $01, x: $01, y: $00, sp: $FD], status: ----I-C +sbc $00 PC: $8016, cycles: 42, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC +sta $03 PC: $8018, cycles: 45, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC +sec PC: $8019, cycles: 47, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC +rol $03 PC: $801B, cycles: 52, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ- +sta $04 PC: $801D, cycles: 55, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ- +sec PC: $801E, cycles: 57, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZC +ror $02 PC: $8020, cycles: 62, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ- +sta $05 PC: $8022, cycles: 65, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ- +asl PC: $8023, cycles: 67, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ- +sta $06 PC: $8025, cycles: 70, registers: [a: $00, x: $01, y: $00, sp: $FD], status: ----IZ- +ldy #$01 PC: $8027, cycles: 72, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----I-- +tya PC: $8028, cycles: 74, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I-- +and $00, x PC: $802A, cycles: 78, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I-- +sta $07 PC: $802C, cycles: 81, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I-- +ora $00 PC: $802E, cycles: 84, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I-- +sta $08 PC: $8030, cycles: 87, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I-- +inc $00 PC: $8032, cycles: 92, registers: [a: $01, x: $01, y: $01, sp: $FD], status: ----I-- +eor $00 PC: $8034, cycles: 95, registers: [a: $03, x: $01, y: $01, sp: $FD], status: ----I-- +sta $09 PC: $8036, cycles: 98, registers: [a: $03, x: $01, y: $01, sp: $FD], status: ----I-- +lda #$FF PC: $8038, cycles: 100, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I-- +clc PC: $8039, cycles: 102, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I-- +adc #$01 PC: $803B, cycles: 104, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC +sta $0A PC: $803D, cycles: 107, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC +lda #$00 PC: $803F, cycles: 109, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC +sec PC: $8040, cycles: 111, registers: [a: $00, x: $01, y: $01, sp: $FD], status: ----IZC +sbc #$01 PC: $8042, cycles: 113, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I-- +sta $0B PC: $8044, cycles: 116, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I-- +rts PC: $8045, cycles: 122, registers: [a: $FF, x: $01, y: $01, sp: $FD], status: N---I-- +<end> + +== Memory dump == + +[$0] = $02 [reads=6, writes=2] +[$1] = $03 [reads=1, writes=1] +[$2] = $82 [reads=1, writes=2] +[$3] = $01 [reads=1, writes=2] +[$4] = $00 [reads=0, writes=1] +[$5] = $00 [reads=0, writes=1] +[$6] = $00 [reads=0, writes=1] +[$7] = $01 [reads=0, writes=1] +[$8] = $01 [reads=0, writes=1] +[$9] = $03 [reads=0, writes=1] +[$A] = $00 [reads=0, writes=1] +[$B] = $FF [reads=0, writes=1] diff --git a/tests/runrom/arithlog.s b/tests/runrom/arithlog.s new file mode 100644 index 0000000..99648cb --- /dev/null +++ b/tests/runrom/arithlog.s @@ -0,0 +1,68 @@ +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + .byte $02, $01 + .byte $00 + .byte $00 + +.segment "CHARS" +.byte 0 + +.segment "VECTORS" + .addr reset, reset, reset + +.segment "CODE" + +reset: + lda #1 + sta $00 + + sec + adc $00 + ldx #$FF + inx + inx + sta $00, x + + clc + adc $00 + sta $02 + + txa + sec + sbc $00 + sta $03 + + sec + rol $03 + sta $04 + + sec + ror $02 + sta $05 + + asl + sta $06 + + ldy #1 + tya + and $00, x + sta $07 + + ora $00 + sta $08 + + inc $00 + eor $00 + sta $09 + + lda #$FF + clc + adc #1 + sta $0A + + lda #$00 + sec + sbc #1 + sta $0B + + rts |
