787 lines
22 KiB
Python
787 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
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,
|
|
]
|
|
keypad_keys = list(zip(keypad_keys, [
|
|
pyglet.window.key.NUM_7, pyglet.window.key.NUM_8, pyglet.window.key.NUM_9, pyglet.window.key.NUM_SUBTRACT,
|
|
pyglet.window.key.NUM_4, pyglet.window.key.NUM_5, pyglet.window.key.NUM_6, pyglet.window.key.NUM_ADD,
|
|
pyglet.window.key.NUM_1, pyglet.window.key.NUM_2, pyglet.window.key.NUM_3, pyglet.window.key.NUM_ENTER,
|
|
pyglet.window.key.NUM_DIVIDE, pyglet.window.key.NUM_0, pyglet.window.key.NUM_MULTIPLY, pyglet.window.key.NUM_SEPARATOR,
|
|
]))
|
|
# 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 in 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 in keypad_keys[i]:
|
|
keys_pressed[i] = False
|
|
break
|
|
|
|
def draw_screen():
|
|
global display_screen
|
|
to_draw = []
|
|
for y in range(32):
|
|
for x in range(64):
|
|
if display_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
|
|
|
|
# Keep track of whether we did a draw or a jump, for the draw
|
|
# sprint feature
|
|
did_draw = False
|
|
did_jump = False
|
|
|
|
# Don't execute any code in a waitstate, as it'd only end up
|
|
# busylooping
|
|
if waiting_for_keypress and not keypress_arrived: return did_draw, did_jump
|
|
|
|
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()
|
|
did_jump = True
|
|
|
|
# 0nnn call_machine
|
|
elif high_byte >> 4 == 0:
|
|
# Our own non-standard debug
|
|
if high_byte & 0xf == 0x1:
|
|
print(low_byte & 0xf, data_registers[low_byte & 0xf])
|
|
else:
|
|
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
|
|
did_jump = True
|
|
|
|
# 2nnn call nnn
|
|
elif high_byte >> 4 == 2:
|
|
stack.append(ip)
|
|
ip = ((high_byte & 0xf) << 8) | low_byte
|
|
did_jump = True
|
|
|
|
# 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
|
|
did_jump = True
|
|
|
|
# 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:
|
|
# OSCOM Nano manual (page 38) says "<The screen behaves as
|
|
# if the top was connected to the bottom and the sides to
|
|
# each other>", but I can't find any info on how to do this
|
|
# wrapping on the generally used sources.
|
|
#
|
|
# https://github.com/AfBu/haxe-chip-8-emulator looks like
|
|
# it wraps the pixels to the next row down, but as it
|
|
# doesn't seem to handle drawing out the bottom of the
|
|
# screen at all, unsure how intentional this is.
|
|
#
|
|
# https://github.com/dmatlack/chip8 wraps the pixels on the
|
|
# other side of the same line.
|
|
#
|
|
# The game "Blitz" seems to require draws off the bottom of
|
|
# the screen to not result in anything. Therefore, as I
|
|
# have yet to find any games that break with it, I'm just
|
|
# skipping pixels that fall outside the screen.
|
|
|
|
did_draw = True
|
|
|
|
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:
|
|
continue
|
|
|
|
for dx in range(8):
|
|
x = x_start + dx
|
|
# Screen is 64 columns wide
|
|
if x >= 64:
|
|
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))
|
|
|
|
return did_draw, did_jump
|
|
|
|
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
|
|
|
|
# Play a sound using pyglet if we have a non-zero sound timer
|
|
# It will run asynchronously from the other parts, which means it
|
|
# won't experience slowdown similarily to the delay timer
|
|
# Unsure if this is good or bad
|
|
if sound_timer > 0:
|
|
# sound_timer is in the unit of 1/60th of a second, while
|
|
# Square takes length in seconds
|
|
pyglet.media.synthesis.Square(sound_timer/60).play()
|
|
sound_timer = 0
|
|
|
|
def advance_interpreter(dt):
|
|
global cpu_clock
|
|
global screen, display_screen, screen_fps
|
|
global cpu_cycles_to_go, frames_to_go
|
|
# Each tic (60Hz) we need to run cpu_clock / 60Hz cycles and
|
|
# draw screen_fps / 60 frames
|
|
cpu_cycles_to_go += cpu_clock / 60
|
|
frames_to_go += screen_fps / 60
|
|
tick_timers()
|
|
|
|
while cpu_cycles_to_go >= 1:
|
|
did_draw, _ = step()
|
|
cpu_cycles_to_go -= 1
|
|
|
|
# If we did a draw, run upto two ticks' cycles to reach
|
|
# the next draw. Chip-8 draws are often paired, an erase
|
|
# and a draw. If we don't hit both in the same frame, it
|
|
# will cause flicker.
|
|
# Since this will put the number of cycles to go below zero
|
|
# if we run out of our own cycles, on average we will run
|
|
# the right number of cycles per tick
|
|
# Also, to avoid pairing draw half of one pair and erase
|
|
# half of another, the sprint is invalidated by a jump, a
|
|
# call or a ret
|
|
if did_draw:
|
|
#sprint_run_out = True#debg
|
|
for cycle in range(2*cpu_clock // 60):
|
|
did_draw, did_jump = step()
|
|
cpu_cycles_to_go -= 1
|
|
|
|
# Exit as soon as we hit the other draw
|
|
if did_draw:
|
|
#sprint_run_out = False#debg
|
|
#print('Sprint succeeded! %i' % cycle)#debg
|
|
break
|
|
|
|
if did_jump:
|
|
#sprint_run_out = False#debg
|
|
#print('Sprint failed! (jump) %i' % cycle)#debg
|
|
break
|
|
#if sprint_run_out: print('Sprint failed! (ran out)')#debg
|
|
|
|
if frames_to_go >= 1:
|
|
# Only draw the latest frame. This means FPS >60 won't work
|
|
# but why would you ever want to run chip-8 faster than
|
|
# that
|
|
display_screen = screen[:]
|
|
frames_to_go -= int(frames_to_go)
|
|
|
|
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, display_screen, screen_fps
|
|
screen = [False] * 64 * 32
|
|
display_screen = screen[:]
|
|
# TODO: Support changing FPS
|
|
screen_fps = 60
|
|
|
|
def initialize_timers():
|
|
global delay_timer, sound_timer
|
|
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(clockspeed):
|
|
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
|
|
|
|
cpu_clock = clockspeed
|
|
|
|
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, frames_to_go
|
|
|
|
if len(sys.argv) == 2:
|
|
program_path = sys.argv[1]
|
|
# Run the interpreter at 500Hz by default
|
|
# This was picked from https://github.com/AfBu/haxe-CHIP-8-emulator/wiki/(Super)CHIP-8-Secrets
|
|
cpu_clock = 500
|
|
elif len(sys.argv) == 3:
|
|
program_path = sys.argv[1]
|
|
cpu_clock = int(sys.argv[2])
|
|
else:
|
|
print(f'Usage: {sys.argv[0]} program [cpu-speed]', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# TODO: Don't hardcode the size
|
|
window = pyglet.window.Window(640, 320, resizable = True)
|
|
|
|
# Don't use pulse driver, as it is really buggy and can crash the
|
|
# python process
|
|
pyglet.options['audio'] = ('openal', 'directsound', 'silent')
|
|
|
|
# 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(cpu_clock)
|
|
|
|
# TODO: Deal with missing file gracefully
|
|
with open(program_path, 'rb') as f:
|
|
load_program(f)
|
|
|
|
cpu_cycles_to_go = 0
|
|
frames_to_go = 0
|
|
# Start the emulation
|
|
pyglet.clock.schedule_interval(advance_interpreter, 1/60)
|
|
|
|
pyglet.app.run()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|