Initial commit

This commit is contained in:
baltdev 2024-04-01 23:22:35 -05:00
commit 55f524d73e
15 changed files with 1732 additions and 0 deletions

12
.gitignore vendored Normal file
View 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 generated vendored Normal file
View 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 generated Normal file
View 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 generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

Binary file not shown.

BIN
tests/AllSuiteA.bin Normal file

Binary file not shown.

74
tests/test_roms.rs Normal file
View 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;
}
}
}