Skip to content
Permalink
Browse files

use reset/init routine from kernal ROM if present

  • Loading branch information...
irmen committed Jul 11, 2018
1 parent 64b5e1f commit e65737cf3d5dde371935a32972164965668901d1
Showing with 140 additions and 57 deletions.
  1. +12 −1 README.md
  2. +5 −4 pyc64/basic.py
  3. +4 −1 pyc64/cputools.py
  4. +20 −12 pyc64/emulator.py
  5. +65 −8 pyc64/memory.py
  6. +32 −29 pyc64/shared.py
  7. +2 −2 start64plusplus.py
@@ -1,4 +1,4 @@
# pyc64: Commodore-64 "emulator" in pure Python!
# pyc64: Commodore-64 simulator in pure Python!

![Screenshot one](demo_screenshot1.png)

@@ -19,6 +19,17 @@ you can only do the same things with it as what is supported
for the BASIC mode. So no fancy screen scrolling or rasterbars...


## can load and use real C64 ROMs

If basic/kernal/chargen ROM files are supplied in the roms folder,
the simulator will load these and make them part of the memory of the machine as ROM areas.
You can actually call machine code routines from the roms!
Such as the well-known reset routine, SYS 64738 .
The simulator will use this routine as well when the machine is reset!
(just a few slight kernel patches are made to make it compatible and to avoid ending
up in the actual ROM BASIC program loop - that doesn't work for now)


## BASIC V2 and the VIC-registers (video chip)

A subset and variant of the BASIC V2 in the C-64 is provided.
@@ -109,9 +109,11 @@ def reset(self):
self.program_lines = None
self.sleep_until = None
self.must_run_stop = False
self.screen.writestr("\n **** commodore 64 basic v2 ****\n")
self.screen.writestr("\n 64k ram system 38911 basic bytes free\n")
self.write_prompt("\n")
if not self.screen.using_roms:
# only print the basic header when we're not using actual roms
self.screen.writestr("\n **** commodore 64 basic v2 ****\n")
self.screen.writestr("\n 64k ram system 38911 basic bytes free\n")
self.write_prompt("\n")
self.stop_running_program()

@property
@@ -211,7 +213,6 @@ def runstop(self):
self.write_prompt()

def _execute_cmd(self, cmd, all_cmds_on_line=None):
# print("RUN CMD:", repr(cmd)) # XXX
if cmd.startswith(("read", "rE")):
self.execute_read(cmd)
elif cmd.startswith(("restore", "reS")):
@@ -30,8 +30,9 @@ def _install_mpu_observers(self, getc_addr, putc_addr):


class CPU(mpu6502.MPU):
def run(self, pc=None, microsleep=None, loop_detect_delay=2):
def run(self, pc=None, microsleep=None, loop_detect_delay=0.5):
end_address = 0xffff
self.sp = 0xf2
self.stPushWord(end_address - 1) # push a sentinel return address
if pc is not None:
self.pc = pc
@@ -43,6 +44,8 @@ def run(self, pc=None, microsleep=None, loop_detect_delay=2):
# JMP to itself, instead of looping forever we also consider this a program end
end_time = time.perf_counter()
time.sleep(loop_detect_delay)
print(self.name + " CPU simulator: infinite jmp loop detected at ${:04x}, considered as end-of-program.".format(self.pc))
self.stPopWord() # pop the sentinel return address
break
self.step()
instructions += 1
@@ -1,5 +1,5 @@
"""
Commodore-64 'emulator' in 100% pure Python 3.x :)
Commodore-64 simulator in 100% pure Python 3.x :)
This module is the GUI window logic, handling keyboard input
and screen drawing via tkinter bitmaps.
@@ -20,13 +20,13 @@
from PIL import Image
from .memory import ScreenAndMemory
from .basic import BasicInterpreter
from .shared import ResetMachineException
from .shared import ResetMachineException, do_sys
from .python import PythonInterpreter


def create_bitmaps_from_char_rom(temp_graphics_folder, roms_directory):
# create char bitmaps from the orignal c-64 chargen rom file
rom = open(roms_directory+"/"+"chargen", "rb").read()
rom = open(roms_directory+"/chargen", "rb").read()
def doublewidth_and_mirror(b):
result = 0
for _ in range(8):
@@ -68,7 +68,7 @@ class EmulatorWindowBase(tkinter.Tk):
charset_normal = "charset-normal.png"
charset_shifted = "charset-shifted.png"
colorpalette = []
welcome_message = "Welcome to the emulator!"
welcome_message = "Welcome to the simulator!"

def __init__(self, screen, title, roms_directory):
if len(self.colorpalette) not in (2, 4, 8, 16, 32, 64, 128, 256):
@@ -94,8 +94,8 @@ def __init__(self, screen, title, roms_directory):
borderwidth=0, highlightthickness=0, background="black",
xscrollincrement=1, yscrollincrement=1)
self.buttonbar = tkinter.Frame(self)
resetbut = tkinter.Button(self.buttonbar, text="reset", command=self.reset_machine)
resetbut.pack(side=tkinter.LEFT)
resetbut1 = tkinter.Button(self.buttonbar, text="reset", command=self.reset_machine)
resetbut1.pack(side=tkinter.LEFT)
self.buttonbar.pack(fill=tkinter.X)
self.refreshtick = threading.Event()
self.spritebitmapbytes = [None] * self.sprites
@@ -243,12 +243,15 @@ def create_bitmaps(self, roms_directory=""):
os.makedirs(self.temp_graphics_folder, exist_ok=True)
with open(self.temp_graphics_folder + "/readme.txt", "w") as f:
f.write("this is a temporary folder to cache pyc64 files for tkinter graphics bitmaps.\n")
if roms_directory and os.path.isfile(roms_directory+"/"+"chargen"):
if roms_directory and os.path.isfile(roms_directory+"/chargen"):
# create char bitmaps from the C64 chargen rom file.
print("creating char bitmaps from chargen rom")
create_bitmaps_from_char_rom(self.temp_graphics_folder, roms_directory)
else:
print("creating char bitmaps from png images in the package")
if roms_directory:
print("creating char bitmaps from png images in the package (consider supplying {:s}/chargen ROM file)".format(roms_directory))
else:
print("creating char bitmaps from png images in the package")
# normal
with Image.open(io.BytesIO(pkgutil.get_data(__name__, "charset/" + self.charset_normal))) as source_chars:
for i in range(256):
@@ -302,7 +305,7 @@ def create_sprite_bitmap(self, spritenum, bitmapbytes):
raise NotImplementedError("implement in subclass")

def reset_machine(self):
self.screen.reset()
self.screen.reset(False)
self.repaint()


@@ -325,9 +328,9 @@ def __init__(self, screen, title, roms_directory):

def start(self):
super().start()
self.switch_interpreter("basic")
self._cyclic_herztick()
self._cyclic_blink_cursor()
self.reset_machine()

def _cyclic_herztick(self):
self.after(1000 // self.screen.hz, self._cyclic_herztick)
@@ -616,6 +619,10 @@ def reset_machine(self):
super().reset_machine()
self.screen.memory[0x00fb] = EmulatorWindowBase.update_rate
self.switch_interpreter("basic")
if self.screen.using_roms:
print("using actual ROM reset routine (sys 64738)")
do_sys(self.screen, 64738, use_rom_routines=True)
self.interpreter.write_prompt("\n\n\n\n\n")


class InterpretThread(threading.Thread):
@@ -742,11 +749,12 @@ def runstop(self):


def start():
rom_directory = "roms"
screen = ScreenAndMemory(columns=C64EmulatorWindow.columns,
rows=C64EmulatorWindow.rows,
sprites=C64EmulatorWindow.sprites,
rom_directory="roms")
emu = C64EmulatorWindow(screen, "Commodore-64 'emulator' in pure Python!", "roms")
rom_directory=rom_directory)
emu = C64EmulatorWindow(screen, "Commodore-64 simulator in pure Python!", rom_directory)
emu.start()
emu.mainloop()

@@ -63,7 +63,6 @@ def __len__(self):

def clear(self):
"""set all memory values to 0."""
print("clear mem") # XXX
for a in range(0, 0x10000):
self[a] = 0

@@ -161,8 +160,15 @@ def _write_with_romcheck_slice(self, addrslice, value):
if addrslice.start <= rom_end and addrslice.stop >= rom_start+1:
# the slice could be *partially* in RAM and *partially* in ROM
# we're not figuring that out here, just write/check every byte individually.
for addr in range(*addrslice.indices(self.size)):
self[addr] = value
if type(value) is int:
for addr in range(*addrslice.indices(self.size)):
self[addr] = value
else:
slice_range = range(*addrslice.indices(self.size))
if len(slice_range) != len(value):
raise ValueError("value length differs from memory slice length")
for addr, value in zip(slice_range, value):
self[addr] = value
return
# whole slice is outside of all rom areas, just write it
self.mem[addrslice] = value
@@ -189,6 +195,10 @@ def load_rom(self, romfile, address):
self.mem[address:address+len(data)] = data
self.rom_areas.add((address, address+len(data)-1))

def _patch(self, address, value):
# hard overwrite a value, don't do ROM check, no callbacks
self.mem[address] = value


class ScreenAndMemory:
colorpalette_morecontrast = ( # this is a palette with more contrast
@@ -251,11 +261,22 @@ def __init__(self, columns=40, rows=25, sprites=8, rom_directory=""):
# screen chars $0400-$07ff
# screen colors $d800-$dbff
self.memory = Memory(65536) # 64 Kb
self.using_roms = False
if rom_directory:
for rom, address in (("basic", 0xa000), ("kernal", 0xe000)):
if os.path.isfile(rom_directory + "/" + rom):
print("loading rom file", rom, "at", hex(address))
try:
self.memory.load_rom(rom_directory+"/"+rom, address)
print("loading rom file", rom, "at", hex(address))
self.using_roms = True
except IOError:
print("can't load rom-file {:s}/{:s}, consider supplying it".format(rom_directory, rom))
self.memory._patch(0xe388, 0x4c) # JMP to same address near the end of the reset routine
self.memory._patch(0xe389, 0x88) # to avoid entering actual basic program loop. RTS won't work because the stack is clobbered I think.
self.memory._patch(0xe38a, 0xe3) # (this jmp loop is recognised by the cpu emulator as an 'end of the program')
# self.memory._patch(0xfce5, 0xea) # NOP to not clobber stack pointer register in reset routine
self.memory._patch(0xff61, 0xea) # NOP to skip a loop in the reset routine
self.memory._patch(0xff62, 0xea) # NOP to skip a loop in the reset routine
self.memory._patch(0xfcf6, 0x90) # skip a large part of the memory init routine that is very slow and may cause issues
self.hz = 60 # NTSC
self.columns = columns
self.rows = rows
@@ -323,9 +344,43 @@ def reset(self, hard=False):
self._cursor_enabled = True
self._previous_checked_chars = bytearray(self.columns * self.rows)
self._previous_checked_colors = bytearray(self.columns * self.rows)
self.memory[0x000:0x0300] = bytearray(256 * 3) # clear first 3 pages
if hard:
self.memory.clear()
# initialize the zero page and stack with a dump of the values of these pages from a running c64 memory dump
self.memory[0x0000:0x0200] = [0x2f, 0x37, 0x00, 0xaa, 0xb1, 0x91, 0xb3, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x16, 0x00, 0x0a, 0x76, 0xa3, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x76, 0xa3, 0xb3, 0xbd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x03, 0x08, 0x03,
0x08, 0x03, 0x08, 0x00, 0xa0, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00,
0x00, 0x0a, 0x76, 0xa3, 0x19, 0x00, 0x20, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x04, 0x00, 0x76,
0x00, 0x80, 0xa3, 0xe6, 0x7a, 0xd0, 0x02, 0xe6, 0x7b, 0xad, 0x00, 0x08, 0xc9, 0x3a, 0xb0, 0x0a,
0xc9, 0x20, 0xf0, 0xef, 0x38, 0xe9, 0x30, 0x38, 0xe9, 0xd0, 0x60, 0x80, 0x4f, 0xc7, 0x52, 0x58,
0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x00, 0x00,
0x00, 0x01, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xa0, 0x30, 0xfd, 0x40, 0x00, 0x00, 0x00, 0x06, 0x00, 0x40, 0x00, 0x01, 0x20, 0x01,
0x00, 0xf0, 0x04, 0x00, 0x00, 0x27, 0x06, 0x85, 0x00, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x87, 0x87, 0x87,
0x87, 0x87, 0x87, 0xf0, 0xd8, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20,
0x33, 0x38, 0x39, 0x31, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x03, 0x8d, 0x94, 0x9e, 0xa9, 0x00,
0x8d, 0x00, 0xde, 0x60, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x3f, 0x7d, 0xea, 0x7d, 0xea, 0x20, 0x07, 0xff, 0x7d, 0xea, 0x85, 0x01,
0x00, 0x22, 0xd4, 0xe5, 0x00, 0x0a, 0x14, 0xe1, 0x64, 0xa5, 0x85, 0xa4, 0x79, 0xa6, 0x9c, 0xe3]
self.memory[0x0200:0x0300] = bytearray(256) # clear the third page
if hard:
# from $0800-$ffff we have a 00/FF pattern alternating every 64 bytes
for m in range(0x0840, 0x10000, 128):
self.memory[m: m + 64] = b"\xff" * 64
@@ -342,8 +397,10 @@ def reset(self, hard=False):
self.memory[0x02a6] = 1 # we're a PAL machine
self.memory[0xd011] = 27 # Vic control register 1 (yscroll etc)
self.memory[0xd016] = 200 # Vic control register 2 (xscroll etc)
self.memory[0xa000:0xc000] = 96 # basic ROM are all RTS instructions
self.memory[0xe000:0x10000] = 96 # kernal ROM are all RTS instructions
if (0xa000, 0xbfff) not in self.memory.rom_areas:
self.memory[0xa000:0xc000] = 96 # basic ROM are all RTS instructions (in case no ROM file is present)
if (0xe000, 0xffff) not in self.memory.rom_areas:
self.memory[0xe000:0x10000] = 96 # kernal ROM are all RTS instructions (in case no ROM file is present)
self.jiffieclock_epoch = time.perf_counter()
self.border = 14
self.screen = 6
@@ -106,35 +106,38 @@ def do_load(screen, arg):
raise IOError("unknown file type")


def do_sys(screen, addr, microsleep=None):
def do_sys(screen, addr, microsleep=None, use_rom_routines=False):
if addr < 0 or addr > 0xffff:
raise ValueError("illegal quantity")
if addr in (64738, 64760):
raise ResetMachineException()
if addr == 58640: # set cursorpos
x, y = screen.memory[211], screen.memory[214]
screen.cursormove(x, y)
elif addr in (58629, 65517): # kernel SCREEN (get screen size X=colums Y=rows)
screen.memory[0x30d] = screen.columns
screen.memory[0x30e] = screen.rows
elif addr in (65520, 58634): # kernel PLOT (get/set cursorpos)
if screen.memory[0x030f] & 1:
# carry set, read position
x, y = screen.cursorpos()
screen.memory[211], screen.memory[214] = x, y
screen.memory[0x030e], screen.memory[0x030d] = x, y
else:
# carry clear, set position
x, y = screen.memory[0x030e], screen.memory[0x030d]
screen.memory[211], screen.memory[214] = x, y
if not use_rom_routines:
if addr in (64738, 64760):
raise ResetMachineException()
if addr == 58640: # set cursorpos
x, y = screen.memory[211], screen.memory[214]
screen.cursormove(x, y)
else:
from .cputools import CPU
cpu = CPU(memory=screen.memory, pc=addr)
# read A,X,Y and P from the ram
cpu.a, cpu.x, cpu.y, cpu.p = screen.memory[0x030c:0x0310]
try:
cpu.run(microsleep=microsleep)
finally:
# store result A,X,Y and P back to ram
screen.memory[0x030c:0x0310] = cpu.a, cpu.x, cpu.y, cpu.p
return
elif addr in (58629, 65517): # kernel SCREEN (get screen size X=colums Y=rows)
screen.memory[0x30d] = screen.columns
screen.memory[0x30e] = screen.rows
return
elif addr in (65520, 58634): # kernel PLOT (get/set cursorpos)
if screen.memory[0x030f] & 1:
# carry set, read position
x, y = screen.cursorpos()
screen.memory[211], screen.memory[214] = x, y
screen.memory[0x030e], screen.memory[0x030d] = x, y
else:
# carry clear, set position
x, y = screen.memory[0x030e], screen.memory[0x030d]
screen.memory[211], screen.memory[214] = x, y
screen.cursormove(x, y)
return
from .cputools import CPU
cpu = CPU(memory=screen.memory, pc=addr)
# read A,X,Y and P from the ram
cpu.a, cpu.x, cpu.y, cpu.p = screen.memory[0x030c:0x0310]
try:
cpu.run(microsleep=microsleep)
finally:
# store result A,X,Y and P back to ram
screen.memory[0x030c:0x0310] = cpu.a, cpu.x, cpu.y, cpu.p
Oops, something went wrong.

0 comments on commit e65737c

Please sign in to comment.
You can’t perform that action at this time.