import random import sys import pyglet # TODO: Don't hardcode keyboard input keypad_keys = [ pyglet.window.key._1, pyglet.window.key._2, pyglet.window.key._3, pyglet.window.key._4, pyglet.window.key.Q, pyglet.window.key.W, pyglet.window.key.E, pyglet.window.key.R, pyglet.window.key.A, pyglet.window.key.S, pyglet.window.key.D, pyglet.window.key.F, pyglet.window.key.Z, pyglet.window.key.X, pyglet.window.key.C, pyglet.window.key.V, ] # OSCOM Nano keys are arranged like # 123C # 456D # 789E # A0BF # according to http://hobbylabs.org/oscom_nano.htm # However, at the moment the indices of the keys are # 0123 # 4567 # 89ab # cdef # This rearranges them so that the index and the hex digit match keypad_keys = [keypad_keys[i] for i in [0xd, 0, 1, 2, 4, 5, 6, 8, 9, 0xa, 0xc, 0xe, 3, 7, 0xb, 0xf]] def key_pressed(symbol): global keys_pressed, keypress_arrived for i in range(16): if symbol == keypad_keys[i]: keys_pressed[i] = True keypress_arrived = True break def key_released(symbol): global keys_pressed for i in range(16): if symbol == keypad_keys[i]: keys_pressed[i] = False break def draw_screen(): to_draw = [] for y in range(32): for x in range(64): if screen[y * 64 + x]: to_draw.append((x, y)) screen_points = [] for x, y in to_draw: # pyglet has 0,0 at bottom left, so reverse the y y = 31 - y # TODO: Don't hardcode pixel size # Lol, what is a winding order screen_points.append(x * 10) screen_points.append(y * 10) screen_points.append(x * 10 + 10) screen_points.append(y * 10) screen_points.append(x * 10 + 10) screen_points.append(y * 10 + 10) screen_points.append(x * 10) screen_points.append(y * 10 + 10) if len(screen_points) > 0: pyglet.graphics.draw(len(screen_points) // 2, pyglet.gl.GL_QUADS, ('v2i', screen_points) ) def step(): global data_registers, ip, stack, i_register global ram, font_start global screen global delay_timer, sound_timer global keys_pressed, waiting_for_keypress, keypress_arrived # Don't execute any code in a waitstate, as it'd only end up # busylooping if waiting_for_keypress and not keypress_arrived: return high_byte = ram[ip] ip = (ip + 1) & 0xfff low_byte = ram[ip] ip = (ip + 1) & 0xfff #print(hex(high_byte), hex(low_byte), hex(ip), [hex(i) for i in data_registers], [hex(i) for i in stack], hex(i_register), delay_timer)#debg #input()#debg # Instruction info gotten from http://mattmik.com/files/chip8/mastering/chip8.html # 00E0 clearscreen if high_byte == 0x00 and low_byte == 0xE0: screen = [False] * 64 * 32 # 00EE ret elif high_byte == 0x00 and low_byte == 0xEE: ip = stack.pop() # 0nnn call_machine elif high_byte >> 4 == 0: print("%03x: Can't call machine language!" % (ip - 2)) sys.exit(1) # 1nnn jmp nnn elif high_byte >> 4 == 1: ip = ((high_byte & 0xf) << 8) | low_byte # 2nnn call nnn elif high_byte >> 4 == 2: stack.append(ip) ip = ((high_byte & 0xf) << 8) | low_byte # 3xnn skipeq vx, nn elif high_byte >> 4 == 3: if data_registers[high_byte & 0xf] == low_byte: ip = (ip + 2) & 0xfff # 4xnn skipneq vx, nn elif high_byte >> 4 == 4: if data_registers[high_byte & 0xf] != low_byte: ip = (ip + 2) & 0xfff # 5xy0 skipeq vx, vy elif high_byte >> 4 == 5 and low_byte & 0xf == 0: if data_registers[high_byte & 0xf] == data_registers[low_byte >> 4]: ip = (ip + 2) & 0xfff # 6xnn mov vx, nn elif high_byte >> 4 == 6: data_registers[high_byte & 0xf] = low_byte # 7xnn add vx, nn (no flags) elif high_byte >> 4 == 7: old_value = data_registers[high_byte & 0xf] data_registers[high_byte & 0xf] = (old_value + low_byte) & 0xff # 8xy0 mov vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 0: data_registers[high_byte & 0xf] = data_registers[low_byte >> 4] # 8xy1 or vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 1: old_value = data_registers[high_byte & 0xf] result = old_value | data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result # 8xy2 and vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 2: old_value = data_registers[high_byte & 0xf] result = old_value & data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result # 8xy3 xor vx, vy elif high_byte >> 4 == 8 and low_byte & 0xf == 3: old_value = data_registers[high_byte & 0xf] result = old_value ^ data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result # 8xy4 add vx, vy (carry flag) elif high_byte >> 4 == 8 and low_byte & 0xf == 4: old_value = data_registers[high_byte & 0xf] result = old_value + data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result & 0xff if result > 255: data_registers[0xf] = 1 else: data_registers[0xf] = 0 # 8xy5 sub vx, vy (carry flag) elif high_byte >> 4 == 8 and low_byte & 0xf == 5: old_value = data_registers[high_byte & 0xf] result = old_value - data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = result & 0xff if result < 0: data_registers[0xf] = 0 else: data_registers[0xf] = 1 # 8xy6 shr vx, vy, 1 (LSB to VF) elif high_byte >> 4 == 8 and low_byte & 0xf == 6: other_value = data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = other_value >> 1 data_registers[0xf] = other_value & 0x1 # 8xy7 sub vx, vy, vx (carry flag) elif high_byte >> 4 == 8 and low_byte & 0xf == 7: old_value = data_registers[high_byte & 0xf] result = data_registers[low_byte >> 4] - old_value data_registers[high_byte & 0xf] = result & 0xff if result < 0: data_registers[0xf] = 0 else: data_registers[0xf] = 1 # 8xyE shl vx, vy, 1 (MSB to VF) elif high_byte >> 4 == 8 and low_byte & 0xf == 0xE: other_value = data_registers[low_byte >> 4] data_registers[high_byte & 0xf] = (other_value << 1) & 0xff data_registers[0xf] = other_value >> 7 # 9xy0 skipneq vx, vy elif high_byte >> 4 == 9 and low_byte & 0xf == 0: if data_registers[high_byte & 0xf] != data_registers[low_byte >> 4]: ip = (ip + 2) & 0xfff # Annn mov i, nnn elif high_byte >> 4 == 0xA: i_register = ((high_byte & 0xf) << 8) | low_byte # Bnnn jmp nnn + v0 elif high_byte >> 4 == 0xB: ip = ((high_byte & 0xf) << 8) | low_byte ip = (ip + data_registers[0]) & 0xfff # Cxnn maskedrandom vx, nn elif high_byte >> 4 == 0xC: result = random.randint(0, 255) data_registers[high_byte & 0xf] = result & low_byte # Dxyn draw vx, vy, n elif high_byte >> 4 == 0xD: # TODO: OSCOM Nano manual (page 38) says "" Investigate how this wrapping # should be implemented x_start = data_registers[high_byte & 0xf] y_start = data_registers[low_byte >> 4] any_unset = False # Sprite will be 8 pixels wide and n tall for dy in range(low_byte & 0xf): # Load this line's graphics line = ram[(i_register + dy) & 0xfff] y = y_start + dy # Screen is 32 lines tall if y >= 32: # TODO: Figure how y >= 32 works continue for dx in range(8): x = x_start + dx # Screen is 64 columns wide if x >= 64: # TODO: Figure how x >= 64 works continue # Pixels are stored MSB-left pixel = (line >> (7 - dx)) & 1 screen_index = y * 64 + x if pixel: # Check if we are unsetting a pixel if screen[screen_index]: any_unset = True # XOR the pixel screen[screen_index] = not screen[screen_index] if any_unset: data_registers[0xf] = 1 else: data_registers[0xf] = 0 # Ex9E skipkey vx elif high_byte >> 4 == 0xE and low_byte == 0x9E: if keys_pressed[data_registers[high_byte & 0xf]]: ip = (ip + 2) & 0xfff # ExA1 skipnkey vx elif high_byte >> 4 == 0xE and low_byte == 0xA1: if not keys_pressed[data_registers[high_byte & 0xf]]: ip = (ip + 2) & 0xfff # Fx07 getdelay vx elif high_byte >> 4 == 0xF and low_byte == 0x07: data_registers[high_byte & 0xf] = delay_timer # Fx0A getkey vx elif high_byte >> 4 == 0xF and low_byte == 0x0A: if not waiting_for_keypress: # Wait for a key to be pressed and then re-execute # this instruction waiting_for_keypress = True keypress_arrived = False ip = (ip - 2) & 0xfff if keypress_arrived: # A key was pressed for i in range(16): if keys_pressed[i]: # Set vx to first key we found any_pressed = True data_registers[high_byte & 0xf] = i break waiting_for_keypress = False # Fx15 setdelay vx elif high_byte >> 4 == 0xF and low_byte == 0x15: delay_timer = data_registers[high_byte & 0xf] # Fx18 setsound vx elif high_byte >> 4 == 0xF and low_byte == 0x18: sound_timer = data_registers[high_byte & 0xf] # Fx1E add i, vx elif high_byte >> 4 == 0xF and low_byte == 0x1E: i_register = (i_register + data_registers[high_byte & 0xf]) & 0xfff # Fx29 mov i, digit(vx) elif high_byte >> 4 == 0xF and low_byte == 0x29: value = data_registers[high_byte & 0xf] if 0 <= value <= 0xf: # Each digit is 5 lines tall = 5 bytes long i_register = font_start + value * 5 else: print('%03x: Bad font character' % (ip-2)) sys.exit(1) # Fx33 mov [i … i+2], bcd(vx) elif high_byte >> 4 == 0xF and low_byte == 0x33: value = data_registers[high_byte & 0xf] highdigit = value // 100 middigit = value // 10 % 10 lowdigit = value % 10 ram[i_register] = highdigit ram[(i_register + 1) & 0xfff] = middigit ram[(i_register + 2) & 0xfff] = lowdigit # Fx55 mov [i … i+x], v0 … vx (updates i) elif high_byte >> 4 == 0xF and low_byte == 0x55: for register in range((high_byte & 0xf) + 1): ram[i_register] = data_registers[register] i_register = (i_register + 1) & 0xfff # Fx65 mov v0 … vx, [i … i+x] (updates i) elif high_byte >> 4 == 0xF and low_byte == 0x65: for register in range((high_byte & 0xf) + 1): data_registers[register] = ram[i_register] i_register = (i_register + 1) & 0xfff # Fallback else: print('%03x: Unrecognized!' % (ip-2)) def tick_timers(): global delay_timer, sound_timer # Tick the delay timer down until it reaches 0 if delay_timer > 0: # The timer should tick down at 60Hz delay_timer -= 1 # TODO: Do sth about the sound timer def advance_interpreter(dt): global cpu_speed global cpu_cycles_to_go # Each tic (60Hz) we need to run cpu_clock / 60Hz cycles cpu_cycles_to_go += cpu_clock / 60 tick_timers() while cpu_cycles_to_go >= 1: step() cpu_cycles_to_go -= 1 def initialize_ram(): global ram, font_start ram = [0]*(1<<12) # Font data is from http://mattmik.com/files/chip8/mastering/chip8.html # and http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.4 font = [ 0xf0, 0x90, 0x90, 0x90, 0xf0, # 0 0x20, 0x60, 0x20, 0x20, 0x70, # 1 0xf0, 0x10, 0xf0, 0x80, 0xf0, # 2 0xf0, 0x10, 0xf0, 0x10, 0xf0, # 3 0x90, 0x90, 0xf0, 0x10, 0x10, # 4 0xf0, 0x80, 0xf0, 0x10, 0xf0, # 5 0xf0, 0x80, 0xf0, 0x90, 0xf0, # 6 0xf0, 0x10, 0x20, 0x40, 0x40, # 7 0xf0, 0x90, 0xf0, 0x90, 0xf0, # 8 0xf0, 0x90, 0xf0, 0x10, 0xf0, # 9 0xf0, 0x90, 0xf0, 0x90, 0x90, # A 0xe0, 0x90, 0xe0, 0x90, 0xe0, # B 0xf0, 0x80, 0x80, 0x80, 0xf0, # C 0xe0, 0x90, 0x90, 0x90, 0xe0, # D 0xf0, 0x80, 0xf0, 0x80, 0xf0, # E 0xf0, 0x80, 0xf0, 0x80, 0x80, # F ] # Unsure where the font should go. Both http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/ # and http://stevelosh.com/blog/2016/12/chip8-graphics/?m=1#fonts # say it goes at 0x50, but most resources don't have its location # specified and I have run into implementations that have it at # start of memory (http://craigthomas.ca/blog/2017/10/15/writing-a-chip-8-emulator-built-in-font-set-part-4/) # and even a resource stating it's at high memory (http://www.multigesture.net/wp-content/uploads/mirror/goldroad/chip8.shtml) # The location honestly shouldn't matter, but I'm putting it at # 0x50 font_start = 0x50 ram[font_start:font_start + len(font)] = font test_program = [ ## Arithmetic test #0x65, 0x42, # 0x42 → v5 | v5: 42 #0x82, 0x50, # v5 → v2 | v2: 42, v5: 42 #0x72, 0x69, # v2 + 0x69 → v2 | v2: ab, v5: 42 #0x85, 0x24, # v5 + v2 → v5 | v2: ab, v5: ed, vf: 00 #0x85, 0x24, # v5 + v2 → v5 | v2: ab, v5: 98, vf: 01 #0x85, 0x25, # v5 - v2 → v5 | v2: ab, v5: ed, vf: 00 #0x82, 0x57, # v5 - v2 → v2 | v2: 42, v5: ed, vf: 01 #0x63, 0x01, # 0x01 → v3 | v2: 42, v3: 01, v5: ed, vf: 01 #0x85, 0x22, # v5 & v2 → v5 | v2: 42, v3: 01, v5: 40, vf: 01 #0x83, 0x51, # v3 | v5 → v3 | v2: 42, v3: 41, v5: 40, vf: 01 #0x82, 0x33, # v2 ^ v3 → v2 | v2: 3, v3: 41, v5: 40, vf: 01 #0x82, 0x56, # v5 >> 1 → v2 | v2: 20, v3: 41, v5: 40, vf: 00 #0x85, 0x36, # v3 >> 1 → v5 | v2: 20, v3: 41, v5: 20, vf: 01 #0x64, 0x70, # 0x70 → v4 | v2: 20, v3: 41, v4: 70, v5: 20, vf: 01 #0x84, 0x4E, # v4 << 1 → v4 | v2: 20, v3: 41, v4: e0, v5: 20, vf: 00 #0x84, 0x4E, # v4 << 1 → v4 | v2: 20, v3: 41, v4: c0, v5: 20, vf: 01 ## RNG test #0xC0, 0xff, # RNG → v0 #0xC0, 0xff, # RNG → v0 #0xC0, 0xff, # RNG → v0 #0xC0, 0xff, # RNG → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 #0xC0, 0x06, # RNG & 0x00001110 → v0 ## Flow control test #0x60, 0x00, # mov v0, 0 #0x22, 0x06, # call 0x206 #0x12, 0x02, # jmp 0x202 #0x70, 0x01, # add v0, 1 #0x00, 0xee, # ret ## Skip test #0x61, 0xf0, # mov v1, 0xf0 #0x62, 0xf1, # mov v2, 0xf1 #0x83, 0x10, # mov v3, v1 #0x31, 0xf0, # skipeq v3, 0xf0 #0x00, 0x00, # ill #0x31, 0x00, # skipeq v3, 0 #0x6e, 0x01, # mov ve, 1 #0x51, 0x30, # skipeq v1, v3 #0x00, 0x00, # ill #0x42, 0xf0, # skipneq v2, 0xf0 #0x00, 0x00, # ill #0x91, 0x20, # skipneq v1, v2 #0x00, 0x00, # ill #0x6d, 0x01, # mov vd, 1 ## Timer test #0xF0, 0x07, # getdelay v0 #0x40, 0x00, # skipneq v0, 0 #0x22, 0x08, # call 0x208 #0x12, 0x00, # jmp 0x200 #0x60, 0xff, # mov v0, 0xff #0xf0, 0x15, # setdelay v0 #0x00, 0xee, # ret ## Input wait test #0xf1, 0x0a, # getkey v1 ## Input skip test #0x61, 0x00, # mov v1, 0 #0x62, 0x0a, # mov v2, 0xa #0x63, 0x0b, # mov v3, 0xb #0xe2, 0xa1, # skipnkey v2 #0x71, 0x01, # add v1, 1 #0xe3, 0x9e, # skipkey v3 #0x12, 0x06, # jmp 0x206 ## Input wait test the second #0xa1, 0x23, # mov i, 0x123 #0xf0, 0x0a, # getkey v0 #0xf0, 0x1e, # add i, v0 #0x12, 0x02, # jmp 0x202 ## Drawing program / drawing test ## From the OSCOM Nano manual #0x6a, 0x01, # mov va, 1 #0x60, 0x10, # mov v0, 0x10 #0x61, 0x20, # mov v1, 0x20 #0xa2, 0x50, # mov i, 0x250 #0xf2, 0x0a, # getkey v2 #0x42, 0x01, # skipneq v2, 1 #0x22, 0x2e, # call 0x22e #0x42, 0x02, # skipneq v2, 2 #0x80, 0xa5, # sub v0, va #0x42, 0x03, # skipneq v2, 3 #0x22, 0x34, # call 0x234 #0x42, 0x04, # skipneq v2, 4 #0x81, 0xa5, # sub v1, va #0x42, 0x06, # skipneq v2, 6 #0x81, 0xa4, # add v1, va #0x42, 0x07, # skipneq v2, 7 #0x22, 0x3a, # call 0x23a #0x42, 0x08, # skipneq v2, 8 #0x80, 0xa4, # add v0, va #0x42, 0x09, # skipneq v2, 9 #0x22, 0x40, # call 0x240 #0xD1, 0x01, # draw v0, v1, 1 #0x12, 0x08, # jmp 0x208 ## 22e #0x81, 0xa5, # sub v1, va #0x80, 0xa5, # sub v0, va #0x00, 0xee, # ret ## 234 #0x80, 0xa5, # sub v0, va #0x81, 0xa4, # add v1, va #0x00, 0xee, # ret ## 23a #0x80, 0xa4, # add v0, va #0x81, 0xa5, # sub v1, va #0x00, 0xee, # ret ## 240 #0x80, 0xa4, # add v0, va #0x81, 0xa4, # add v1, va #0x00, 0xee, # ret ## This was missing from the original ## Actually, never mind, it was just separately ## 246 #0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ## 250 #0x80, ## Font test #0x60, 0x0a, # mov v0, 0x0a #0xf1, 0x0a, # getkey v1 #0x00, 0xe0, # clearscreen #0xf1, 0x29, # mov i, digit(v1) #0xd0, 0x05, # draw v0, v0, 5 #0x12, 0x02, # jmp 0x202 ## Base converter ## From the OSCOM Nano manual ## Modified to run several times and display input #0x6a, 0x00, # mov va, 0 #0x6b, 0x00, # mov vb, 0 #0xf0, 0x0a, # getkey v0 #0x82, 0x00, # mov v2, v0 #0x00, 0xe0, # clearscreen #0xf0, 0x29, # mov i, digit(v0) #0xda, 0xb5, # draw va, vb, 5 #0x7a, 0x06, # add va, 6 #0xf1, 0x0a, # getkey v1 #0xf1, 0x29, # mov i, digit(v1) #0xda, 0xb5, # draw va, vb, 5 #0x7b, 0x07, # add vb, 7 #0x6a, 0x00, # mov va, 0 #0x63, 0x00, # mov v3, 0 ## 21c #0x80, 0x24, # add v0, v2 #0x73, 0x01, # add v3, 1 #0x33, 0x0f, # skipeq v3, 0xf #0x12, 0x1c, # jmp 0x21c #0x80, 0x14, # add v0, v1 #0xA2, 0x50, # mov i, 0x250 #0xf0, 0x33, # mov [i … i+2], bcd(v0) #0xf2, 0x65, # mov v0 … v2, [i … i+2] #0xf0, 0x29, # mov i, digit(v0) #0xda, 0xb5, # draw va, vb, 5 #0x7a, 0x06, # add va, 6 #0xf1, 0x29, # mov i, digit(v1) #0xda, 0xb5, # draw va, vb, 5 #0x7a, 0x06, # add va, 6 #0xf2, 0x29, # mov i, digit(v2) #0xda, 0xb5, # draw va, vb, 5 #0x12, 0x00, # jmp 0x200 ] #ram[0x200:len(test_program) + 0x200] = test_program def initialize_screen(): global screen screen = [False] * 64 * 32 def initialize_timers(): global delay_timer, sound_timer # Tick timers down at 60Hz delay_timer = 0 sound_timer = 0 def initialize_keyboard(): global keys_pressed, waiting_for_keypress, keypress_arrived keys_pressed = [False]*16 waiting_for_keypress = False keypress_arrived = False def initialize_cpu(): global cpu_clock, data_registers, ip, stack, i_register data_registers = [0] * 16 # According to the OSCOM Nano manual the program is loaded here ip = 0x200 # It doesn't seem to be specified where this is stored, so put it # in its own "address space" stack = [] i_register = 0 # TODO: Don't hardcode the update speed # Run the interpreter at 500Hz # This was picked from https://github.com/AfBu/haxe-CHIP-8-emulator/wiki/(Super)CHIP-8-Secrets cpu_clock = 500 def load_program(f): global ram # ram is an array of numbers, not a bytestring program = [i for i in f.read()] # Load at 0x200 ram[0x200: 0x200 + len(program)] = program def main(): global window global cpu_cycles_to_go # Don't hardcode the size window = pyglet.window.Window(640, 320, resizable = True) # Hook up our screen drawing routine @window.event def on_draw(): # Clear the screen window.clear() # Draw things draw_screen() # Handle keyboard input @window.event def on_key_press(symbol, modifiers): key_pressed(symbol) @window.event def on_key_release(symbol, modifiers): key_released(symbol) initialize_ram() initialize_screen() initialize_timers() initialize_keyboard() initialize_cpu() with open(sys.argv[1], 'rb') as f: load_program(f) cpu_cycles_to_go = 0 # Start the emulation pyglet.clock.schedule_interval(advance_interpreter, 1/60) pyglet.app.run() if __name__ == '__main__': main()