mirror of
https://github.com/balt-dev/r6502.git
synced 2024-11-23 15:31:07 +00:00
Initial commit
This commit is contained in:
commit
55f524d73e
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -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
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -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
|
12
.idea/6502.iml
Normal file
12
.idea/6502.iml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/6502.iml" filepath="$PROJECT_DIR$/.idea/6502.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
29
Cargo.toml
Normal file
29
Cargo.toml
Normal file
@ -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"]
|
201
LICENSE-APACHE
Normal file
201
LICENSE-APACHE
Normal file
@ -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.
|
21
LICENSE-MIT
Normal file
21
LICENSE-MIT
Normal file
@ -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.
|
71
README.md
Normal file
71
README.md
Normal file
@ -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.<br/>Enabled by default. Disable if you're writing a NES emulator.<br/>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.
|
643
src/emulation.rs
Normal file
643
src/emulation.rs
Normal file
@ -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<F>(pub F);
|
||||
|
||||
impl<F> ReadCallback for FunctionReadCallback<F> 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<F>(pub F);
|
||||
|
||||
impl<F> WriteCallback for FunctionWriteCallback<F> 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<R: ReadCallback, W: WriteCallback> {
|
||||
/// 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<R: ReadCallback, W: WriteCallback> Emulator<R, W> {
|
||||
/// 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<R2: ReadCallback>(self, callback: R2) -> Emulator<R2, W> {
|
||||
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<W2: WriteCallback>(self, callback: W2) -> Emulator<R, W2> {
|
||||
Emulator {
|
||||
state: self.state,
|
||||
read_callback: self.read_callback,
|
||||
write_callback: callback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Emulator<DefaultReadCallback, DefaultWriteCallback> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: State::default(),
|
||||
read_callback: DefaultReadCallback,
|
||||
write_callback: DefaultWriteCallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[allow(clippy::missing_fields_in_debug)]
|
||||
impl<R: ReadCallback + core::fmt::Debug, W: WriteCallback + core::fmt::Debug> core::fmt::Debug for Emulator<R, W> {
|
||||
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<R: ReadCallback, W: WriteCallback> Emulator<R, W> {
|
||||
/// Gets an address from an address mode.
|
||||
fn addr(&mut self, mode: AddressMode) -> Option<u16> {
|
||||
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<bool, u16> {
|
||||
// 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<bool> {
|
||||
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)
|
||||
}
|
535
src/instructions.rs
Normal file
535
src/instructions.rs
Normal file
@ -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<AddressMode>,
|
||||
}
|
||||
|
||||
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<AddressMode>) -> 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<Option<Opcode>, 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<bool, usize> {
|
||||
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,
|
||||
}
|
13
src/lib.rs
Normal file
13
src/lib.rs
Normal file
@ -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};
|
105
src/state.rs
Normal file
105
src/state.rs
Normal file
@ -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::<Bytes>"))]
|
||||
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;
|
BIN
tests/6502_functional_test.bin
Normal file
BIN
tests/6502_functional_test.bin
Normal file
Binary file not shown.
BIN
tests/AllSuiteA.bin
Normal file
BIN
tests/AllSuiteA.bin
Normal file
Binary file not shown.
74
tests/test_roms.rs
Normal file
74
tests/test_roms.rs
Normal file
@ -0,0 +1,74 @@
|
||||
extern crate std;
|
||||
|
||||
use r6502::*;
|
||||
|
||||
#[test]
|
||||
fn all_suite_a() -> 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user