From dc5a7ed2eb28375a26b3066dd8512be559bb5d5f Mon Sep 17 00:00:00 2001 From: Craig Thomas Date: Sun, 23 Jun 2024 12:19:13 -0400 Subject: [PATCH 1/2] Add bitplane selection command. --- chip8/cpu.py | 143 ++++++++++++++++++++++++++++-------------- test/test_chip8cpu.py | 14 +++++ 2 files changed, 109 insertions(+), 48 deletions(-) diff --git a/chip8/cpu.py b/chip8/cpu.py index d703ec9..11ea9ce 100644 --- a/chip8/cpu.py +++ b/chip8/cpu.py @@ -94,6 +94,8 @@ def __init__( self.pitch = 64 self.playback_rate = 4000 + self.bitplane = 1 + self.shift_quirks = shift_quirks self.index_quirks = index_quirks self.jump_quirks = jump_quirks @@ -125,6 +127,16 @@ def __init__( 0xF: self.misc_routines, # see subfunctions below } + self.clear_routines = { + 0xE0: self.clear_screen, # 00E0 - CLS + 0xEE: self.return_from_subroutine, # 00EE - RTS + 0xFB: self.scroll_right, # 00FB - SCRR + 0xFC: self.scroll_left, # 00FC - SCRL + 0xFD: self.exit, # 00FD - EXIT + 0xFE: self.disable_extended_mode, # 00FE - SET NORMAL + 0xFF: self.enable_extended_mode, # 00FF - SET EXTENDED + } + # This set of operations is invoked when the operand loaded into the # CPU starts with 8 (e.g. operand 8nn0 would call # self.move_reg_into_reg) @@ -144,6 +156,7 @@ def __init__( # CPU starts with F (e.g. operand Fn07 would call # self.move_delay_timer_into_reg) self.misc_routine_lookup = { + 0x01: self.set_bitplane, # Fn01 - BITPLANE n 0x07: self.move_delay_timer_into_reg, # Ft07 - LOAD Vt, DELAY 0x0A: self.wait_for_keypress, # Ft0A - KEYD Vt 0x15: self.move_reg_into_delay_timer, # Fs15 - LOAD DELAY, Vs @@ -249,17 +262,8 @@ def misc_routines(self): def clear_return(self): """ - Opcodes starting with a 0 are one of the following instructions: - - 0nnn - Jump to machine code function (ignored) - 00Cn - Scroll n pixels down - 00E0 - Clear the display - 00EE - Return from subroutine - 00FB - Scroll 4 pixels right - 00FC - Scroll 4 pixels left - 00FD - Exit - 00FE - Disable extended mode - 00FF - Enable extended mode + Opcodes starting with a 0 usually correspond to screen clearing or scrolling + routines, or emulator exit routines. """ operation = self.operand & 0x00FF sub_operation = operation & 0x00F0 @@ -267,32 +271,20 @@ def clear_return(self): num_lines = self.operand & 0x000F self.screen.scroll_down(num_lines) self.last_op = f"Scroll Down {num_lines:01X}" + else: + try: + self.clear_routines[operation]() + except KeyError: + raise UnknownOpCodeException(self.operand) - if operation == 0x00E0: - self.screen.clear_screen() - self.last_op = "CLS" - - if operation == 0x00EE: - self.return_from_subroutine() - - if operation == 0x00FB: - self.screen.scroll_right() - self.last_op = "Scroll Right" - - if operation == 0x00FC: - self.screen.scroll_left() - self.last_op = "Scroll Left" - - if operation == 0x00FD: - self.running = False - - if operation == 0x00FE: - self.disable_extended_mode() - self.last_op = "Set Normal Mode" + def clear_screen(self): + """ + 00E0 - CLS - if operation == 0x00FF: - self.enable_extended_mode() - self.last_op = "Set Extended Mode" + Clears the screen + """ + self.screen.clear_screen() + self.last_op = "CLS" def return_from_subroutine(self): """ @@ -307,6 +299,53 @@ def return_from_subroutine(self): self.pc += self.memory[self.sp] self.last_op = "RTS" + def scroll_right(self): + """ + 00FB - SCRR + + Scrolls the screen right by 4 pixels. + """ + self.screen.scroll_right() + self.last_op = "Scroll Right" + + def scroll_left(self): + """ + 00FC - SCRL + + Scrolls the screen left by 4 pixels. + """ + self.screen.scroll_left() + self.last_op = "Scroll Left" + + def exit(self): + """ + 00FD - EXIT + + Exits the emulator. + """ + self.running = False + self.last_op = "EXIT" + + def disable_extended_mode(self): + """ + 00FE - SET NORMAL + + Disables extended mode. + """ + self.screen.set_normal() + self.mode = MODE_NORMAL + self.last_op = "Set Normal Mode" + + def enable_extended_mode(self): + """ + 00FF - SET EXTENDED + + Set extended mode. + """ + self.screen.set_extended() + self.mode = MODE_EXTENDED + self.last_op = "Set Extended Mode" + def jump_to_address(self): """ 1nnn - JUMP nnn @@ -778,6 +817,26 @@ def draw_extended(self, x_pos, y_pos): self.v[0xF] += 1 self.screen.update() + def set_bitplane(self): + """ + Fn01 - BITPLANE n + + Selects the active bitplane for screen drawing operations. Bitplane + selection is as follows: + + 0 - no bitplane selected + 1 - first bitplane selected + 2 - second bitplane selected + 3 - first and second bitplane selected + + The bitplane selection values is as follows: + + Bits: 15-12 11-8 7-4 3-0 + F n 0 1 + """ + self.bitplane = (self.operand & 0x0F00) >> 8 + self.last_op = f"BITPLANE {self.bitplane:01X}" + def move_delay_timer_into_reg(self): """ Fx07 - LOAD Vx, DELAY @@ -1031,6 +1090,7 @@ def reset(self): self.rpl = [0] * NUM_REGISTERS self.pitch = 64 self.playback_rate = 4000 + self.bitplane = 1 def load_rom(self, filename, offset=PROGRAM_COUNTER_START): """ @@ -1054,18 +1114,5 @@ def decrement_timers(self): self.delay -= 1 if self.delay > 0 else 0 self.sound -= 1 if self.delay > 0 else 0 - def enable_extended_mode(self): - """ - Set extended mode. - """ - self.screen.set_extended() - self.mode = MODE_EXTENDED - - def disable_extended_mode(self): - """ - Disables extended mode. - """ - self.screen.set_normal() - self.mode = MODE_NORMAL # E N D O F F I L E ######################################################## diff --git a/test/test_chip8cpu.py b/test/test_chip8cpu.py index 6da2310..98b7f98 100644 --- a/test/test_chip8cpu.py +++ b/test/test_chip8cpu.py @@ -34,6 +34,20 @@ def setUp(self): self.cpu = Chip8CPU(self.screen) self.cpu_spy = mock.Mock(wraps=self.cpu) + def test_bitplane_1_init(self): + self.assertEqual(1, self.cpu.bitplane) + + def test_set_bitplane(self): + self.cpu.operand = 0xF201 + self.cpu.set_bitplane() + self.assertEqual(2, self.cpu.bitplane) + + def test_set_bitplane_integration(self): + self.cpu.memory[0x0200] = 0xF2 + self.cpu.memory[0x0201] = 0x01 + self.cpu.execute_instruction() + self.assertEqual(2, self.cpu.bitplane) + def test_memory_size_default_64k(self): self.assertEqual(65536, len(self.cpu.memory)) From 87b989b0a9e3a7c41aa9958d1d11804bc7bcdf10 Mon Sep 17 00:00:00 2001 From: Craig Thomas Date: Sun, 23 Jun 2024 15:50:03 -0400 Subject: [PATCH 2/2] Update unit tests to cover unknown opcode exception. --- chip8/cpu.py | 2 +- test/test_chip8cpu.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/chip8/cpu.py b/chip8/cpu.py index 11ea9ce..c48508b 100644 --- a/chip8/cpu.py +++ b/chip8/cpu.py @@ -36,7 +36,7 @@ class UnknownOpCodeException(Exception): A class to raise unknown op code exceptions. """ def __init__(self, op_code): - Exception.__init__(self, f"Unknown op-code: {op_code:X}") + Exception.__init__(self, f"Unknown op-code: {op_code:04X}") class Chip8CPU: diff --git a/test/test_chip8cpu.py b/test/test_chip8cpu.py index 98b7f98..6586767 100644 --- a/test/test_chip8cpu.py +++ b/test/test_chip8cpu.py @@ -34,6 +34,11 @@ def setUp(self): self.cpu = Chip8CPU(self.screen) self.cpu_spy = mock.Mock(wraps=self.cpu) + def test_clear_return_with_unknown_opcode(self): + with self.assertRaises(UnknownOpCodeException) as context: + self.cpu.execute_instruction(operand=0x00FA) + self.assertEqual("Unknown op-code: 00FA", str(context.exception)) + def test_bitplane_1_init(self): self.assertEqual(1, self.cpu.bitplane) @@ -747,7 +752,7 @@ def test_misc_routines_raises_exception_on_unknown_op_codes(self): self.cpu.operand = 0x0 with self.assertRaises(UnknownOpCodeException) as context: self.cpu.misc_routines() - self.assertEqual("Unknown op-code: 0", str(context.exception)) + self.assertEqual("Unknown op-code: 0000", str(context.exception)) def test_scroll_down_called(self): self.cpu.operand = 0x00C4