Compare commits

..

2 Commits

4 changed files with 256 additions and 52 deletions

Binary file not shown.

View File

@ -1,35 +1,55 @@
// chip8.rs // chip8.rs
// Implementation of chip8 machine, registers, and memory makeup // Implementation of chip8 machine, registers, and memory makeup
use crate::util; use std::fmt::Debug;
use std::process::exit;
use std::io::{self, Read};
use crate::{util, EmulationOpts};
// Memory map constants // Memory map constants
pub const PRGRM_START: usize = 512; pub const PRGRM_START : usize = 512;
pub const MAX_MEM_SIZE: usize = 4096; pub const MAX_MEM_SIZE : usize = 4096;
pub const MAX_PRG_SIZE: usize = 3584; pub const MAX_PRG_SIZE : usize = 3584;
const MAX_NUM_REGS : usize = 16;
const MAX_STACK_SIZE : usize = 16;
// Instruction constants // Instruction constants
const INSTR_NOOP: u16 = 0x0000; const INSTR_NOOP : u16 = 0x0000;
const INSTR_EXIT: u16 = 0x00FD; const INSTR_EXIT : u16 = 0x00FD;
const INSTR_CLS: u16 = 0x00E0; const INSTR_CLS : u16 = 0x00E0;
const INSTR_RET: u16 = 0x00EE; const INSTR_RET : u16 = 0x00EE;
// Commonn Instruction Masks // Commonn Instruction Masks
const INSTR_MASK_SYS: u16 = 0x0000; const INSTR_MASK_JP : u16 = 0x1000;
const INSTR_MASK_JP: u16 = 0x1000; const INSTR_MASK_CALL : u16 = 0x2000;
const INSTR_MASK_CALL: u16 = 0x2000; const INSTR_MASK_SE_VX : u16 = 0x3000;
const INSTR_MASK_SNE : u16 = 0x4000;
const INSTR_MASK_SE_VX_VY : u16 = 0x5000;
const INSTR_MASK_LD_VX : u16 = 0x6000;
const INSTR_MASK_ADD_VX : u16 = 0x7000;
const INSTR_MASK_LD_VX_VY : u16 = 0x8000;
const INSTR_MASK_OR_VX_VY : u16 = 0x8001;
const INSTR_MASK_AND_VX_VY : u16 = 0x8002;
const INSTR_MASK_XOR_VX_VY : u16 = 0x8003;
const INSTR_MASK_ADD_VX_VY : u16 = 0x8004;
const INSTR_MASK_SUB_VX_VY : u16 = 0x8005;
const INSTR_MASK_SHR_VX_VY : u16 = 0x8006;
const INSTR_MASK_SUBN_VX_VY : u16 = 0x8007;
const INSTR_MASK_SHL_VX_VY : u16 = 0x800E;
const INSTR_MASK_SNE_VX_VY : u16 = 0x9000;
const INSTR_MASK_LD_I : u16 = 0xA000;
const INSTR_MASK_JP_V0 : u16 = 0xB000;
const ADDR_MASK: u16 = 0x0FFF; const ADDR_MASK: u16 = 0x0FFF;
#[derive(Debug)]
struct EmulationState { struct EmulationState {
// Miscellanious registers // Miscellanious registers
program_counter: u16, program_counter: u16,
stack_ptr: u8, stack_ptr: usize,
// General purpose registers // General purpose registers
registers: [u8; 16], registers: [u8; MAX_NUM_REGS],
// Memory Map: // Memory Map:
// +---------------+= 0xFFF (4095) End of Chip-8 RAM // +---------------+= 0xFFF (4095) End of Chip-8 RAM
@ -54,38 +74,67 @@ struct EmulationState {
// | Reserved for | // | Reserved for |
// | interpreter | // | interpreter |
// +---------------+= 0x000 (0) Start of Chip-8 RAM // +---------------+= 0x000 (0) Start of Chip-8 RAM
memory: [u8; 4096] memory: [u8; MAX_MEM_SIZE],
options: EmulationOpts,
// Program stack
stack: [u16; MAX_STACK_SIZE]
} }
pub fn start_emulation(data: [u8; MAX_PRG_SIZE]) { /// # start_emulation
let mut state: EmulationState = EmulationState{ ///
/// Begin emulating the loaded CHIP8 program.
///
/// * `data` - u8 array of program data.
///
pub fn start_emulation(data: [u8; MAX_PRG_SIZE], options_in: EmulationOpts)
{
let mut state: EmulationState = EmulationState {
program_counter: 0, program_counter: 0,
stack_ptr: 0, stack_ptr: 0,
registers: [0; 16], registers: [0; 16],
memory: [0; MAX_MEM_SIZE] memory: [0; MAX_MEM_SIZE],
stack: [0; 16],
options: options_in
}; };
load_program(data, &mut state); load_program(data, &mut state);
exec_program(&mut state); exec_program(&mut state);
} }
// Load the program into virtual memory /// # load_program
fn load_program(data: [u8; MAX_PRG_SIZE], state: &mut EmulationState) { ///
/// Load the program into virtual memory.
///
/// * `data` - u8 array of program data.
/// * `state` - Global emulation state.
///
fn load_program(data: [u8; MAX_PRG_SIZE], state: &mut EmulationState)
{
println!("Copying program data to memory"); println!("Copying program data to memory");
for i in 0..MAX_PRG_SIZE { for i in 0..MAX_PRG_SIZE
{
state.memory[i+PRGRM_START] = data[i]; state.memory[i+PRGRM_START] = data[i];
} }
} }
// Execute the program starting at the userspace address /// # exec_program
fn exec_program(state: &mut EmulationState) { ///
/// Execute the program starting at the userspace address.
///
/// * `state` - Global emulation state.
///
fn exec_program(state: &mut EmulationState)
{
println!("Starting emulation"); println!("Starting emulation");
let mut head_byte: u8; let mut head_byte: u8;
let mut tail_byte: u8; let mut tail_byte: u8;
let mut instruction: u16; let mut instruction: u16;
for _i in PRGRM_START+usize::from(state.program_counter)..MAX_MEM_SIZE { for _i in PRGRM_START+usize::from(state.program_counter)..MAX_MEM_SIZE
{
// Grab the first byte of instruction // Grab the first byte of instruction
head_byte = state.memory[PRGRM_START + usize::from(state.program_counter)]; head_byte = state.memory[PRGRM_START + usize::from(state.program_counter)];
state.program_counter += 1; state.program_counter += 1;
@ -96,45 +145,151 @@ fn exec_program(state: &mut EmulationState) {
instruction = util::u8_tuple_to_u16((head_byte, tail_byte)); instruction = util::u8_tuple_to_u16((head_byte, tail_byte));
if instruction == INSTR_EXIT
println!("0x{:02X?}{:02X?}", head_byte, tail_byte); {
if instruction == INSTR_EXIT {
println!("Read exit instruction, stopping emulation"); println!("Read exit instruction, stopping emulation");
return; return;
} }
if instruction == INSTR_NOOP { if instruction == INSTR_NOOP
{
println!("Read no-op instruction, stopping emulation"); println!("Read no-op instruction, stopping emulation");
return; return;
} }
parse_instruction(state, instruction); parse_instruction(state, instruction);
if state.options.step_mode
{
pause();
}
} }
} }
/// # parse_instruction
///
/// Parse binary instructions passed from program execution.
///
/// * `state` - Global emulation state.
/// * `instr` - Incoming instruction.
///
fn parse_instruction(state: &mut EmulationState, instr: u16) { fn parse_instruction(state: &mut EmulationState, instr: u16) {
// CLS; Clear the display
if (instr == INSTR_CLS) { if instr == INSTR_CLS
println!("Clear screen"); {
// Clear the display
debug_log_instr(state, instr, "CLS".to_string());
// TODO: Implement this
return;
} }
if (instr == INSTR_RET) { // RET; Return from subroutine
println!("Return"); // Sets the program counter to the address at the top of the stack, then pops the value from
// the stack
if instr == INSTR_RET
{
debug_log_instr(state, instr, "RET".to_string());
// Halt emulation if stack_ptr is 0, since decrementing on an empty stack
// is undefined behavior
if state.stack_ptr == 0
{
halt(state, instr, format!("Cannot decrement stack pointer below stack limit {}", MAX_STACK_SIZE));
}
state.stack_ptr -= 1;
state.program_counter = state.stack[state.stack_ptr];
state.stack[state.stack_ptr] = 0x0000;
return;
} }
if ((instr & INSTR_MASK_SYS) > 0) { // JP nnn; Jump to location nnn.
let addr: u16 = (instr & ADDR_MASK); // Sets the program counter to nnn.
println!("Got SYS instruction {:04X?} with address {:04X?}", instr, addr); if (instr & INSTR_MASK_JP) > 0
{
let addr: u16 = instr & ADDR_MASK;
debug_log_instr(state, instr, format!("JP {:04X?}", addr));
state.program_counter = addr;
return;
} }
if ((instr & INSTR_MASK_JP) > 0) { // CALL nnn; Call subroutine at nnn
let addr: u16 = (instr & ADDR_MASK); // Puts the current PC in the stack, then increments the stack pointer. The program counter
println!("Got JP instruction {:04X?} with address {:04X?}", instr, addr); // is then set to nnn.
if (instr & INSTR_MASK_CALL) > 0
{
let addr: u16 = instr & ADDR_MASK;
debug_log_instr(state, instr, format!("CALL {:04X?}", addr));
// Halt emulation if stack_ptr is increments above stack limit, as this will cause a
// stack overflow
if (state.stack_ptr + 1) > MAX_STACK_SIZE
{
halt(state, instr, format!("Cannot increment stack pointer above stack limit {}", MAX_STACK_SIZE));
}
state.stack[state.stack_ptr] = state.program_counter;
state.stack_ptr += 1;
state.program_counter = addr;
return;
} }
if ((instr & INSTR_MASK_CALL) > 0) { // SE Vx, byte; Skip next instruction if Vx = kk.
let addr: u16 = (instr & ADDR_MASK); // Compares register Vx to kk, and if they are equal, increments the
println!("Got CALL instruction {:04X?} with address {:04X?}", instr, addr); // program counter by 2.
if (instr & INSTR_MASK_SE_VX) > 0
{
let bytes:[u8; 2] = instr.to_be_bytes();
let register_num: u8 = bytes[0] & 0x0F;
let value: u8 = bytes[1] & 0xFF;
debug_log_instr(state, instr, format!("SE V{:01X?} {:02X?}", register_num, value));
if value == state.registers[register_num as usize]
{
state.program_counter += 2;
}
return;
} }
} }
/// # debug_log_instr
///
/// Debug log the program counter, instruction, and a brief string message in one line.
///
/// * `state` - Global emulation state.
/// * `instr` - Incoming instruction.
/// * `msg` - Message string to append
///
fn debug_log_instr(state: &mut EmulationState, instr: u16, msg: String)
{
if state.options.debug_mode
{
println!("PC: {:04?}; INSTR {:04X?}; {}", state.program_counter, instr, msg);
}
}
/// # halt
///
/// Halt the emulator and throw an error
///
/// * `msg` - Message string describing the error
///
fn halt(state: &mut EmulationState, instr: u16, msg: String)
{
println!("ERROR: EMULATION HALTED\n");
println!("\t{}\n", msg);
println!("\tPC: {:04?}\t\tINSTR {:04X?}", state.program_counter, instr);
exit(0);
}
/// # Pause
///
/// Pause the emulation and poll for keyboard input;
/// Used for stepping over program data 1 instruction at a time
///
fn pause() {
// Read a single byte and discard
let mut stdin = io::stdin();
let _ = stdin.read(&mut [0u8]).unwrap();
}

View File

@ -1,3 +1,4 @@
use std::option;
use std::path; use std::path;
use std::env; use std::env;
use std::fs; use std::fs;
@ -7,15 +8,27 @@ use std::process::exit;
mod util; mod util;
mod chip8; mod chip8;
#[derive(Default)]
struct EmulationOpts {
debug_mode: bool,
step_mode: bool
}
/// # main
///
/// Main entry point of the emulator
///
fn main() fn main()
{ {
// Collect our execution args // Collect our execution args
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().skip(1).collect();
// Grab our filepath from our options // Check if we received enough arguments
if &args.len() < &2 if args.len() == 0
{ {
// No file given, terminate // No arguments given, terminate
util::print_help(); util::print_help();
exit(0); exit(0);
} }
@ -23,6 +36,39 @@ fn main()
// Read last argument as path of chip8 executable // Read last argument as path of chip8 executable
let file_path: &String = &args[args.len()-1]; let file_path: &String = &args[args.len()-1];
// Check if an actual file path was provided and not just options
if file_path.starts_with('-')
{
// Only options were given, terminate
println!("[Error] A filename must be provided when passing arguments");
exit(0);
}
// Runtime options
let mut emu_options: EmulationOpts = EmulationOpts{..Default::default()};
// Option parsing
let num_options: usize = args.len() - 1;
for i in 0..num_options
{
if args[i].starts_with('-')
{
let flags: &str = &args[i][1..args[i].len()];
for char in flags.chars() {
if char == 'd' {
println!("Enabling debug mode...");
emu_options.debug_mode = true;
}
if char == 's' {
println!("Enabling step-through mode...");
emu_options.step_mode = true;
}
}
}
}
// Check if file_path exists in filesystem
if path::Path::new(file_path).exists() if path::Path::new(file_path).exists()
{ {
println!("'{}' exists, reading...", file_path); println!("'{}' exists, reading...", file_path);
@ -47,7 +93,7 @@ fn main()
} }
println!("Read {} bytes of program data", file_data.len()); println!("Read {} bytes of program data", file_data.len());
chip8::start_emulation(program); chip8::start_emulation(program, emu_options);
} }
} }
else else

View File

@ -1,5 +1,8 @@
pub fn print_help() { pub fn print_help() {
print!("Usage: saltnvinegar [CHIP8_FILE]\n"); println!("Usage: saltnvinegar [-OPTIONS] [CHIP8_FILE]");
println!("Options:");
println!("\t-d\tEnable debug output to terminal");
println!("\t-s\tEnable step-through-instruction mode");
} }
pub fn u8_tuple_to_u16(u8_in: (u8, u8)) -> u16 { pub fn u8_tuple_to_u16(u8_in: (u8, u8)) -> u16 {