sipsi-8/sipsi-8.py

788 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()