From 55f524d73e450c6f64b25c37af808496f5c1707e Mon Sep 17 00:00:00 2001 From: baltdev Date: Mon, 1 Apr 2024 23:22:35 -0500 Subject: [PATCH] Initial commit --- .gitignore | 12 + .idea/.gitignore | 8 + .idea/6502.iml | 12 + .idea/modules.xml | 8 + Cargo.toml | 29 ++ LICENSE-APACHE | 201 +++++++++++ LICENSE-MIT | 21 ++ README.md | 71 ++++ src/emulation.rs | 643 +++++++++++++++++++++++++++++++++ src/instructions.rs | 535 +++++++++++++++++++++++++++ src/lib.rs | 13 + src/state.rs | 105 ++++++ tests/6502_functional_test.bin | Bin 0 -> 65536 bytes tests/AllSuiteA.bin | Bin 0 -> 49152 bytes tests/test_roms.rs | 74 ++++ 15 files changed, 1732 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/6502.iml create mode 100644 .idea/modules.xml create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 src/emulation.rs create mode 100644 src/instructions.rs create mode 100644 src/lib.rs create mode 100644 src/state.rs create mode 100644 tests/6502_functional_test.bin create mode 100644 tests/AllSuiteA.bin create mode 100644 tests/test_roms.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acb3005 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/6502.iml b/.idea/6502.iml new file mode 100644 index 0000000..bbe0a70 --- /dev/null +++ b/.idea/6502.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..395c1d6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a2f6a3d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "r6502" +version = "0.1.0" +authors = ["baltdev"] +edition = "2021" +description = "A simple NMOS 6502 emulator." +license = "Apache-2.0 OR MIT" +documentation = "https://docs.rs/r6502" +repository = "" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + + +[dependencies] +bitflags = "2" + +bytemuck = {version = "1", features = ["derive", "min_const_generics"], optional = true} +arbitrary = {version = "1", features = ["derive"], optional = true} +serde = {version = "1", features = ["derive"], optional = true} +serde_with = {version = "3", optional = true, default-features = false, features = ["macros"]} +cfg_eval = {version = "0.1", optional = true} + +[features] +default = ["bcd"] +bcd = [] +bytemuck = ["dep:bytemuck", "bitflags/bytemuck"] +arbitrary = ["dep:arbitrary", "bitflags/arbitrary"] +serde = ["dep:serde", "dep:serde_with", "dep:cfg_eval", "bitflags/serde"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..2716cbf --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Balt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..05226bf --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/balt-dev/r6502/.github%2Fworkflows%2Frust.yml?branch=master&style=flat&label=tests)](https://github.com/balt-dev/descape/actions/) +[![Documentation](https://docs.rs/r6502/badge.svg)](https://docs.rs/r6502) +[![MSRV](https://img.shields.io/badge/MSRV-1.66.1-gold)](https://gist.github.com/alexheretic/d1e98d8433b602e57f5d0a9637927e0c) +[![Repository](https://img.shields.io/badge/-GitHub-%23181717?style=flat&logo=github&labelColor=%23555555&color=%23181717)](https://github.com/balt-dev/r6502) +[![Latest version](https://img.shields.io/crates/v/r6502.svg)](https://crates.io/crates/r6502) +[![License](https://img.shields.io/crates/l/r6502.svg)](https://github.com/balt-dev/r6502/blob/master/LICENSE-MIT) +[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) +# r6502 + +### Yet another NMOS 6502 emulator. + +--- + +Designed to support `no-std` and not require an allocator nor any unsafe code. + +The API of this crate shies away from implementing interrupt handling, +instead having you step the emulator one opcode at a time and handle them yourself. + +## Feature Flags +The following feature flags exist: + +| Name | Description | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| bcd | Enable binary-coded decimal arithmetic.
Enabled by default. Disable if you're writing a NES emulator.
Note that invalid BCD is left untested and will not function faithfully to the NMOS 6502. | +| bytemuck | Enables [bytemuck](https://docs.rs/bytemuck/) support. | +| arbitrary | Enables [arbitrary](https://docs.rs/arbitrary/) support. This will pull in `std`. | +| serde | Enables [serde](https://docs.rs/serde) support. | +| hashbrown | Enables [hashbrown](https://docs.rs/hashbrown) support. | + +## Example +```rust ignore +extern crate std; + +use std::eprintln; + +use r6502::{Emulator, FunctionReadCallback, FunctionWriteCallback}; + +fn main() { + let mut emu = Emulator::default() + .with_read_callback(FunctionReadCallback(|state: &mut State, addr| { + // Log reads + eprintln!("Read from #${addr:04x}"); + state.memory[addr as usize] + })) + .with_write_callback(FunctionWriteCallback(|state: &mut State, addr, byte| + // Don't write to ROM + if addr < 0xFF00 { + state.memory[addr as usize] = byte + }) + ) + .with_rom(include_bytes!("rom.bin")) + .with_program_counter(0x200); + + loop { + let interrupt_requested = emu.step() + .expect("found an invalid opcode (only NMOS 6502 opcodes are supported)"); + if interrupt_requested { // Go to IRQ interrupt vector + let vector = u16::from_le_bytes([ + emu.read(0xFFFE), + emu.read(0xFFFF) + ]); + emu.state.program_counter = vector; + } + } +} +``` + +--- +## Licensing + +This may be licensed under either the MIT or Apache-2.0 license, at your option. diff --git a/src/emulation.rs b/src/emulation.rs new file mode 100644 index 0000000..4b1cac0 --- /dev/null +++ b/src/emulation.rs @@ -0,0 +1,643 @@ +//! Handles everything pertaining to actual emulation of a 6502 processor. + +use crate::{AddressMode, Instruction, Opcode, State, Status}; + +/// A trait dictating a memory read callback for an emulator. +/// If you want to easily create a one-off implementation, see [`FunctionReadCallback`]. +pub trait ReadCallback { + /// The callback to be called when memory is read. + /// This will be called in place of actually reading the memory! + fn callback(&mut self, state: &mut State, address: u16) -> u8; +} + +/// A trait dictating a memory write callback for an emulator. +/// If you want to easily create a one-off implementation, see [`FunctionWriteCallback`]. +pub trait WriteCallback { + /// The callback to be called when memory is read. + /// This will be called in place of actually reading the memory! + fn callback(&mut self, state: &mut State, address: u16, byte: u8); +} + +/// A helper for easily creating simple implementations of [`ReadCallback`]. +pub struct FunctionReadCallback(pub F); + +impl ReadCallback for FunctionReadCallback where + F: FnMut(&mut State, u16) -> u8 +{ + fn callback(&mut self, state: &mut State, address: u16) -> u8 { + self.0(state, address) + } +} + +/// A helper for easily creating simple implementations of [`WriteCallback`]. +pub struct FunctionWriteCallback(pub F); + +impl WriteCallback for FunctionWriteCallback where + F: FnMut(&mut State, u16, u8) +{ + fn callback(&mut self, state: &mut State, address: u16, byte: u8) { + self.0(state, address, byte); + } +} + +/// Default implementor of [`ReadCallback`] for [`Emulator`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, core::hash::Hash, Ord, PartialOrd)] +pub struct DefaultReadCallback; + +impl ReadCallback for DefaultReadCallback { + fn callback(&mut self, state: &mut State, address: u16) -> u8 { + state.memory[address as usize] + } +} + +/// Default implementor of [`WriteCallback`] for [`Emulator`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, core::hash::Hash, Ord, PartialOrd)] +pub struct DefaultWriteCallback; + +impl WriteCallback for DefaultWriteCallback { + fn callback(&mut self, state: &mut State, address: u16, byte: u8) { + state.memory[address as usize] = byte; + } +} + +#[derive(Clone, PartialEq, Eq, core::hash::Hash)] +/// A wrapper around state to aid in emulation. +pub struct Emulator { + /// Contains the state of the emulator. + pub state: State, + /// Contains a callback to be called on memory reads. + pub read_callback: R, + /// Contains a callback to be called on memory writes. + pub write_callback: W, +} + +/// Gets the top and bottom nibbles of a byte +#[inline] +fn to_bcd_nibbles(value: u8) -> (u8, u8) { + (value >> 4, value & 0xF) +} + +/// Makes a byte from nibbles, also returning an overflow value if it was too large +#[inline] +fn from_bcd_nibbles(mut low: u8, mut high: u8) -> (u8, bool) { + let mut overflow = false; + high += low / 10; + low %= 10; + if high > 9 { + high %= 10; + overflow = true; + } + ((high << 4) + low, overflow) +} + +impl Emulator { + /// Sets the program counter in a way allowing for call chaining. + #[inline] + #[must_use] + pub const fn with_program_counter(mut self, counter: u16) -> Self { + self.state.program_counter = counter; + self + } + + /// Sets the ROM in a way allowing for call chaining. + #[inline] + #[must_use] + pub const fn with_rom(mut self, rom: [u8; 256 * 256]) -> Self { + self.state.memory = rom; + self + } + + /// Sets the ROM in a way allowing for call chaining. + /// + /// This version of the method copies the slice into rom at the given location. + /// + /// # Panics + /// Panics if the rom is too small to hold the slice at the given location. + #[inline] + #[must_use] + pub fn with_rom_from(mut self, slice: &[u8], location: u16) -> Self { + self.state.memory[location as usize..location as usize + slice.len()].copy_from_slice(slice); + self + } + + /// Sets the processor status in a way allowing for call chaining. + #[inline] + #[must_use] + pub const fn with_status(mut self, status: Status) -> Self { + self.state.status = status; + self + } + + /// Gets a reference to a single page of memory. + #[inline] + #[must_use] + pub fn page(&self, page: u8) -> &[u8; 256] { + // NOTE: + // This gets optimized down to run without any runtime checks in release mode, but not debug mode. + // See https://godbolt.org/z/nos8W9nK3 for details. + (&self.state.memory[(page as usize) * 256..(page as usize + 1) * 256]) + .try_into() + .unwrap() + } + + /// Gets a mutable reference to a single page of memory. + #[inline] + pub fn page_mut(&mut self, page: u8) -> &mut [u8; 256] { + (&mut self.state.memory[(page as usize) * 256..(page as usize + 1) * 256]) + .try_into() + .unwrap() + } + + /// Read a byte from memory, respecting read callbacks. + #[must_use] + #[inline] + pub fn read(&mut self, index: u16) -> u8 { + self.read_callback.callback(&mut self.state, index) + } + + /// Write a byte to memory, respecting write callbacks. + #[inline] + pub fn write(&mut self, index: u16, byte: u8) { + self.write_callback.callback(&mut self.state, index, byte); + } + + /// Push a byte to the stack. + pub fn push(&mut self, value: u8) { + let new_pointer = self.state.stack_pointer.wrapping_sub(1); + self.write(0x100 + u16::from(self.state.stack_pointer), value); + self.state.stack_pointer = new_pointer; + } + + /// Pops a byte from the stack. + pub fn pop(&mut self) -> u8 { + self.state.stack_pointer = self.state.stack_pointer.wrapping_add(1); + self.read(0x100 + u16::from(self.state.stack_pointer)) + } + + /// Gets the byte on the top of the stack. + #[inline] + #[must_use] + pub fn peek(&mut self) -> u8 { + self.read(0x100 + u16::from(self.state.stack_pointer)) + } + + + /// Sets a callback to be called when memory is read from, allowing for memory-mapped reads. + /// See [`ReadCallback`]. + #[inline] + #[must_use] + pub fn with_read_callback(self, callback: R2) -> Emulator { + Emulator { + state: self.state, + read_callback: callback, + write_callback: self.write_callback + } + } + + /// Sets a callback to be called when memory is written to, allowing for memory-mapped writes. + /// See [`WriteCallback`]. + #[inline] + #[must_use] + pub fn with_write_callback(self, callback: W2) -> Emulator { + Emulator { + state: self.state, + read_callback: self.read_callback, + write_callback: callback + } + } +} + +impl Default for Emulator { + fn default() -> Self { + Self { + state: State::default(), + read_callback: DefaultReadCallback, + write_callback: DefaultWriteCallback + } + } +} + + +#[allow(clippy::missing_fields_in_debug)] +impl core::fmt::Debug for Emulator { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + fmt.debug_struct("Emulator") + .field("state", &self.state) + .field("memory_read_callback", &self.read_callback) + .field("memory_write_callback", &self.write_callback) + .finish() + } +} + +#[allow( + clippy::cast_possible_truncation, + clippy::enum_glob_use, + clippy::cast_possible_wrap, + clippy::cast_sign_loss +)] +impl Emulator { + /// Gets an address from an address mode. + fn addr(&mut self, mode: AddressMode) -> Option { + use AddressMode::*; + Some(match mode { + Absolute(addr) => addr, + ZeroPage(addr) => u16::from(addr), + Relative(offset) => self.state.program_counter.wrapping_add_signed(i16::from(offset)), + AbsoluteIndirect(addr) => { + let high = self.read(addr); + let low = self.read(addr.wrapping_add(1)); + u16::from_le_bytes([high, low]) + } + AbsoluteX(addr) => addr.wrapping_add(u16::from(self.state.x_register)), + AbsoluteY(addr) => addr.wrapping_add(u16::from(self.state.y_register)), + // Wrapping around the zero page seems to be consistent with actual behavior of the 6502. + ZeroPageX(addr) => u16::from(addr.wrapping_add(self.state.x_register)), + ZeroPageY(addr) => u16::from(addr.wrapping_add(self.state.y_register)), + ZeroPageIndirectX(addr) => { + let shifted_addr = addr.wrapping_add(self.state.x_register); + let high = self.read(u16::from(shifted_addr)); + let low = self.read(u16::from(shifted_addr.wrapping_add(1))); + u16::from_le_bytes([high, low]) + } + ZeroPageYIndirect(addr) => { + let high = self.read(u16::from(addr)); + let low = self.read(u16::from(addr.wrapping_add(1))); + u16::from_le_bytes([high, low]).wrapping_add(u16::from(self.state.y_register)) + } + _ => return None, + }) + } + + /// Gets a byte from an address mode + fn byte(&mut self, mode: AddressMode) -> u8 { + use AddressMode::*; + match mode { + Accumulator => self.state.accumulator, + Immediate(value) => value, + _ => { + let addr = self.addr(mode).expect("address should be valid here"); + self.read(addr) + } + } + } + + /// Writes to the target for an address mode + fn write_mode(&mut self, mode: AddressMode, value: u8) { + use AddressMode::*; + match mode { + Accumulator => self.state.accumulator = value, + Immediate(_) => {} + _ => { + let addr = self.addr(mode).expect("address should be valid here"); + self.write(addr, value); + } + } + } + + /// Set flags from an arithmetic evaluation + fn set_flags(&mut self, value: u8) -> u8 { + self.state.status.set(Status::ZERO, value == 0); + self.state.status.set(Status::NEGATIVE, (value as i8) < 0); + value + } + + /// Steps the state by one opcode. + /// + /// On success, returns a boolean value on whether an interrupt was raised that cycle. + /// + /// If an interrupt is not handled + /// (i.e. setting the interrupt flag back to 0 and popping the program counter and status) + /// before execution continues, your 6502 code may misbehave! + /// + /// # Errors + /// Will error if given an invalid opcode, passing back its index in memory. + pub fn step(&mut self) -> Result { + // Get the opcode from memory + let mut length = 1; + let starting_byte = self.read(self.state.program_counter); + let Some(opcode) = Opcode::load(&[starting_byte]) + .or_else(|needed| { + // This is a really, REALLY hacky way to do this. + // However, it doesn't need an allocator. + length = needed; + match needed { + 2 => Opcode::load(&[ + starting_byte, + self.read(self.state.program_counter.wrapping_add(1)), + ]), + 3 => Opcode::load(&[ + starting_byte, + self.read(self.state.program_counter.wrapping_add(1)), + self.read(self.state.program_counter.wrapping_add(2)), + ]), + v => Err(v), + } + }) + .expect("all opcodes need between 1 and 3 bytes") + else { + // We have an invalid opcode! + return Err(self.state.program_counter); + }; + + self.process_opcode(opcode, length as u8) + .ok_or(self.state.program_counter) + } + + /// Processes a single opcode. + /// + /// Returns `None` if the opcode is invalid. + #[allow(clippy::too_many_lines)] + pub fn process_opcode(&mut self, opcode: Opcode, opcode_length: u8) -> Option { + use Instruction::*; + + let mut increment = true; + + match opcode.instruction { + LDA => { + let loaded = opcode.address_mode.map(|v| self.byte(v))?; + self.state.accumulator = self.set_flags(loaded); + } + LDX => { + let loaded = opcode.address_mode.map(|v| self.byte(v))?; + self.state.x_register = self.set_flags(loaded); + } + LDY => { + let loaded = opcode.address_mode.map(|v| self.byte(v))?; + self.state.y_register = self.set_flags(loaded); + } + STA => { + let addr = opcode.address_mode.and_then(|v| self.addr(v))?; + self.write(addr, self.state.accumulator); + } + STX => { + let addr = opcode.address_mode.and_then(|v| self.addr(v))?; + self.write(addr, self.state.x_register); + } + STY => { + let addr = opcode.address_mode.and_then(|v| self.addr(v))?; + self.write(addr, self.state.y_register); + } + ADC => opcode.address_mode.map(|mode| { + let rhs = self.byte(mode); + if cfg!(feature = "bcd") && self.state.status.contains(Status::DECIMAL) { + let lhs = self.state.accumulator; + + let (high_lhs, low_lhs) = to_bcd_nibbles(lhs); + let (high_rhs, low_rhs) = to_bcd_nibbles(rhs); + let low_sum = low_lhs + low_rhs + u8::from(self.state.status.contains(Status::CARRY)); + let (low_carry, low) = (low_sum / 10, low_sum % 10); + let high_sum = high_lhs + high_rhs + low_carry; + let (sum, carry) = from_bcd_nibbles(low, high_sum); + self.state.status.set(Status::CARRY, carry); + let overflow_sum = u16::from(from_bcd(lhs)) + u16::from(from_bcd(rhs)) + u16::from(self.state.status.contains(Status::CARRY)); + self.state.status.set(Status::OVERFLOW, (lhs & 0x80) != (overflow_sum & 0x80) as u8); + self.state.status.set(Status::ZERO, sum == 0); + self.state.status.set(Status::NEGATIVE, (sum as i8) < 0); + self.state.accumulator = sum; + } else { + self.state.accumulator = self.adc(rhs); + } + })?, + SBC => opcode.address_mode.map(|mode| { + let rhs = self.byte(mode); + if cfg!(feature = "bcd") && self.state.status.contains(Status::DECIMAL) { + let lhs = self.state.accumulator; + + let (high_lhs, low_lhs) = to_bcd_nibbles(lhs); + let (high_rhs, low_rhs) = to_bcd_nibbles(rhs); + let mut low = low_lhs as i8 - low_rhs as i8 - i8::from(!self.state.status.contains(Status::CARRY)); + let mut low_borrow = 0; + if low < 0 { + low += 10; + low_borrow = 1; + } + let mut high = high_lhs as i8 - high_rhs as i8 - low_borrow; + self.state.status.set(Status::OVERFLOW, false); + if high < 0 { + high += 10; + self.state.status.set(Status::CARRY, false); // Take the carry out + if !self.state.status.contains(Status::CARRY) { + self.state.status.set(Status::OVERFLOW, true); + } + } else { + self.state.status.set(Status::CARRY, true); + } + let (diff, _) = from_bcd_nibbles(low as u8, high as u8); + + self.state.status.set(Status::NEGATIVE, (diff as i8) < 0); + self.state.status.set(Status::ZERO, diff == 0); + self.state.accumulator = diff; + } else { + self.state.accumulator = self.adc(!rhs); + } + })?, + INC => opcode.address_mode.map(|mode| { + let new = self.byte(mode).wrapping_add(1); + self.set_flags(new); + self.write_mode(mode, new); + })?, + INX => self.state.x_register = self.set_flags(self.state.x_register.wrapping_add(1)), + INY => self.state.y_register = self.set_flags(self.state.y_register.wrapping_add(1)), + DEC => opcode.address_mode.map(|mode| { + let new = self.byte(mode).wrapping_sub(1); + self.set_flags(new); + self.write_mode(mode, new); + })?, + DEX => self.state.x_register = self.set_flags(self.state.x_register.wrapping_sub(1)), + DEY => self.state.y_register = self.set_flags(self.state.y_register.wrapping_sub(1)), + ASL => opcode.address_mode.map(|mode| { + let mut old = self.byte(mode); + self.state.status.set(Status::CARRY, old & 0b1000_0000 != 0); + old <<= 1; + self.set_flags(old); + self.write_mode(mode, old); + })?, + LSR => opcode.address_mode.map(|mode| { + let mut old = self.byte(mode); + self.state.status.set(Status::CARRY, old & 0b0000_0001 != 0); + old >>= 1; + self.set_flags(old); + self.write_mode(mode, old); + })?, + ROL => opcode.address_mode.map(|mode| { + let mut old = self.byte(mode); + let high_bit = (old & 0b1000_0000) != 0; + old <<= 1; + old |= u8::from(self.state.status.contains(Status::CARRY)); + self.state.status.set(Status::CARRY, high_bit); + self.set_flags(old); + self.write_mode(mode, old); + })?, + ROR => opcode.address_mode.map(|mode| { + let mut old = self.byte(mode); + let low_bit = (old & 0b0000_0001) != 0; + old >>= 1; + old |= u8::from(self.state.status.contains(Status::CARRY)) << 7; + self.state.status.set(Status::CARRY, low_bit); + self.set_flags(old); + self.write_mode(mode, old); + })?, + AND => opcode.address_mode.map(|mode| { + self.state.accumulator &= self.byte(mode); + self.set_flags(self.state.accumulator); + })?, + ORA => opcode.address_mode.map(|mode| { + self.state.accumulator |= self.byte(mode); + self.set_flags(self.state.accumulator); + })?, + EOR => opcode.address_mode.map(|mode| { + self.state.accumulator ^= self.byte(mode); + self.set_flags(self.state.accumulator); + })?, + BIT => opcode.address_mode.map(|mode| { + let byte = self.byte(mode); + let result = self.state.accumulator & byte; + self.state.status.set(Status::ZERO, result == 0); + self.state.status.set(Status::NEGATIVE, byte & 0b1000_0000 != 0); + self.state.status.set(Status::OVERFLOW, byte & 0b0100_0000 != 0); + })?, + CMP => opcode.address_mode.map(|mode| + self.compare(self.state.accumulator, mode) + )?, + CPX => opcode.address_mode.map(|mode| + self.compare(self.state.x_register, mode) + )?, + CPY => opcode.address_mode.map(|mode| + self.compare(self.state.y_register, mode) + )?, + inst @ (BCC | BNE | BPL | BVC | BCS | BEQ | BMI | BVS) => { + opcode.address_mode.and_then(|mode| { + let mask = match inst { + BCC | BCS => Status::CARRY, + BNE | BEQ => Status::ZERO, + BPL | BMI => Status::NEGATIVE, + BVC | BVS => Status::OVERFLOW, + _ => unreachable!(), + }; + let inverse = matches!(inst, BCC | BNE | BPL | BVC); + if inverse ^ self.state.status.contains(mask) { + self.state.program_counter = self.addr(mode)?; + } + Some(()) + })?; + } + TAX => self.state.x_register = self.set_flags(self.state.accumulator), + TAY => self.state.y_register = self.set_flags(self.state.accumulator), + TXA => self.state.accumulator = self.set_flags(self.state.x_register), + TYA => self.state.accumulator = self.set_flags(self.state.y_register), + TSX => self.state.x_register = self.set_flags(self.state.stack_pointer), + TXS => self.state.stack_pointer = self.state.x_register, + PHA => self.push(self.state.accumulator), + PLA => { + let new_acc = self.pop(); + self.state.accumulator = self.set_flags(new_acc); + }, + PHP => self.push((self.state.status | Status::UNUSED | Status::BREAK).bits()), + PLP => self.state.status = Status::from_bits_retain(self.pop()) | Status::UNUSED, + JMP => opcode.address_mode.and_then(|mode| { + self.state.program_counter = self.addr(mode)?; + increment = false; + Some(()) + })?, + JSR => opcode.address_mode.and_then(|mode| { + let [high, low] = + self.state.program_counter + .wrapping_add(u16::from(opcode_length)) + .wrapping_sub(1) + .to_le_bytes(); + self.state.program_counter = self.addr(mode)?; + self.push(low); + self.push(high); + increment = false; + Some(()) + })?, + RTS => { + let addr = u16::from_le_bytes([self.pop(), self.pop()]); + self.state.program_counter = addr; + } + inst @ (CLC | SEC | CLD | SED | CLI | SEI | CLV) => { + let mask = match inst { + CLC | SEC => Status::CARRY, + CLD | SED => Status::DECIMAL, + CLI | SEI => Status::INTERRUPT_DISABLE, + CLV => Status::OVERFLOW, + _ => unreachable!(), + }; + let target = matches!(inst, SEC | SED | SEI); + self.state.status.set(mask, target); + } + BRK => { + self.force_interrupt(); + return Some(true); + } + RTI => { + self.state.status = Status::from_bits_retain(self.pop()) & !Status::BREAK; + let bytes = [self.pop(), self.pop()]; + self.state.program_counter = u16::from_le_bytes(bytes); + increment = false; + } + NOP => {} + } + + if increment { + self.state.program_counter = self.state.program_counter.wrapping_add(u16::from(opcode_length)); + } + + Some(false) + } + + /// Requests a maskable interrupt. Returns whether the interrupt happened. + pub fn request_interrupt(&mut self) -> bool { + if !self.state.status.contains(Status::INTERRUPT_DISABLE) { + self.force_interrupt(); + return true; + } + false + } + + /// Forces a non-maskable interrupt. + pub fn force_interrupt(&mut self) { + self.state.program_counter = self.state.program_counter.wrapping_add(2); + let [high, low] = self.state.program_counter.to_le_bytes(); + self.push(low); + self.push(high); + self.push((self.state.status | Status::BREAK).bits()); + self.state.status.set(Status::INTERRUPT_DISABLE, true); + } + + /// Drives CMP, CPX, and CPY + fn compare(&mut self, lhs: u8, mode: AddressMode) { + let rhs = self.byte(mode); + let diff = lhs.wrapping_sub(rhs); + + self.state.status.set(Status::ZERO, diff == 0); + self.state.status.set(Status::CARRY, (diff as i8) >= 0); + self.state.status.set(Status::NEGATIVE, (diff as i8) < 0); + } + + /// Computes binary add with carry + fn adc(&mut self, rhs: u8) -> u8 { + let sum = + u16::from(self.state.accumulator) + + u16::from(rhs) + + u16::from(self.state.status.contains(Status::CARRY)); + + let acc = u16::from(self.state.accumulator); + let rhs = u16::from(rhs); + + self.state.status.set(Status::CARRY, sum > 0xFF); + self.state.status.set(Status::OVERFLOW, !(acc ^ rhs) & (acc ^ sum) & 0x80 != 0); + + let sum = sum as u8; + + self.state.status.set(Status::ZERO, sum == 0); + self.state.status.set(Status::NEGATIVE, (sum as i8) < 0); + + sum + } +} + +/// Converts a value from BCD to a normal number +fn from_bcd(val: u8) -> u8 { + 10 * (val >> 4) + (0x0F & val) +} diff --git a/src/instructions.rs b/src/instructions.rs new file mode 100644 index 0000000..5169d79 --- /dev/null +++ b/src/instructions.rs @@ -0,0 +1,535 @@ +//! A module containing all structures pertaining to instructions and opcodes. + +use core::fmt; +use core::fmt::Formatter; + +#[cfg(feature = "arbitrary")] +extern crate std; + +/// An enumeration over all 6502 memory addressing modes. +#[derive(Debug, Copy, Clone, PartialEq, Eq, core::hash::Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum AddressMode { + /// Use the accumulator as the operand. + Accumulator, + /// Use the next byte as the operand. + Immediate(u8), + /// Use the byte at the address as the operand. + Absolute(u16), + /// Use the byte at the address in the zero page as the operand. + ZeroPage(u8), + /// Use a byte offset from the current program counter as the operand. + Relative(i8), + /// Use a byte at the address stored at the address as the operand. + AbsoluteIndirect(u16), + /// Use a byte at the address specified plus the X register as the operand. + AbsoluteX(u16), + /// Use a byte at the address specified plus the Y register as the operand. + AbsoluteY(u16), + /// Use a byte at the address in the zero page plus the X register as the operand. + ZeroPageX(u8), + /// Use a byte at the address in the zero page plus the Y register as the operand. + ZeroPageY(u8), + /// Use a byte at the address stored at "the address in the zero page plus the X register" as the operand. + ZeroPageIndirectX(u8), + /// Use a byte at the address stored at "the address in the zero page" plus the Y register as the operand. + ZeroPageYIndirect(u8), +} + +impl fmt::Display for AddressMode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + AddressMode::Accumulator => Ok(()), + AddressMode::Immediate(n) => write!(f, "#${n:02X}"), + AddressMode::Absolute(n) => write!(f, "${n:04X}"), + AddressMode::ZeroPage(n) => write!(f, "${n:02X}"), + AddressMode::Relative(n) => write!(f, "${n:02X}"), + AddressMode::AbsoluteIndirect(n) => write!(f, "(${n:04X})"), + AddressMode::AbsoluteX(n) => write!(f, "${n:04X},X"), + AddressMode::AbsoluteY(n) => write!(f, "${n:04X},Y"), + AddressMode::ZeroPageX(n) => write!(f, "${n:02X},X"), + AddressMode::ZeroPageY(n) => write!(f, "${n:02X},Y"), + AddressMode::ZeroPageIndirectX(n) => write!(f, "(${n:02X},X)"), + AddressMode::ZeroPageYIndirect(n) => write!(f, "(${n:02X}),Y") + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, core::hash::Hash)] +#[allow(non_camel_case_types)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// An enumeration over all instructions in a 6502. +pub enum Instruction { + /// Load a byte into the accumulator. + LDA, + /// Load a byte into the X register. + LDX, + /// Load a byte into the Y register. + LDY, + /// Store the accumulator into memory. + STA, + /// Store the X register into memory. + STX, + /// Store the Y register into memory. + STY, + /// Add a byte to the accumulator. + ADC, + /// Subtract a byte from the accumulator. + SBC, + /// Increment a byte by one. + INC, + /// Increment the X register by one. + INX, + /// Increment the Y register by one. + INY, + /// Decrement a byte by one. + DEC, + /// Decrement the X register by one. + DEX, + /// Decrement the Y register by one. + DEY, + /// Arithmetically bit-shift a byte to the left by one. + ASL, + /// Logically bit-shift a byte to the right by one. + LSR, + /// Rotate the bits of a byte leftwards, in and out of the carry bit. + ROL, + /// Rotate the bits of a byte rightwards, in and out of the carry bit. + ROR, + /// Take the bitwise AND of the accumulator and a byte. + AND, + /// Take the bitwise OR of the accumulator and a byte. + ORA, + /// Take the bitwise XOR of the accumulator and a byte. + EOR, + /// Tests a byte's bits with the accumulator. + BIT, + /// Compare a byte with the value in the accumulator. + CMP, + /// Compare a byte with the value in the X register. + CPX, + /// Compare a byte with the value in the Y register. + CPY, + /// Branches if the carry bit of the processor status is clear. + BCC, + /// Branches if the carry bit of the processor status is set. + BCS, + /// Branches if the zero bit of the processor status is clear. + BNE, + /// Branches if the zero bit of the processor status is set. + BEQ, + /// Branches if the negative bit of the processor status is clear. + BPL, + /// Branches if the negative bit of the processor status is set. + BMI, + /// Branches if the overflow bit of the processor status is clear. + BVC, + /// Branches if the overflow bit of the processor status is set. + BVS, + /// Transfers the byte in the accumulator to the X register. + TAX, + /// Transfers the byte in the accumulator to the Y register. + TAY, + /// Transfers the byte in the X register to the accumulator. + TXA, + /// Transfers the byte in the Y register to the accumulator. + TYA, + /// Transfers the stack pointer to the X register. + TSX, + /// Transfers the X register to the stack pointer. + TXS, + /// Pushes the accumulator to the stack. + PHA, + /// Pulls the accumulator from the stack. + PLA, + /// Pushes the processor status to the stack. + PHP, + /// Pulls the processor status from the stack. + PLP, + /// Jumps the program counter to a new location. + /// + /// # Note + /// Due to a hardware bug in the original 6502 microprocessor (which is reproduced here for accuracy), + /// in [AddressMode::AbsoluteIndirect] addressing mode, the high byte will be read from the + /// start of the current page instead of the next one when the low byte's address is + /// at the end of a page (i.e. the address mod 256 is 255). + JMP, + /// Jumps the program counter to a new location, pushing the current location to the stack. + JSR, + /// Pops a return address from the stack and sets the program counter to it. + RTS, + /// Handles returning from an interrupt by popping the status and program counter from the stack. + RTI, + /// Clears the carry flag. + CLC, + /// Sets the carry flag. + SEC, + /// Clears the decimal mode flag. + CLD, + /// Sets the decimal mode flag. + SED, + /// Clears the interrupt disabling flag. + CLI, + /// Sets the interrupt disabling flag. + SEI, + /// Clears the overflow flag. + CLV, + /// Forces a hardware interrupt. + BRK, + /// Does nothing. + NOP +} + +impl Instruction { + /// Alias for [`Instruction::EOR`] that fits the modern language of calling exclusive or XOR. + pub const XOR: Instruction = Self::EOR; +} + +impl fmt::Display for Instruction { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") // The Debug impl is good for this as well + } +} + +/// A struct representing a ([`Instruction`], [`AddressMode`]) pair as an opcode. +/// +/// Not all possible states of this struct are valid opcodes - +/// some may have address modes that are invalid for the instruction. +#[derive(Debug, Copy, Clone, PartialEq, Eq, core::hash::Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct Opcode { + /// The instruction of the opcode. + pub instruction: Instruction, + /// The address mode of the opcode if it has one. + pub address_mode: Option, +} + +impl fmt::Display for Opcode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.instruction)?; + if let Some(mode) = self.address_mode { + write!(f, " {mode}")?; + } + Ok(()) + } +} + +impl Opcode { + /// Creates a new opcode from an instruction and an addressing mode. + #[must_use] + #[inline] + pub fn new(instruction: Instruction, address_mode: Option) -> Self { + Self { instruction, address_mode } + } +} + +macro_rules! opcodes { + ($(Opcode::new($inst: ident, $($tt: tt)+) => $repr: literal),* $(,)?) => { + impl Opcode { + /// Loads an opcode from a byte slice. + /// + /// # Errors + /// If not enough bytes are supplied, returns an `Err` with the amount of bytes needed. + /// If the bit pattern is not a valid opcode, returns `Ok(None)`. + pub fn load(data: &[u8]) -> Result, usize> { + if data.is_empty() { return Err(1) } + match data[0] { + $($repr => opcodes!(__handle data $inst $($tt)+)),*, + _ => Ok(None) + } + } + + /// Dumps an opcode into a buffer. + /// + /// # Errors + /// If the opcode is not valid, returns an `Ok(false)`. + /// If the opcode won't fit in the buffer, returns an `Err` with how many bytes it needs. + pub fn dump(&self, buf: &mut [u8]) -> Result { + if buf.is_empty() { return Err(1) } + $(opcodes!{__dump self $inst buf $repr $($tt)+})* + Ok(false) + } + } + }; + (__handle $data: ident $inst: ident None) => { + Ok(Some(Opcode::new(Instruction::$inst, None))) + }; + (__handle $data: ident $inst: ident Some(Accumulator)) => { + Ok(Some(Opcode::new(Instruction::$inst, Some(AddressMode::Accumulator)))) + }; + (__handle $data: ident $inst: ident Some(Immediate)) => { + opcodes!(__handle_u8 $data $inst Immediate) + }; + (__handle $data: ident $inst: ident Some(Absolute)) => { + opcodes!(__handle_u16 $data $inst Absolute) + }; + (__handle $data: ident $inst: ident Some(ZeroPage)) => { + opcodes!(__handle_u8 $data $inst ZeroPage) + }; + (__handle $data: ident $inst: ident Some(Relative)) => {{ + let Some(immediate_byte) = $data.get(1) else { return Err(2) }; + Ok(Some(Opcode::new(Instruction::$inst, Some(AddressMode::Relative(*immediate_byte as i8))))) + }}; + (__handle $data: ident $inst: ident Some(AbsoluteIndirect)) => { + opcodes!(__handle_u16 $data $inst AbsoluteIndirect) + }; + (__handle $data: ident $inst: ident Some(AbsoluteX)) => { + opcodes!(__handle_u16 $data $inst AbsoluteX) + }; + (__handle $data: ident $inst: ident Some(AbsoluteY)) => { + opcodes!(__handle_u16 $data $inst AbsoluteY) + }; + (__handle $data: ident $inst: ident Some(ZeroPageX)) => { + opcodes!(__handle_u8 $data $inst ZeroPageX) + }; + (__handle $data: ident $inst: ident Some(ZeroPageY)) => { + opcodes!(__handle_u8 $data $inst ZeroPageY) + }; + (__handle $data: ident $inst: ident Some(ZeroPageIndirectX)) => { + opcodes!(__handle_u8 $data $inst ZeroPageIndirectX) + }; + (__handle $data: ident $inst: ident Some(ZeroPageYIndirect)) => { + opcodes!(__handle_u8 $data $inst ZeroPageYIndirect) + }; + + (__dump $self: ident $inst: ident $buf: ident $repr: literal None) => { + if let Opcode {instruction: Instruction::$inst, address_mode: None} = $self { + $buf[0] = $repr; + return Ok(true); + }; + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(Accumulator)) => { + if let Opcode {instruction: Instruction::$inst, address_mode: Some(AddressMode::Accumulator)} = $self { + $buf[0] = $repr; + return Ok(true); + }; + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(Immediate)) => { + opcodes!{__dump_u8 $self $inst $buf $repr Immediate} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(Absolute)) => { + opcodes!{__dump_u16 $self $inst $buf $repr Absolute} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(ZeroPage)) => { + opcodes!{__dump_u8 $self $inst $buf $repr ZeroPage} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(Relative)) => { + if let Opcode {instruction: Instruction::$inst, address_mode: Some(AddressMode::Relative(v))} = $self { + if $buf.len() > 2 { return Err(2) } + $buf[0] = $repr; + $buf[1] = *v as u8; + return Ok(true); + }; + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(AbsoluteIndirect)) => { + opcodes!{__dump_u16 $self $inst $buf $repr AbsoluteIndirect} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(AbsoluteX)) => { + opcodes!{__dump_u16 $self $inst $buf $repr AbsoluteX} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(AbsoluteY)) => { + opcodes!{__dump_u16 $self $inst $buf $repr AbsoluteY} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(ZeroPageX)) => { + opcodes!{__dump_u8 $self $inst $buf $repr ZeroPageX} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(ZeroPageY)) => { + opcodes!{__dump_u8 $self $inst $buf $repr ZeroPageY} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(ZeroPageIndirectX)) => { + opcodes!{__dump_u8 $self $inst $buf $repr ZeroPageIndirectX} + }; + (__dump $self: ident $inst: ident $buf: ident $repr: literal Some(ZeroPageYIndirect)) => { + opcodes!{__dump_u8 $self $inst $buf $repr ZeroPageYIndirect} + }; + + (__handle_u8 $data: ident $inst: ident $name: ident) => {{ + let Some(immediate_byte) = $data.get(1) else { return Err(2) }; + Ok(Some(Opcode::new(Instruction::$inst, Some(AddressMode::$name(*immediate_byte))))) + }}; + (__handle_u16 $data: ident $inst: ident $name: ident) => {{ + let [Some(absolute_1), Some(absolute_2)] = [$data.get(1), $data.get(2)] + else { return Err(3) }; + Ok(Some(Opcode::new(Instruction::$inst, Some(AddressMode::$name( + u16::from_le_bytes([*absolute_1, *absolute_2]) + ))))) + }}; + + (__dump_u8 $self: ident $inst: ident $buf: ident $repr: literal $name: ident) => { + if let Opcode {instruction: Instruction::$inst, address_mode: Some(AddressMode::$name(v))} = $self { + if $buf.len() > 2 { return Err(2) } + $buf[0] = $repr; + $buf[1] = *v; + return Ok(true); + }; + }; + (__dump_u16 $self: ident $inst: ident $buf: ident $repr: literal $name: ident) => { + if let Opcode {instruction: Instruction::$inst, address_mode: Some(AddressMode::$name(v))} = $self { + if $buf.len() > 3 { return Err(3) } + let [low, high] = v.to_le_bytes(); + $buf[0] = $repr; + $buf[1] = low; + $buf[2] = high; + return Ok(true); + }; + } +} + + +opcodes! { + Opcode::new(BRK, None) => 0x00, + Opcode::new(ORA, Some(ZeroPageIndirectX)) => 0x01, + Opcode::new(ORA, Some(ZeroPage)) => 0x05, + Opcode::new(ASL, Some(ZeroPage)) => 0x06, + Opcode::new(PHP, None) => 0x08, + Opcode::new(ORA, Some(Immediate)) => 0x09, + Opcode::new(ASL, Some(Accumulator)) => 0x0A, + Opcode::new(ORA, Some(Absolute)) => 0x0D, + Opcode::new(ASL, Some(Absolute)) => 0x0E, + Opcode::new(BPL, Some(Relative)) => 0x10, + Opcode::new(ORA, Some(ZeroPageYIndirect)) => 0x11, + Opcode::new(ORA, Some(ZeroPageX)) => 0x15, + Opcode::new(ASL, Some(ZeroPageX)) => 0x16, + Opcode::new(CLC, None) => 0x18, + Opcode::new(ORA, Some(AbsoluteY)) => 0x19, + Opcode::new(ORA, Some(AbsoluteX)) => 0x1D, + Opcode::new(ASL, Some(AbsoluteX)) => 0x1E, + Opcode::new(JSR, Some(Absolute)) => 0x20, + Opcode::new(AND, Some(ZeroPageIndirectX)) => 0x21, + Opcode::new(BIT, Some(ZeroPage)) => 0x24, + Opcode::new(AND, Some(ZeroPage)) => 0x25, + Opcode::new(ROL, Some(ZeroPage)) => 0x26, + Opcode::new(PLP, None) => 0x28, + Opcode::new(AND, Some(Immediate)) => 0x29, + Opcode::new(ROL, Some(Accumulator)) => 0x2A, + Opcode::new(BIT, Some(Absolute)) => 0x2C, + Opcode::new(AND, Some(Absolute)) => 0x2D, + Opcode::new(ROL, Some(Absolute)) => 0x2E, + Opcode::new(BMI, Some(Relative)) => 0x30, + Opcode::new(AND, Some(ZeroPageYIndirect)) => 0x31, + Opcode::new(AND, Some(ZeroPageX)) => 0x35, + Opcode::new(ROL, Some(ZeroPageX)) => 0x36, + Opcode::new(SEC, None) => 0x38, + Opcode::new(AND, Some(AbsoluteY)) => 0x39, + Opcode::new(AND, Some(AbsoluteX)) => 0x3D, + Opcode::new(ROL, Some(AbsoluteX)) => 0x3E, + Opcode::new(RTI, None) => 0x40, + Opcode::new(EOR, Some(ZeroPageIndirectX)) => 0x41, + Opcode::new(EOR, Some(ZeroPage)) => 0x45, + Opcode::new(LSR, Some(ZeroPage)) => 0x46, + Opcode::new(PHA, None) => 0x48, + Opcode::new(EOR, Some(Immediate)) => 0x49, + Opcode::new(LSR, Some(Accumulator)) => 0x4A, + Opcode::new(JMP, Some(Absolute)) => 0x4C, + Opcode::new(EOR, Some(Absolute)) => 0x4D, + Opcode::new(LSR, Some(Absolute)) => 0x4E, + Opcode::new(BVC, Some(Relative)) => 0x50, + Opcode::new(EOR, Some(ZeroPageYIndirect)) => 0x51, + Opcode::new(EOR, Some(ZeroPageX)) => 0x55, + Opcode::new(LSR, Some(ZeroPageX)) => 0x56, + Opcode::new(CLI, None) => 0x58, + Opcode::new(EOR, Some(AbsoluteY)) => 0x59, + Opcode::new(EOR, Some(AbsoluteX)) => 0x5D, + Opcode::new(LSR, Some(AbsoluteX)) => 0x5E, + Opcode::new(RTS, None) => 0x60, + Opcode::new(ADC, Some(ZeroPageIndirectX)) => 0x61, + Opcode::new(ADC, Some(ZeroPage)) => 0x65, + Opcode::new(ROR, Some(ZeroPage)) => 0x66, + Opcode::new(PLA, None) => 0x68, + Opcode::new(ADC, Some(Immediate)) => 0x69, + Opcode::new(ROR, Some(Accumulator)) => 0x6A, + Opcode::new(JMP, Some(AbsoluteIndirect)) => 0x6C, + Opcode::new(ADC, Some(Absolute)) => 0x6D, + Opcode::new(ROR, Some(Absolute)) => 0x6E, + Opcode::new(BVS, Some(Relative)) => 0x70, + Opcode::new(ADC, Some(ZeroPageYIndirect)) => 0x71, + Opcode::new(ADC, Some(ZeroPageX)) => 0x75, + Opcode::new(ROR, Some(ZeroPageX)) => 0x76, + Opcode::new(SEI, None) => 0x78, + Opcode::new(ADC, Some(AbsoluteY)) => 0x79, + Opcode::new(ADC, Some(AbsoluteX)) => 0x7D, + Opcode::new(ROR, Some(AbsoluteX)) => 0x7E, + Opcode::new(STA, Some(ZeroPageIndirectX)) => 0x81, + Opcode::new(STY, Some(ZeroPage)) => 0x84, + Opcode::new(STA, Some(ZeroPage)) => 0x85, + Opcode::new(STX, Some(ZeroPage)) => 0x86, + Opcode::new(DEY, None) => 0x88, + Opcode::new(BIT, Some(Immediate)) => 0x89, + Opcode::new(TXA, None) => 0x8A, + Opcode::new(STY, Some(Absolute)) => 0x8C, + Opcode::new(STA, Some(Absolute)) => 0x8D, + Opcode::new(STX, Some(Absolute)) => 0x8E, + Opcode::new(BCC, Some(Relative)) => 0x90, + Opcode::new(STA, Some(ZeroPageYIndirect)) => 0x91, + Opcode::new(STY, Some(ZeroPageX)) => 0x94, + Opcode::new(STA, Some(ZeroPageX)) => 0x95, + Opcode::new(STX, Some(ZeroPageY)) => 0x96, + Opcode::new(TYA, None) => 0x98, + Opcode::new(STA, Some(AbsoluteY)) => 0x99, + Opcode::new(TXS, None) => 0x9A, + Opcode::new(STA, Some(AbsoluteX)) => 0x9D, + Opcode::new(LDY, Some(Immediate)) => 0xA0, + Opcode::new(LDA, Some(ZeroPageIndirectX)) => 0xA1, + Opcode::new(LDX, Some(Immediate)) => 0xA2, + Opcode::new(LDY, Some(ZeroPage)) => 0xA4, + Opcode::new(LDA, Some(ZeroPage)) => 0xA5, + Opcode::new(LDX, Some(ZeroPage)) => 0xA6, + Opcode::new(TAY, None) => 0xA8, + Opcode::new(LDA, Some(Immediate)) => 0xA9, + Opcode::new(TAX, None) => 0xAA, + Opcode::new(LDY, Some(Absolute)) => 0xAC, + Opcode::new(LDA, Some(Absolute)) => 0xAD, + Opcode::new(LDX, Some(Absolute)) => 0xAE, + Opcode::new(BCS, Some(Relative)) => 0xB0, + Opcode::new(LDA, Some(ZeroPageYIndirect)) => 0xB1, + Opcode::new(LDY, Some(ZeroPageX)) => 0xB4, + Opcode::new(LDA, Some(ZeroPageX)) => 0xB5, + Opcode::new(LDX, Some(ZeroPageY)) => 0xB6, + Opcode::new(CLV, None) => 0xB8, + Opcode::new(LDA, Some(AbsoluteY)) => 0xB9, + Opcode::new(TSX, None) => 0xBA, + Opcode::new(LDY, Some(AbsoluteX)) => 0xBC, + Opcode::new(LDA, Some(AbsoluteX)) => 0xBD, + Opcode::new(LDX, Some(AbsoluteY)) => 0xBE, + Opcode::new(CPY, Some(Immediate)) => 0xC0, + Opcode::new(CMP, Some(ZeroPageIndirectX)) => 0xC1, + Opcode::new(CPY, Some(ZeroPage)) => 0xC4, + Opcode::new(CMP, Some(ZeroPage)) => 0xC5, + Opcode::new(DEC, Some(ZeroPage)) => 0xC6, + Opcode::new(INY, None) => 0xC8, + Opcode::new(CMP, Some(Immediate)) => 0xC9, + Opcode::new(DEX, None) => 0xCA, + Opcode::new(CPY, Some(Absolute)) => 0xCC, + Opcode::new(CMP, Some(Absolute)) => 0xCD, + Opcode::new(DEC, Some(Absolute)) => 0xCE, + Opcode::new(BNE, Some(Relative)) => 0xD0, + Opcode::new(CMP, Some(ZeroPageYIndirect)) => 0xD1, + Opcode::new(CMP, Some(ZeroPageX)) => 0xD5, + Opcode::new(DEC, Some(ZeroPageX)) => 0xD6, + Opcode::new(CLD, None) => 0xD8, + Opcode::new(CMP, Some(AbsoluteY)) => 0xD9, + Opcode::new(CMP, Some(AbsoluteX)) => 0xDD, + Opcode::new(DEC, Some(AbsoluteX)) => 0xDE, + Opcode::new(CPX, Some(Immediate)) => 0xE0, + Opcode::new(SBC, Some(ZeroPageIndirectX)) => 0xE1, + Opcode::new(CPX, Some(ZeroPage)) => 0xE4, + Opcode::new(SBC, Some(ZeroPage)) => 0xE5, + Opcode::new(INC, Some(ZeroPage)) => 0xE6, + Opcode::new(INX, None) => 0xE8, + Opcode::new(SBC, Some(Immediate)) => 0xE9, + Opcode::new(NOP, None) => 0xEA, + Opcode::new(CPX, Some(Absolute)) => 0xEC, + Opcode::new(SBC, Some(Absolute)) => 0xED, + Opcode::new(INC, Some(Absolute)) => 0xEE, + Opcode::new(BEQ, Some(Relative)) => 0xF0, + Opcode::new(SBC, Some(ZeroPageYIndirect)) => 0xF1, + Opcode::new(SBC, Some(ZeroPageX)) => 0xF5, + Opcode::new(INC, Some(ZeroPageX)) => 0xF6, + Opcode::new(SED, None) => 0xF8, + Opcode::new(SBC, Some(AbsoluteY)) => 0xF9, + Opcode::new(SBC, Some(AbsoluteX)) => 0xFD, + Opcode::new(INC, Some(AbsoluteX)) => 0xFE, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..48e6f9c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,13 @@ +#![no_std] +#![warn(missing_docs, clippy::pedantic, clippy::perf)] +#![allow(clippy::type_complexity, clippy::missing_panics_doc)] +#![forbid(unsafe_code)] +#![doc = include_str!("../README.md")] + +pub mod state; +pub mod instructions; +pub mod emulation; + +pub use instructions::{Opcode, Instruction, AddressMode}; +pub use state::{State, Status}; +pub use emulation::{Emulator, ReadCallback, WriteCallback, DefaultReadCallback, DefaultWriteCallback, FunctionReadCallback, FunctionWriteCallback}; diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..272b4ac --- /dev/null +++ b/src/state.rs @@ -0,0 +1,105 @@ +//! Holds the structs pertaining to the state of the emulator. +#[cfg(feature = "arbitrary")] +extern crate std; + +#[cfg(feature = "serde")] +use { + serde::{Serialize, Deserialize}, + serde_with::{As, Bytes} +}; + +#[derive(Copy, Clone, PartialEq, Eq, core::hash::Hash)] +#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// A full representation of the state of the emulator. +/// +/// # Note +/// This struct is quite large. It's not advised to leave this on the stack if you have an allocator. +#[repr(C)] +pub struct State { + /// The current program counter of the emulator. + pub program_counter: u16, + /// The current state of the accumulator in the emulator. + pub accumulator: u8, + /// The current state of the X register in the emulator. + pub x_register: u8, + /// The current state of the Y register in the emulator. + pub y_register: u8, + /// The current stack pointer of the emulator. + pub stack_pointer: u8, + /// The emulator's status flags. + pub status: Status, + #[doc(hidden)] + #[cfg_attr(feature = "serde", serde(skip))] + _padding: u8, + /// The emulator's memory, left as one contiguous array of bytes. + /// + /// This is stored inside the struct to prevent needing an allocator, + /// but grows the struct's size considerably. + /// Consider storing this struct on the heap if this is undesirable and you have one. + #[cfg_attr(feature = "serde", serde(with = "As::"))] + pub memory: [u8; 256 * 256] +} + +#[allow(clippy::missing_fields_in_debug)] +impl core::fmt::Debug for State { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + fmt.debug_struct("State") + .field("accumulator", &self.accumulator) + .field("x_register", &self.x_register) + .field("y_register", &self.y_register) + .field("program_counter", &self.program_counter) + .field("stack_pointer", &self.stack_pointer) + .field("status", &self.status) + .finish() + } +} + +impl Default for State { + fn default() -> Self { + State { + accumulator: 0, + x_register: 0, + y_register: 0, + program_counter: 0x200, + stack_pointer: 0xFF, + status: Status::default(), + memory: [0; 256 * 256], + _padding: 0 + } + } +} + +mod flags { + #![allow(missing_docs)] // shut up + #[cfg(feature = "arbitrary")] + extern crate std; + + bitflags::bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, core::hash::Hash)] + #[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + #[repr(transparent)] + /// A [bitflags]-based representation of the status flags of a 6502 microprocessor. + pub struct Status: u8 { + const NEGATIVE = 0b1000_0000; + const OVERFLOW = 0b0100_0000; + const UNUSED = 0b0010_0000; + const BREAK = 0b0001_0000; + const DECIMAL = 0b0000_1000; + const INTERRUPT_DISABLE = 0b0000_0100; + const ZERO = 0b0000_0010; + const CARRY = 0b0000_0001; + } + } + + impl Default for Status { + fn default() -> Self { + Status::UNUSED | Status::INTERRUPT_DISABLE | Status::ZERO + } + } +} + +pub use flags::Status; diff --git a/tests/6502_functional_test.bin b/tests/6502_functional_test.bin new file mode 100644 index 0000000000000000000000000000000000000000..c9a35e1d6bd2e7d85844da2abf7034d5ed820e6e GIT binary patch literal 65536 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 E3ya{&@Bjb+ literal 0 HcmV?d00001 diff --git a/tests/AllSuiteA.bin b/tests/AllSuiteA.bin new file mode 100644 index 0000000000000000000000000000000000000000..18f6f0e1d0345db59084c98c3933f527d1302d0f GIT binary patch literal 49152 zcmeIy{clrM7zglkPP?slY|swS`Ga~p#>*DLZi5MlFKxFe=+I7qsEMW{&QL_`Hs?#C zgCPe9@n`{OtjUtD*}d^u+{DO03^OwkBE*vV14c!RB!WPcbP$dHAobj?{uAcAbF*{u zoaelK?zttVyn@y%CTLV~L3t$vZB;HoVdWOoq)G%0sZv4PRGA=4c?8|A3_+(=xu7~V z%gL({v_(}qt=W32bbYE<4iJ}})Nc(auR;A|USn_r^-pZr8QmA9{*R+W`7-LC%$Fsd z`e?pvpqcuIn|DR*=t$(PxkkEXe6I24JR_C-aGvqb(sy4@B@Zo4*BsXMv6LrW^ZBr6 zh>5X!kzpd|YM*MO8--KUJ!Y(2A ztmF#seBb&i9eqK0Io+*%oEEDpZY7s`d8Lo9t>U_GuQEA3p!}TPRyDfs9xuPu$DgX= zDO30MssN{Y)xc?5HR|3~UVe*@KT#MNmjR8?K;5!Y*G<$Tm+N{39anKquc>ZM!>UJ* z@teFRRI4Y8%AigNQKt;+x`j5%R$aHzT`D4If$DHx#G}GWrg9BIU$~=Hl0_LVavtLP z^o~M4&1B-DE>^-$3?}|eOeE}mLkWI$Kc7tS@%>!4ZzKxs1AHjXe~Ro?1|9XDKq6^o{x)n%f>o|4(-zUID{`mC&- z)%nYg6rZg}3ez0n>Kwn9@&3Xyc8+IU&Zb<&Otk zgN9|4()vp0W4c@(mq9z&$Nu15(F>i<4O#2lkOj}1{C7W>IXg$ss9H`N)k02-UT6Ft z&gJyTG+j_jI9*nCoR+`J_}@Bu&AvvajO!QOO|`N|*S(#g^Y_W?BiHE%xt*B)lzx;u zbo~a6c5k2FAp=$*Q|j!ow0IDvFIL{Uh)FfX>8wg}x>Mh|M)?Q@^u3GAkEmJ(PFCkc z9W(gxKvvgjmI?5j+d0OW#o1D0rwrMly0hmPcd}%7BwWY7XFMy6IkLrWF>1dE=7h0L zwvM!}ne$Ck)@`-Q`PTeQnX{3y;zlfKIu=h#f7<%l%84}p#nk`BZ>F9#?b;O>2~6p5 z%KfDdr%Jwd!l}}o4OT;~nH5xahp87NPpv;IWY`L4JkI2v;^gb}kv@^bv|F~MhrJ@* zc!+u3RALVY#AR1NP-sm+Huf?7skr32!>q4Ew=VPr<0VPXy4J@9u~;tQmeH=3g#MN5 zT6fSL+igb=>iA%nU+$!q7gP0b zMvSlYR4<9K)xjnF=SP;=d_g`OC9OnvZ|y z+0 Result<(), usize> { + static TEST_BIN: &[u8] = include_bytes!("AllSuiteA.bin"); + + let mut emulator = Emulator::default() + .with_program_counter(0x4000) + .with_rom_from(TEST_BIN, 0x4000); + let mut last_pc; + loop { + last_pc = emulator.state.program_counter; + let needs_interrupt = emulator.step().unwrap(); + if emulator.state.program_counter == last_pc { + let trap_number = emulator.state.memory[0x0210]; + if trap_number != 0xFF { + eprintln!("!!! ENTERED TRAP !!!"); + eprintln!("{:02x?}", emulator.state); + eprintln!("Failed test: {trap_number}", ); + return Err(0); + } + break; + } + if needs_interrupt { + eprintln!("!!! ENTERED INTERRUPT !!!"); + eprintln!("{:02x?}", emulator.state); + return Err(0); + } + } + Ok(()) +} + +#[test] +fn functional_test() -> Result<(), usize> { + static TEST_BIN: &[u8; 65536] = include_bytes!("6502_functional_test.bin"); + + let mut emulator = Emulator::default() + .with_program_counter(0x400) + .with_rom(*TEST_BIN); + let mut last_pc; + loop { + last_pc = emulator.state.program_counter; + let needs_interrupt = emulator.step().unwrap(); + if emulator.state.program_counter == last_pc { + if last_pc == 0x3469 { + // https://github.com/Klaus2m5/6502_65C02_functional_tests/blob/master/bin_files/6502_functional_test.lst#L13377C1-L13377C5 + // Success! + return Ok(()); + } + eprintln!("!!! ENTERED TRAP !!!"); + eprintln!("{:02x?}", emulator.state); + eprintln!("--------------------------[ZERO PAGE]---------------------------"); + for i in 0..16 { + eprintln!("{:02x?}", &emulator.state.memory[i * 16..(i + 1) * 16]) + } + eprintln!("----------------------------[STACK]-----------------------------"); + for i in 0..16 { + eprintln!("{:02x?}", &emulator.state.memory[0x100 + (i * 16)..0x100 + ((i + 1) * 16)]) + } + return Err(0); + } + if needs_interrupt { + let vector = u16::from_le_bytes([ + emulator.read(0xFFFE), + emulator.read(0xFFFF) + ]); + eprintln!("[MASKABLE INTERRUPT, GOING TO IRQ VECTOR @ {vector:02X}]"); + // Go to interrupt vector + emulator.state.program_counter = vector; + } + } +} \ No newline at end of file