Skip to content

Instantly share code, notes, and snippets.

@itarato
Created December 31, 2016 04:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itarato/794e3d543a8d13f77586909848574fc3 to your computer and use it in GitHub Desktop.
Save itarato/794e3d543a8d13f77586909848574fc3 to your computer and use it in GitHub Desktop.
Chip8 emulator.
"""
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