Created
December 31, 2016 04:12
-
-
Save itarato/794e3d543a8d13f77586909848574fc3 to your computer and use it in GitHub Desktop.
Chip8 emulator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Chip8 emulator. | |
""" | |
import array | |
import random | |
import pygame | |
# 4K. | |
MEMORY_SIZE = 1 << 12 | |
# Display 64 by 32 | |
VIDEO_DISPLAY_W = 0x40 | |
VIDEO_DISPLAY_H = 0x20 | |
# Colors. | |
BLACK = (0, 0, 0) | |
WHITE = (255, 255, 255) | |
class SizedArray(array.array): | |
def __init__(self, type, with_size=None, with_default=None): | |
super(SizedArray, self).__init__(type) | |
if with_size is not None: | |
for _ in range(with_size): | |
self.append(with_default) | |
class Memory(object): | |
def __init__(self): | |
self.memory = SizedArray('B', with_size=MEMORY_SIZE, with_default=0) | |
print "Memory has been initialized to:", MEMORY_SIZE | |
def reset(self): | |
for i in range(MEMORY_SIZE): | |
self.memory[i] = 0 | |
fonts = [ | |
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 | |
] | |
for i in range(len(fonts)): | |
self.memory[i] = fonts[i] | |
class CPU(object): | |
def __init__(self): | |
self.pc = 0 | |
self.gpr = SizedArray('B', with_size=16, with_default=0) | |
self.reg_i = 0 | |
self.stack = [] | |
print "CPU has been initialized" | |
def reset(self): | |
self.pc = 0x200 | |
class Display(object): | |
def __init__(self, width, height, pixel_size=1): | |
self.width = width | |
self.height = height | |
self.size = width * height | |
self.memory = SizedArray('B', with_size=self.size, with_default=0) | |
self.pixel_size = pixel_size | |
self.screen = pygame.display.set_mode([self.width * self.pixel_size, self.height * self.pixel_size]) | |
pygame.display.set_caption("Chip8 emulator") | |
print "Display has been initialized" | |
def draw(self): | |
self.screen.fill(BLACK) | |
pix = self.pixel_size | |
for y in range(self.height): | |
for x in range(self.width): | |
pygame.draw.rect(self.screen, WHITE if self.memory[y * self.width + x] > 0 else BLACK, [x * pix + (pix >> 1) - 1, y * pix, 1, pix], pix) | |
pygame.display.flip() | |
def set_pixels(self, coord_x, coord_y, byte_pattern): | |
has_flip = 0 | |
for i in range(0x8): | |
new_bit = (byte_pattern >> (0x7 - i)) & 1 | |
if new_bit == 0x1: | |
coord_idx = coord_y * self.width + coord_x + i | |
if 0 <= coord_idx < 2048: | |
has_flip |= self.memory[coord_idx] | |
self.memory[coord_idx] ^= new_bit | |
else: | |
print "Too much idx", coord_idx, "x", coord_x, "y", coord_y, "bit offs", i | |
return bool(has_flip) | |
def clear(self): | |
for i in range(self.size): | |
self.memory[i] = 0 | |
class Interrupt(object): | |
def __init__(self): | |
self.delay_timer = 0 | |
self.sound_timer = 0 | |
print "Interrupt has been initialized" | |
class Input(object): | |
def __init__(self): | |
self.size = 0x10 | |
self.key = SizedArray('B', with_size=self.size, with_default=0) | |
def reset(self): | |
for i in range(self.size): | |
self.key[i] = 0 | |
def handle_key_event(self, event): | |
self.reset() | |
key_map = [ | |
pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, | |
pygame.K_q, pygame.K_w, pygame.K_e, pygame.K_r, | |
pygame.K_a, pygame.K_s, pygame.K_d, pygame.K_f, | |
pygame.K_z, pygame.K_x, pygame.K_c, pygame.K_v, | |
] | |
for i in range(self.size): | |
if event.key == key_map[i]: | |
self.key[i] = 1 | |
def get_current_key_code(self): | |
for i in range(len(self.key)): | |
if self.key[i]: | |
return i | |
return None | |
class Audio(object): | |
def __init__(self): | |
print "Audio has been initialized" | |
def beep(self): | |
print "> BEEP <" | |
class Chip8(object): | |
def __init__(self): | |
pygame.init() | |
self.memory = Memory() | |
self.cpu = CPU() | |
self.display = Display(VIDEO_DISPLAY_W, VIDEO_DISPLAY_H, 4) | |
self.interrupt = Interrupt() | |
self.input = Input() | |
self.audio = Audio() | |
self.draw_flag = False | |
self.reset() | |
print "Chip8 has been initialized" | |
def reset(self): | |
self.init_gfx() | |
self.init_input() | |
self.init_chip() | |
def init_gfx(self): | |
pass | |
def init_input(self): | |
pass | |
def init_chip(self): | |
self.cpu.reset() | |
self.memory.reset() | |
def load_rom(self, rom): | |
for i in range(len(rom)): | |
self.memory.memory[0x200 + i] = rom[i] | |
def execute_cycle(self): | |
opcode = self.memory.memory[self.cpu.pc] << 0x8 | self.memory.memory[self.cpu.pc + 1] | |
# print "OPC: 0x%X as LOC: 0x%X" % (opcode, self.cpu.pc) | |
self.cpu.pc += 2 | |
# | .... , .... | .... , .... | | |
# hi_fst hi_snd lo_fst lo_snd | |
hi = opcode >> 0x8 | |
lo = opcode & 0xFF | |
hi_fst = hi >> 0x4 | |
hi_snd = hi & 0xF | |
lo_fst = lo >> 0x4 | |
lo_snd = lo & 0xF | |
lo_three = opcode & 0xFFF | |
if hi == 0x0: | |
if lo == 0xE0: | |
# 00E0 Display disp_clear() Clears the screen. | |
self.display.clear() | |
self.draw_flag = True | |
elif lo == 0xEE: | |
# 00EE Flow return; Returns from a subroutine. | |
if len(self.cpu.stack) == 0: | |
self.exec_err(opcode=opcode, message="Stack is empty, cannot pop") | |
self.cpu.pc = self.cpu.stack.pop() | |
else: | |
self.exec_err(opcode=opcode) | |
elif hi_fst == 0x0: | |
# 0NNN Call Calls RCA 1802 program at address NNN. Not necessary for most ROMs. | |
self.exec_err(message="RCA 1802 call is not implemented", opcode=opcode) | |
elif hi_fst == 0x1: | |
# 1NNN Flow goto NNN; Jumps to address NNN. | |
self.cpu.pc = lo_three | |
elif hi_fst == 0x2: | |
# 2NNN Flow *(0xNNN)() Calls subroutine at NNN. | |
self.cpu.stack.append(self.cpu.pc) | |
self.cpu.pc = lo_three | |
elif hi_fst == 0x3: | |
# 3XNN Cond if(Vx==NN) Skips the next instruction if VX equals NN. (Usually the next instruction is a jump to skip a code block) | |
if self.cpu.gpr[hi_snd] == lo: | |
self.cpu.pc += 2 | |
elif hi_fst == 0x4: | |
# 4XNN Cond if(Vx!=NN) Skips the next instruction if VX doesn't equal NN. (Usually the next instruction is a jump to skip a code block) | |
if self.cpu.gpr[hi_snd] != lo: | |
self.cpu.pc += 2 | |
elif hi_fst == 0x5: | |
# 5XY0 Cond if(Vx==Vy) Skips the next instruction if VX equals VY. (Usually the next instruction is a jump to skip a code block) | |
if self.cpu.gpr[hi_snd] == self.cpu.gpr[lo_fst]: | |
self.cpu.pc += 2 | |
elif hi_fst == 0x6: | |
# 6XNN Const Vx = NN Sets VX to NN. | |
self.cpu.gpr[hi_snd] = lo | |
elif hi_fst == 0x7: | |
# 7XNN Const Vx += NN Adds NN to VX. | |
self.cpu.gpr[hi_snd] = (self.cpu.gpr[hi_snd] + lo) % 0x100 | |
elif hi_fst == 0x8: | |
if lo_snd == 0x0: | |
# 8XY0 Assign Vx=Vy Sets VX to the value of VY. | |
self.cpu.gpr[hi_snd] = self.cpu.gpr[lo_fst] | |
elif lo_snd == 0x1: | |
# 8XY1 BitOp Vx=Vx|Vy Sets VX to VX or VY. (Bitwise OR operation) | |
self.cpu.gpr[hi_snd] |= self.cpu.gpr[lo_fst] | |
elif lo_snd == 0x2: | |
# 8XY2 BitOp Vx=Vx&Vy Sets VX to VX and VY. (Bitwise AND operation) | |
self.cpu.gpr[hi_snd] &= self.cpu.gpr[lo_fst] | |
elif lo_snd == 0x3: | |
# 8XY3 BitOp Vx=Vx^Vy Sets VX to VX xor VY. | |
self.cpu.gpr[hi_snd] ^= self.cpu.gpr[lo_fst] | |
elif lo_snd == 0x4: | |
# 8XY4 Math Vx += Vy Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't. | |
has_carry, self.cpu.gpr[hi_snd] = carry(self.cpu.gpr[hi_snd] + self.cpu.gpr[lo_fst]) | |
self.cpu.gpr[0xF] = 1 if has_carry else 0 | |
elif lo_snd == 0x5: | |
# 8XY5 Math Vx -= Vy VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't. | |
has_carry, self.cpu.gpr[hi_snd] = carry(self.cpu.gpr[hi_snd] - self.cpu.gpr[lo_fst]) | |
self.cpu.gpr[0xF] = 0 if has_carry else 1 | |
elif lo_snd == 0x6: | |
# 8XY6 BitOp Vx >> 1 Shifts VX right by one. VF is set to the value of the least significant bit of VX before the shift.[2] | |
self.cpu.gpr[0xF] = self.cpu.gpr[hi_snd] & 0x1 | |
self.cpu.gpr[hi_snd] >>= 1 | |
elif lo_snd == 0x7: | |
# 8XY7 Math Vx=Vy-Vx Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't. | |
has_carry, self.cpu.gpr[hi_snd] = carry(self.cpu.gpr[lo_fst] - self.cpu.gpr[hi_snd]) | |
self.cpu.gpr[0xF] = 0 if has_carry else 1 | |
elif lo_snd == 0xE: | |
# 8XYE BitOp Vx << 1 Shifts VX left by one. VF is set to the value of the most significant bit of VX before the shift.[2] | |
self.cpu.gpr[0xF] = (self.cpu.gpr[hi_snd] >> 0x7) & 0x1 | |
self.cpu.gpr[hi_snd] = (self.cpu.gpr[hi_snd] << 1) % 0x100 | |
else: | |
self.exec_err(opcode=opcode) | |
elif hi_fst == 0x9: | |
# 9XY0 Cond if(Vx!=Vy) Skips the next instruction if VX doesn't equal VY. (Usually the next instruction is a jump to skip a code block) | |
if self.cpu.gpr[hi_snd] != self.cpu.gpr[lo_fst]: | |
self.cpu.pc += 2 | |
elif hi_fst == 0xA: | |
# ANNN MEM I = NNN Sets I to the address NNN. | |
self.cpu.reg_i = lo_three | |
elif hi_fst == 0xB: | |
# BNNN Flow PC=V0+NNN Jumps to the address NNN plus V0. | |
self.cpu.pc = self.cpu.gpr[0x0] + lo_three | |
elif hi_fst == 0xC: | |
# CXNN Rand Vx=rand()&NN Sets VX to the result of a bitwise and operation on a random number (Typically: 0 to 255) and NN. | |
self.cpu.gpr[hi_snd] = random.randint(0x0, 0xFF) & lo | |
elif hi_fst == 0xD: | |
# DXYN Disp draw(Vx,Vy,N) Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels and a height of N pixels. | |
# Each row of 8 pixels is read as bit-coded starting from memory location I; I value does not change after the execution of this instruction. | |
# As described above, VF is set to 1 if any screen pixels are flipped from set to unset when the sprite is drawn, and to 0 if that does not happen | |
has_flip = False | |
for i in range(lo_snd): | |
coord_x = self.cpu.gpr[hi_snd] | |
coord_y = self.cpu.gpr[lo_fst] | |
has_flip |= self.display.set_pixels(coord_x, coord_y + i, self.memory.memory[self.cpu.reg_i + i]) | |
self.cpu.gpr[0xF] = 1 if has_flip else 0 | |
self.draw_flag = True | |
elif hi_fst == 0xE: | |
if lo == 0x9E: | |
# EX9E KeyOp if(key()==Vx) Skips the next instruction if the key stored in VX is pressed. (Usually the next instruction is a jump to skip a code block) | |
if self.input.key[self.cpu.gpr[hi_snd]]: | |
self.cpu.pc += 2 | |
elif lo == 0xA1: | |
# EXA1 KeyOp if(key()!=Vx) Skips the next instruction if the key stored in VX isn't pressed. (Usually the next instruction is a jump to skip a code block) | |
if not self.input.key[self.cpu.gpr[hi_snd]]: | |
self.cpu.pc += 2 | |
else: | |
self.exec_err(opcode=opcode) | |
elif hi_fst == 0xF: | |
if lo == 0x07: | |
# FX07 Timer Vx = get_delay() Sets VX to the value of the delay timer. | |
self.cpu.gpr[hi_snd] = self.interrupt.delay_timer | |
elif lo == 0x0A: | |
# FX0A KeyOp Vx = get_key() A key press is awaited, and then stored in VX. (Blocking Operation. All instruction halted until next key event) | |
last_key_code = self.input.get_current_key_code() | |
if last_key_code is not None: | |
self.cpu.gpr[hi_snd] = last_key_code | |
else: | |
return | |
elif lo == 0x15: | |
# FX15 Timer delay_timer(Vx) Sets the delay timer to VX. | |
self.interrupt.delay_timer = self.cpu.gpr[hi_snd] | |
elif lo == 0x18: | |
# FX18 Sound sound_timer(Vx) Sets the sound timer to VX. | |
self.interrupt.sound_timer = self.cpu.gpr[hi_snd] | |
elif lo == 0x1E: | |
# FX1E MEM I +=Vx Adds VX to I.[3] | |
(self.cpu.reg_i, has_carry) = carry(self.cpu.reg_i + self.cpu.gpr[hi_snd], bound=0x1000) | |
self.cpu.gpr[0xF] = 1 if has_carry else 0 | |
elif lo == 0x29: | |
# FX29 MEM I=sprite_addr[Vx] Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4x5 font. | |
self.cpu.reg_i = (self.cpu.gpr[hi_snd] * 0x5) % 0x1000 | |
elif lo == 0x33: | |
# FX33 BCD set_BCD(Vx); | |
# *(I+0)=BCD(3); | |
# *(I+1)=BCD(2); | |
# *(I+2)=BCD(1); | |
# Stores the binary-coded decimal representation of VX, with the most significant of three digits at the address in I, | |
# the middle digit at I plus 1, and the least significant digit at I plus 2. (In other words, take the decimal | |
# representation of VX, place the hundreds digit in memory at location in I, the tens digit at location I+1, and | |
# the ones digit at location I+2.) | |
self.memory.memory[self.cpu.reg_i + 0] = self.cpu.gpr[hi_snd] % 1000 // 100 | |
self.memory.memory[self.cpu.reg_i + 1] = self.cpu.gpr[hi_snd] % 100 // 10 | |
self.memory.memory[self.cpu.reg_i + 2] = self.cpu.gpr[hi_snd] % 10 // 1 | |
elif lo == 0x55: | |
# FX55 MEM reg_dump(Vx,&I) Stores V0 to VX (including VX) in memory starting at address I.[4] | |
for i in range(0, hi_snd + 1): | |
self.memory.memory[self.cpu.reg_i + i] = self.cpu.gpr[i] | |
self.cpu.reg_i = (self.cpu.reg_i + hi_snd + 1) % 0x1000 | |
elif lo == 0x65: | |
# FX65 MEM reg_load(Vx,&I) Fills V0 to VX (including VX) with values from memory starting at address I.[4] | |
for i in range(0, hi_snd + 1): | |
self.cpu.gpr[i] = self.memory.memory[self.cpu.reg_i + i] | |
self.cpu.reg_i = (self.cpu.reg_i + hi_snd + 1) % 0x1000 | |
else: | |
self.exec_err(opcode=opcode) | |
else: | |
self.exec_err(opcode=opcode) | |
if self.interrupt.delay_timer > 0: | |
self.interrupt.delay_timer -= 1 | |
if self.interrupt.sound_timer > 0: | |
if self.interrupt.sound_timer == 1: | |
self.audio.beep() | |
self.interrupt.sound_timer -= 1 | |
def exec_err(self, opcode=None, message="unknown opcode"): | |
print "Error | PC: 0x%X" % self.cpu.pc, | |
if message is not None: | |
print "|", message, | |
if opcode is not None: | |
print "| opcode: 0x%X" % opcode, | |
print "" | |
exit(1) | |
def run(self): | |
clock = pygame.time.Clock() | |
has_finished = False | |
while not has_finished: | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
has_finished = True | |
elif event.type == pygame.KEYDOWN: | |
self.input.handle_key_event(event) | |
self.execute_cycle() | |
if self.draw_flag: | |
self.display.draw() | |
self.draw_flag = False | |
# clock.tick(2) | |
pygame.quit() | |
def carry(result, bound=0x100): | |
has_carry = result < 0 or result >= bound | |
return result % bound, has_carry | |
if __name__ == "__main__": | |
c8 = Chip8() | |
with open('/Users/itarato/Desktop/c8games/TANK', 'rb') as file: | |
rom_bytes = bytearray() | |
while True: | |
byte = file.read(1) | |
if byte == '': | |
break | |
rom_bytes.append(byte) | |
c8.load_rom(rom_bytes) | |
c8.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment