Port from py6502 to py65 since it is more mature and has fewer CPU

bugs.

Implement basic support for parts of the apple II memory map
- 64K address space is assembled from multiple memory regions
- a memory region can optionally intercept reads and/or writes
- memory regions may be marked non-writable (this has a bug and isn't
  yet working)
- execution handler can intercept execution to defined entry points
  when PC enters a memory region
- can trap when PC enters a memory region to an unhandled entry point

- basic support for some IO page soft switches and status registers
  - mostly just printing an event

- support loading boot1 image from sqlite DB

- load and install bits of the apple IIe ROM
  - but I don't think I have got all of the important bits from the
    image yet -- e.g. the CXROM image at alternate $C100 is not yet
    installed
This commit is contained in:
kris 2017-05-19 22:41:56 +01:00
parent 9f8538b5dc
commit aadc97efcb
2 changed files with 287 additions and 28 deletions

97
memory.py Normal file
View File

@ -0,0 +1,97 @@
from py65 import memory
from collections import defaultdict
class WriteProtectedException(Exception):
def __init__(self, name, addr, value):
self.name = name
self.addr = addr
self.value = value
def __str__(self):
return 'Write denied to %s ($%04X): $%02X' % (self.name, self.addr, self.value)
class UndefinedEntryPointException(Exception):
def __init__(self, region, prev_addr, addr):
self.region = region
self.prev_addr = prev_addr
self.addr = addr
def __str__(self):
return 'Entered region %s via undefined entry point: $%04X --> $%04X' % (
self.region.name, self.prev_addr, self.addr)
class MemoryManager(object):
def __init__(self, memory_map):
self.entrypoints = defaultdict(list)
default_region = MemoryRegion('default', 0x0, 0xffff)
def _default_region():
return default_region
self.regions = defaultdict(_default_region)
self.memory = memory.ObservableMemory()
for region in memory_map:
self.RegisterRegion(region)
def MaybeInterceptExecution(self, cpu, old_pc):
pc = cpu.pc
if pc in self.entrypoints:
handlers = self.entrypoints[pc]
else:
handlers = []
if self.regions[old_pc] != self.regions[pc]:
print "Entering region %s" % self.regions[pc].name
if not handlers:
raise UndefinedEntryPointException(self.regions[pc], old_pc, pc)
for handler in handlers:
handler(cpu)
return
def RegisterRegion(self, region):
addr_range = xrange(region.start, region.end + 1)
if region.read_interceptor:
self.memory.subscribe_to_read(addr_range, region.read_interceptor)
if region.write_interceptor:
self.memory.subscribe_to_write(addr_range, region.write_interceptor)
if not region.writable:
self.memory.subscribe_to_write(addr_range, self.DenyWritesToRegion(region))
for addr in addr_range:
self.regions[addr] = region
for addr, handler in region.entrypoints.iteritems():
self.entrypoints[addr].append(handler)
# TODO: should trap by default
@staticmethod
def DenyWritesToRegion(region):
def _DenyWritesInterceptor(addr, value):
raise WriteProtectedException(region.name, addr, value)
return _DenyWritesInterceptor
class MemoryRegion(object):
def __init__(
self, name, start, end, read_interceptor=None, write_interceptor=None, entrypoints=None,
writable=True):
self.name = name
self.start = start
self.end = end
self.read_interceptor = read_interceptor
self.write_interceptor = write_interceptor
self.writable = writable
# Maps PC to handler
self.entrypoints = entrypoints or {}

View File

@ -1,55 +1,217 @@
"""Partial simulation of Apple II"""
import sqlite3
import memory
from py65.devices import mpu65c02
from py65 import memory
DB_PATH = '/tank/apple2/data/apple2.db'
MODE_READ = 0x1
MODE_WRITE = 0x2
MODE_RW = MODE_READ | MODE_WRITE
class Event(object):
def __init__(self, event_type, details):
self.event_type = event_type
self.details = details
def __str__(self):
return "Event(%s): %s" % (self.event_type, self.details)
def _Event(region, message):
def _Event(cpu):
print "%s event: %s" % (region, message)
return _Event
class TrapException(Exception):
def __init__(self, address, msg):
self.address = address
self.msg = msg
def __str__(self):
return "$%04X: %s" % (self.address, self.msg)
class AppleII(object):
def __init__(self):
# TODO: should trap by default
self.memory = memory.ObservableMemory()
memory_map = [
memory.MemoryRegion("Zero page", 0x0000, 0x00ff),
memory.MemoryRegion("Stack", 0x0100, 0x01ff),
memory.MemoryRegion(
"Text page 1", 0x0400, 0x7ff,
write_interceptor=self.TextPageWriteInterceptor),
memory.MemoryRegion(
"IO page", 0xc000, 0xc0ff, read_interceptor=self.IOInterceptor,
write_interceptor=self.IOInterceptor),
memory.MemoryRegion("Slot 1 ROM", 0xc100, 0xc1ff, writable=False),
memory.MemoryRegion("Slot 2 ROM", 0xc200, 0xc2ff, writable=False),
memory.MemoryRegion("Slot 3 ROM", 0xc300, 0xc3ff, writable=False),
memory.MemoryRegion("Slot 4 ROM", 0xc400, 0xc4ff, writable=False),
memory.MemoryRegion("Slot 5 ROM", 0xc500, 0xc5ff, writable=False),
memory.MemoryRegion("Slot 6 ROM", 0xc600, 0xc6ff,
entrypoints={
0xc65c: self._ReadDiskSector
},
writable=False
),
memory.MemoryRegion("Slot 7 ROM", 0xc700, 0xc7ff, writable=False),
memory.MemoryRegion(
"ROM", 0xd000, 0xffff,
entrypoints={
0xfca8: self._Wait,
0xfe89: _Event("ROM", "Select the keyboard (IN#0)")
},
writable=False
)
]
self.memory_manager = memory.MemoryManager(memory_map)
self.memory = self.memory_manager.memory
self.cpu = mpu65c02.MPU(memory=self.memory)
# Set up interceptors for accessing various interesting parts of the memory map
# Text page 1
self.memory.subscribe_to_write(xrange(0x400, 0x7ff), self.TextPageWriteInterceptor)
self.io_map = {
0xc007: (MODE_WRITE, "Turn CXROM switch on"),
0xc015: (MODE_READ, "Status of Peripheral/CXROM Access"),
self.memory.subscribe_to_write(xrange(0xc000, 0xffff), self.TraceWriteInterceptor)
self.memory.subscribe_to_read(xrange(0xc000, 0xffff), self.TraceReadInterceptor)
0xc051: (MODE_READ, "Display text"),
0xc054: (MODE_READ, "Text page 1"),
0xc056: (MODE_READ, "Enter lores mode"),
# Slot 6 Disk II
0xc0e0: (MODE_READ, "phase 0 off"),
0xc0e1: (MODE_READ, "phase 0 on"),
0xc0e2: (MODE_READ, "phase 1 off"),
0xc0e3: (MODE_READ, "phase 1 on"),
0xc0e4: (MODE_READ, "phase 2 off"),
0xc0e5: (MODE_READ, "phase 2 on"),
0xc0e6: (MODE_READ, "phase 3 off"),
0xc0e7: (MODE_READ, "phase 3 on"),
0xc0e8: (MODE_READ, "Drives off"),
0xc0e9: (MODE_READ, "Selected drive on"),
0xc0ea: (MODE_READ, "Select drive 1"),
0xc0eb: (MODE_READ, "Select drive 2"),
0xc0ec: (MODE_READ, "Shift while writing/read data"),
0xc0ed: (MODE_READ, "Shift while writing/read data"),
0xc0ee: (MODE_READ, "Enabling disk read mode."),
0xc0ef: (MODE_READ, "Enabling disk write mode."),
}
# Need the ability to intercept execution, e.g. for the C65C sector read routine.
def IOInterceptor(self, address, value=None):
access_mode = MODE_READ if (value == None) else MODE_WRITE
try:
(mode, result) = self.io_map[address]
if access_mode & mode:
print "==== IO EVENT: %s" % result
else:
print "**** IO EVENT with unexpected mode: %s" % result
except KeyError:
if value:
print TrapException(address, 'Wrote %02X ("%s")' % (value, chr(value)))
else:
print TrapException(address, 'Read')
def _ReadDiskSector(self, cpu):
print "Read disk sector from Track $%02X Sector $%02X" % (self.memory[0x41], self.memory[0x3d])
cpu.pc = 0x801
def _Wait(self, cpu):
print "Waiting"
# TODO: convert addresses to screen coordinates
# See e.g. https://retrocomputing.stackexchange.com/questions/2534/what-are-the-screen-holes-in-apple-ii-graphics
def TextPageWriteInterceptor(self, address, value):
print 'Wrote "%s" to text page address $%04X' % (chr(value & 0x7f), address)
def TraceWriteInterceptor(self, address, value):
print 'Wrote "%s" to address $%04X' % (chr(value), address)
def TraceReadInterceptor(self, address):
print 'Read from address $%04X' % address
def Run(self, pc):
def Run(self, pc, trace=False):
self.cpu.pc = pc
old_pc = self.cpu.pc
while True:
self.cpu.step(trace=False)
if self.cpu.pc == pc:
self.memory_manager.MaybeInterceptExecution(self.cpu, old_pc)
old_pc = self.cpu.pc
self.cpu.step(trace=trace)
if self.cpu.pc == old_pc:
break
pc = self.cpu.pc
def main():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
boot1 = [
0x01, 0x8d, 0xe8, 0xc0, 0x8d, 0x51, 0xc0, 0x8d, 0x54, 0xc0, 0xa0, 0x00, 0xa9, 0xa0, 0x99, 0x00,
0x04, 0x99, 0x00, 0x05, 0x99, 0x00, 0x06, 0x99, 0x00, 0x07, 0xc8, 0xd0, 0xf1, 0xa9, 0x08, 0x85,
0x01, 0xa9, 0x33, 0x85, 0x00, 0xb1, 0x00, 0xf0, 0x08, 0x09, 0x80, 0x99, 0xaf, 0x05, 0xc8, 0xd0,
0xec, 0xf0, 0xfe, 0x54, 0x48, 0x49, 0x53, 0x20, 0x44, 0x49, 0x53, 0x4b, 0x20, 0x48, 0x41, 0x53,
0x20, 0x4e, 0x4f, 0x20, 0x42, 0x4f, 0x4f, 0x54, 0x20, 0x43, 0x4f, 0x44, 0x45, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
# Load the most popular boot1
# q = cursor.execute(
# """
# select boot1_sha1, boot1.data, count(*) from disks
# join
# (select sha1, data from boot1) as boot1
# on disks.boot1_sha1 = boot1.sha1 group by 1 order by 3 desc limit 1
# """
# )
# Dos 3.3
q = cursor.execute(
"""
select data from boot1 where sha1 = '7ab36247fdf62e87f98d2964dd74d6572d17fff0'
"""
)
for r in q:
(boot1,) = r
#
# # boot1 image that prints stuff to the text page without using ROM entrypoints
# q = cursor.execute(
# """
# select data from boot1 where sha1 = '62bda735bcb4a27ffbd833ebb4ff2503b983ea97'
# """
# )
for r in q:
(boot1,) = r
apple2 = AppleII()
apple2.memory[0x800:0x800+len(boot1)] = boot1
apple2.Run(0x801)
# Read in Apple IIE ROM image
rom = bytearray(open("APPLE2E.ROM", "r").read())
# XXX these should write-trap
# Slot 6 ROM
apple2.memory[0xc600:0xc6ff] = rom[0x0600:0x6ff]
# Main ROM
apple2.memory[0xd000:0xffff] = rom[0x5000:0x7fff]
# TODO: why does this not use the 6502 reset vector?
apple2.cpu.reset()
apple2.memory[0x800:0x800 + len(boot1)] = bytearray(boot1)
# Disk II firmware stores next page load address here
apple2.memory[0x26] = 0x00
apple2.memory[0x27] = 0x09
# "3" is used for controller ID
apple2.memory[0x3c] = 0x03
# Sector to read
apple2.memory[0x3d] = 0x00
# Track to read
apple2.memory[0x41] = 0x00
# Booting from slot 6
apple2.memory[0x2b] = 0x60
#apple2.Run(0xfa62) # Target of 6502 reset vector
apple2.Run(0x801, trace=True)
if __name__ == '__main__':
main()
main()