Basic instruction handling, more formatting, step-through mode

This commit is contained in:
Garrett Dickinson 2024-08-10 05:41:28 -05:00
parent fec824610c
commit 77ef72d2ee
4 changed files with 151 additions and 34 deletions

Binary file not shown.

View File

@ -2,13 +2,17 @@
// Implementation of chip8 machine, registers, and memory makeup // Implementation of chip8 machine, registers, and memory makeup
use std::fmt::Debug; use std::fmt::Debug;
use std::process::exit;
use std::io::{self, Read};
use crate::util; 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;
@ -17,21 +21,35 @@ 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
@ -56,10 +74,12 @@ 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],
// Misc members options: EmulationOpts,
debug_enabled: bool
// Program stack
stack: [u16; MAX_STACK_SIZE]
} }
/// # start_emulation /// # start_emulation
@ -68,14 +88,15 @@ struct EmulationState {
/// ///
/// * `data` - u8 array of program data. /// * `data` - u8 array of program data.
/// ///
pub fn start_emulation(data: [u8; MAX_PRG_SIZE], dbg_mode: bool) pub fn start_emulation(data: [u8; MAX_PRG_SIZE], options_in: EmulationOpts)
{ {
let mut state: EmulationState = EmulationState { 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],
debug_enabled: dbg_mode stack: [0; 16],
options: options_in
}; };
load_program(data, &mut state); load_program(data, &mut state);
@ -137,6 +158,11 @@ fn exec_program(state: &mut EmulationState)
} }
parse_instruction(state, instruction); parse_instruction(state, instruction);
if state.options.step_mode
{
pause();
}
} }
} }
@ -148,32 +174,82 @@ fn exec_program(state: &mut EmulationState)
/// * `instr` - Incoming instruction. /// * `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
{ {
// Clear the display
debug_log_instr(state, instr, "CLS".to_string()); debug_log_instr(state, instr, "CLS".to_string());
// TODO: Implement this
return;
} }
// RET; Return from subroutine
// Sets the program counter to the address at the top of the stack, then pops the value from
// the stack
if instr == INSTR_RET if instr == INSTR_RET
{ {
debug_log_instr(state, instr, "RET".to_string()); debug_log_instr(state, instr, "RET".to_string());
}
if (instr & INSTR_MASK_SYS) > 0 // Halt emulation if stack_ptr is 0, since decrementing on an empty stack
// is undefined behavior
if state.stack_ptr == 0
{ {
let addr: u16 = instr & ADDR_MASK; halt(state, instr, format!("Cannot decrement stack pointer below stack limit {}", MAX_STACK_SIZE));
debug_log_instr(state, instr, format!("SYS {:04X?}", addr));
} }
state.stack_ptr -= 1;
state.program_counter = state.stack[state.stack_ptr];
state.stack[state.stack_ptr] = 0x0000;
return;
}
// JP nnn; Jump to location nnn.
// Sets the program counter to nnn.
if (instr & INSTR_MASK_JP) > 0 if (instr & INSTR_MASK_JP) > 0
{ {
let addr: u16 = instr & ADDR_MASK; let addr: u16 = instr & ADDR_MASK;
debug_log_instr(state, instr, format!("JP {:04X?}", addr)); debug_log_instr(state, instr, format!("JP {:04X?}", addr));
state.program_counter = addr;
return;
} }
// CALL nnn; Call subroutine at nnn
// Puts the current PC in the stack, then increments the stack pointer. The program counter
// is then set to nnn.
if (instr & INSTR_MASK_CALL) > 0 if (instr & INSTR_MASK_CALL) > 0
{ {
let addr: u16 = instr & ADDR_MASK; let addr: u16 = instr & ADDR_MASK;
debug_log_instr(state, instr, format!("CALL {:04X?}", addr)); 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;
}
// SE Vx, byte; Skip next instruction if Vx = kk.
// Compares register Vx to kk, and if they are equal, increments the
// 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;
} }
} }
@ -187,8 +263,33 @@ fn parse_instruction(state: &mut EmulationState, instr: u16) {
/// ///
fn debug_log_instr(state: &mut EmulationState, instr: u16, msg: String) fn debug_log_instr(state: &mut EmulationState, instr: u16, msg: String)
{ {
if state.debug_enabled if state.options.debug_mode
{ {
println!("PC: {:04?}; INSTR {:04X?}; {}", state.program_counter, instr, msg); 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,4 +1,4 @@
use core::num; use std::option;
use std::path; use std::path;
use std::env; use std::env;
use std::fs; use std::fs;
@ -8,6 +8,14 @@ use std::process::exit;
mod util; mod util;
mod chip8; mod chip8;
#[derive(Default)]
struct EmulationOpts {
debug_mode: bool,
step_mode: bool
}
/// # main /// # main
/// ///
/// Main entry point of the emulator /// Main entry point of the emulator
@ -37,7 +45,7 @@ fn main()
} }
// Runtime options // Runtime options
let mut debug_mode: bool = false; let mut emu_options: EmulationOpts = EmulationOpts{..Default::default()};
// Option parsing // Option parsing
let num_options: usize = args.len() - 1; let num_options: usize = args.len() - 1;
@ -49,7 +57,12 @@ fn main()
for char in flags.chars() { for char in flags.chars() {
if char == 'd' { if char == 'd' {
debug_mode = true; println!("Enabling debug mode...");
emu_options.debug_mode = true;
}
if char == 's' {
println!("Enabling step-through mode...");
emu_options.step_mode = true;
} }
} }
} }
@ -80,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, debug_mode); 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 {