diff options
| author | Sophie Forrest <57433227+sophieforrest@users.noreply.github.com> | 2024-01-13 19:17:57 +1300 |
|---|---|---|
| committer | Sophie Forrest <57433227+sophieforrest@users.noreply.github.com> | 2024-01-13 19:17:57 +1300 |
| commit | 13becb31e8b669dd6254c68b3aae110e7711d9db (patch) | |
| tree | f4320bc6c0e86064291ac29f4dfaddb8fbb3f376 | |
chore: init project
Diffstat (limited to '')
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | .rust-toolchain.toml | 2 | ||||
| -rw-r--r-- | .rustfmt.toml | 13 | ||||
| -rw-r--r-- | .taplo.toml | 10 | ||||
| -rw-r--r-- | Cargo.lock | 16 | ||||
| -rw-r--r-- | Cargo.toml | 75 | ||||
| -rw-r--r-- | clippy.toml | 1 | ||||
| -rw-r--r-- | src/cpu.rs | 216 | ||||
| -rw-r--r-- | src/instruction.rs | 51 | ||||
| -rw-r--r-- | src/main.rs | 33 | ||||
| -rw-r--r-- | src/memory.rs | 26 |
11 files changed, 444 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.rust-toolchain.toml b/.rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/.rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..ebf394c --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,13 @@ +edition = "2021" + +format_code_in_doc_comments = true +format_strings = true +group_imports = "StdExternalCrate" +hard_tabs = true +hex_literal_case = "Upper" +imports_granulatiry = "Crate" +reorder_impl_items = true +reorder_modules = true +use_field_init_shorthand = true +use_try_shorthand = true +wrap_comments = true diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..88958ed --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,10 @@ +[formatting] +indent_string = " " +reorder_keys = false + +[[rule]] +include = ["**/Cargo.toml"] +keys = ["dependencies"] + +[rule.formatting] +reorder_keys = true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f7ac645 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "emulator_6502" +version = "0.1.0" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..874b019 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "mos6502" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitflags = "2.4.1" + +[lints.rust] +missing_copy_implementations = "deny" +missing_debug_implementations = "deny" +missing_docs = "deny" +single_use_lifetimes = "deny" +unsafe_code = "deny" +unused = "deny" + +[lints.clippy] +complexity = "deny" +nursery = "deny" +pedantic = "deny" +perf = "deny" +suspicious = "deny" +alloc_instead_of_core = "deny" +as_undercore = "deny" +clone_on_ref_ptr = "deny" +create_dir = "deny" +dbg_macro = "deny" +default_numeric_fallback = "deny" +default_union_representation = "deny" +deref_by_slicing = "deny" +empty_structs_with_brackets = "deny" +exit = "deny" +filetype_is_file = "deny" +fn_to_numeric_cast = "deny" +format_push_string = "deny" +get_unwrap = "deny" +if_then_some_else_none = "deny" +implicit_return = "allow" +indexing_slicing = "deny" +large_include_file = "deny" +let_underscore_must_use = "deny" +lossy_float_literal = "deny" +map_err_ignore = "deny" +mem_forget = "deny" +missing_docs_in_private_items = "deny" +missing_trait_methods = "deny" +mod_module_files = "deny" +multiple_inrerent_impl = "deny" +mutex_atomic = "deny" +needless_return = "deny" +non_ascii_literal = "deny" +panic_in_result_fn = "deny" +pattern_type_mismatch = "deny" +rc_buffer = "deny" +rc_mutex = "deny" +rest_pat_in_fully_bound_structs = "deny" +same_name_method = "deny" +separated_literal_suffix = "deny" +str_to_string = "deny" +string_add = "deny" +string_slice = "deny" +string_to_string = "deny" +tabs_in_doc_comments = "allow" +try_err = "deny" +undocumented_unsafe_blocks = "deny" +unnecessary_self_imports = "deny" +unneeded_field_pattern = "deny" +unwrap_in_resuslt = "deny" +unwrap_used = "deny" +use_debug = "deny" +verbose_file_reads = "deny" +wildcard_dependencies = "deny" +wildcard_enum_match_arm = "deny" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..cda8d17 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +avoid-breaking-exported-api = false diff --git a/src/cpu.rs b/src/cpu.rs new file mode 100644 index 0000000..b009160 --- /dev/null +++ b/src/cpu.rs @@ -0,0 +1,216 @@ +//! Implementation of the MOS 6502 CPU. + +use bitflags::bitflags; + +use crate::{ + instruction::{get_operation, AddressingMode, Instruction}, + memory::Memory, +}; + +/// Implementation of the Central Processing Unit of the 6502. +pub struct Cpu { + /// The 8 bit accumulator is used all arithmetic and logical operations + /// (with the exception of increments and decrements). The contents of the + /// accumulator can be stored and retrieved either from memory or the stack. + /// + /// Most complex operations will need to use the accumulator for arithmetic + /// and efficient optimisation of its use is a key feature of time critical + /// routines. + accumulator: u8, + + /// The 8 bit index register is most commonly used to hold counters or + /// offsets for accessing memory. The value of the X register can be loaded + /// and saved in memory, compared with values held in memory or incremented + /// and decremented. + /// + /// The X register has one special function. It can be used to get a copy of + /// the stack pointer or change its value. + index_register_x: u8, + + /// The Y register is similar to the X register in that it is available for + /// holding counter or offsets memory access and supports the same set of + /// memory load, save and compare operations as wells as increments and + /// decrements. It has no special functions. + index_register_y: u8, + + /// Processor flags. See documentation for [`ProcessorStatus`]. + processor_status: ProcessorStatus, + + /// The program counter is a 16 bit register which points to the next + /// instruction to be executed. The value of program counter is modified + /// automatically as instructions are executed. + /// + /// The value of the program counter can be modified by executing a jump, a + /// relative branch or a subroutine call to another memory address or by + /// returning from a subroutine or interrupt. + program_counter: u16, + + /// The processor supports a 256 byte stack located between $0100 and $01FF. + /// The stack pointer is an 8 bit register and holds the low 8 bits of the + /// next free location on the stack. The location of the stack is fixed and + /// cannot be moved. + /// + /// Pushing bytes to the stack causes the stack pointer to + /// be decremented. Conversely pulling bytes causes it to be incremented. + /// + /// The CPU does not detect if the stack is overflowed by excessive pushing + /// or pulling operations and will most likely result in the program + /// crashing. + stack_pointer: u8, +} + +impl Cpu { + /// Creates a new instance of the [`Cpu`]. + pub const fn new() -> Self { + // TODO: Ensure these are the correct values for init + + Self { + accumulator: 0, + index_register_x: 0, + index_register_y: 0, + program_counter: 0xFFFC, + stack_pointer: 0x0000, + processor_status: ProcessorStatus::empty(), + } + } + + /// Fetches the next byte, pointed to by the program counter. + fn fetch_byte(&mut self, cycles: &mut u32, memory: &Memory) -> u8 { + let data = *memory + .data + .get(usize::from(self.program_counter)) + .expect("program_counter indexed outside of memory"); + + self.program_counter = self.program_counter.wrapping_add(1); + *cycles -= 1; + + data + } + + /// Sets the LDA status after executing an LDA operation. + fn lda_set_status(&mut self) { + self.processor_status + .set(ProcessorStatus::ZERO, self.accumulator == 0); + self.processor_status.set( + ProcessorStatus::NEGATIVE, + (self.accumulator & 0b1000_0000) > 0, + ); + } + + /// Reads the next byte, pointed to by the address. + fn read_byte(cycles: &mut u32, address: u8, memory: &Memory) -> u8 { + let data = *memory + .data + .get(usize::from(address)) + .expect("address indexed outside of memory"); + + *cycles -= 1; + + data + } + + /// Executes instructions on the [`Cpu`] from memory. Runs the specified + /// number of cycles. + pub fn execute(&mut self, cycles: u32, memory: &Memory) { + // Shadow cycles with a mutable copy + let mut cycles = cycles; + + while cycles > 0 { + let opcode: u8 = self.fetch_byte(&mut cycles, memory); + + let operation = get_operation(opcode); + + if let Some(operation) = operation { + match (operation.instruction, operation.addressing_mode) { + (Instruction::Lda, AddressingMode::Immediate) => { + let value = self.fetch_byte(&mut cycles, memory); + + self.accumulator = value; + self.lda_set_status(); + } + (Instruction::Lda, AddressingMode::ZeroPage) => { + let zero_page_address = self.fetch_byte(&mut cycles, memory); + + self.accumulator = Self::read_byte(&mut cycles, zero_page_address, memory); + self.lda_set_status(); + } + (Instruction::Lda, AddressingMode::ZeroPageX) => { + let mut zero_page_address = self.fetch_byte(&mut cycles, memory); + + zero_page_address += self.index_register_x; + cycles -= 1; + + self.accumulator = Self::read_byte(&mut cycles, zero_page_address, memory); + self.lda_set_status(); + } + } + } + } + } + + /// Resets the [`Cpu`]. This will put the Cpu back into the state it was at + /// on boot. + pub fn reset(&mut self, memory: &mut Memory) { + // TODO: Incomplete. Use correct values from later on in tutorial + + self.program_counter = 0xFFFC; + self.stack_pointer = 0x0000; + + // Set all flags to 0 + self.processor_status = ProcessorStatus::empty(); + + self.accumulator = 0; + self.index_register_x = 0; + self.index_register_y = 0; + + // Zero out memory + memory.reset(); + } +} + +bitflags! { + /// As instructions are executed a set of processor flags are set or clear to + /// record the results of the operation. This flags and some additional control + /// flags are held in a special status register. Each flag has a single bit + /// within the register. + pub struct ProcessorStatus: u8 { + /// The negative flag is set if the result of the last operation had bit 7 + /// set to a one. + const NEGATIVE = 0b1000_0000; + + /// The overflow flag is set during arithmetic operations if the result has + /// yielded an invalid 2's complement result (e.g. adding to positive numbers + /// and ending up with a negative result: 64 + 64 => -128). It is determined by + /// looking at the carry between bits 6 and 7 and between bit 7 and the carry + /// flag. + const OVERFLOW = 0b0100_0000; + + // No instruction for 0b0010_0000 + + /// The break command bit is set when a BRK instruction has been executed and an + /// interrupt has been generated to process it. + const BREAK_COMMAND = 0b0001_0000; + + /// While the decimal mode flag is set the processor will obey the rules of + /// Binary Coded Decimal (BCD) arithmetic during addition and subtraction. The + /// flag can be explicitly set using 'Set Decimal Flag' (SED) and cleared with + /// 'Clear Decimal Flag' (CLD). + const DECIMAL_MODE = 0b0000_1000; + + /// The interrupt disable flag is set if the program has executed a 'Set + /// Interrupt Disable' (SEI) instruction. While this flag is set the processor + /// will not respond to interrupts from devices until it is cleared by a 'Clear + /// Interrupt Disable' (CLI) instruction. + const INTERRUPT_DISABLE = 0b0000_0100; + + /// The zero flag is set if the result of the last operation as was zero. + const ZERO = 0b0000_0010; + + /// The carry flag is set if the last operation caused an overflow from bit 7 of + /// the result or an underflow from bit 0. This condition is set during + /// arithmetic, comparison and during logical shifts. It can be explicitly set + /// using the 'Set Carry Flag' (SEC) instruction and cleared with 'Clear Carry + /// Flag' (CLC). + const CARRY = 0b0000_0001; + } +} diff --git a/src/instruction.rs b/src/instruction.rs new file mode 100644 index 0000000..2744048 --- /dev/null +++ b/src/instruction.rs @@ -0,0 +1,51 @@ +//! Implementation of instructions for MOS 6502. + +/// List of all instructions for MOS 6502. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Instruction { + /// Load accumulator. + Lda, +} + +/// Addressing modes for MOS 6502. +#[derive(Clone, Copy, Debug)] +pub enum AddressingMode { + /// Immediate mode + Immediate, + + /// Zero Page mode + ZeroPage, + + /// ZeroPage.X mode + ZeroPageX, +} + +/// Operation retrieved from decoding an opcode. +pub struct Operation { + /// [`AddressingMode`] + pub addressing_mode: AddressingMode, + + /// [`Instruction`] + pub instruction: Instruction, +} + +impl Operation { + /// Creates a new operation from the `instruction` and `addressing_mode`. + pub const fn new(instruction: Instruction, addressing_mode: AddressingMode) -> Self { + Self { + addressing_mode, + instruction, + } + } +} + +/// Gets the operation from the passed in opcode. Returns [`None`] if no +/// operation exists. +pub const fn get_operation(opcode: u8) -> Option<Operation> { + match opcode { + 0xA5 => Some(Operation::new(Instruction::Lda, AddressingMode::ZeroPage)), + 0xA9 => Some(Operation::new(Instruction::Lda, AddressingMode::Immediate)), + 0xB5 => Some(Operation::new(Instruction::Lda, AddressingMode::ZeroPageX)), + _ => None, + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..003d198 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +//! 6502 Emulator written in Rust. Based off of information from Dave's playlist +//! <https://www.youtube.com/playlist?list=PLLwK93hM93Z13TRzPx9JqTIn33feefl37> using the now defunct +//! obelisk 6502 documentation. +//! +//! Many comments are sourced from the Obelisk 6502 documentation <https://web.archive.org/web/20210501031403/http://www.obelisk.me.uk/index.html> + +mod cpu; +mod instruction; +mod memory; + +use cpu::Cpu; +use memory::Memory; + +fn main() { + let mut cpu = Cpu::new(); + let mut memory = Memory::new(); + + cpu.reset(&mut memory); + + // little program + if let Some(opcode) = memory.data.get_mut(0xFFFC) { + *opcode = 0xA5; + } + if let Some(opcode) = memory.data.get_mut(0xFFFD) { + *opcode = 0x42; + } + if let Some(opcode) = memory.data.get_mut(0x0042) { + *opcode = 0x84; + } + // end program + + cpu.execute(3, &memory); +} diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000..d251231 --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,26 @@ +//! Implementation of memory for the MOS 6502. + +/// Maximum amount of memory that exists within the emulator. +pub const MAXIMUM_MEMORY: usize = 1024 * 64; + +/// Implementation of memory for the 6502. +pub struct Memory { + /// Data for the memory. + pub data: [u8; MAXIMUM_MEMORY], +} + +impl Memory { + /// Creates a new instance of the memory. Returns blank memory with all + /// zeroes. + pub const fn new() -> Self { + Self { + data: [0; MAXIMUM_MEMORY], + } + } + + /// Resets all data in the memory to zero. This simply allocates a new array + /// and sets the memory to the new, zeroed array. + pub fn reset(&mut self) { + self.data = [0; MAXIMUM_MEMORY]; + } +} |