diff --git a/README.md b/README.md index 6cce0f1..0da67a1 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,19 @@ Fetch the EPROM file with monitor, CP/M and BASIC, and run: ## Requirements Python, with vt102, pyelftools and hexdump modules installed. + + +### 68Katy +Steve Chamberlin's 68Katy is a board wit 512k of RAM and 512k of flash, +a serial FT245RL console. + +https://www.bigmessowires.com/68-katy/ + +Fetch the zBug monitor or zBug+uClinux image and run: + +`./py68k.py --target 68katy --rom ../68katy/monitor-plus-linux-pcb.bin` + +* zBug seems to work +* uClinux failed to boot for now : Need to fix the ticker's device andi/or interrupt handling. + + diff --git a/configs/68katy.cfg b/configs/68katy.cfg new file mode 100644 index 0000000..b8251ee --- /dev/null +++ b/configs/68katy.cfg @@ -0,0 +1,4 @@ +--target +tiny68k +--eeprom +../../68katy/monitor-plus-linux-pcb.bin diff --git a/device.py b/device.py index 8afe0b3..e6fb6d3 100644 --- a/device.py +++ b/device.py @@ -38,7 +38,9 @@ def __init__(self, args, name, required_options=None, **options): self.address = options['address'] if 'address' in options else None self.interrupt = options['interrupt'] if 'interrupt' in options else None self.size = None + self.debug = self.name in args.debug_device + self._asserted_ipl = 0 @classmethod @@ -177,7 +179,7 @@ def callback_every(self, cb_period, cb_name, cb_func): """ arrange for cb_func to be called every cb_period cycles """ - self.__add_callback(self, self.current_cycle + cp_period, cb_name, cb_func, cb_period) + self.__add_callback(self, self.current_cycle + cb_period, cb_name, cb_func, cb_period) def callback_cancel(self, cb_name): """ diff --git a/devices/compactflash.py b/devices/compactflash.py new file mode 100644 index 0000000..c38acfb --- /dev/null +++ b/devices/compactflash.py @@ -0,0 +1,352 @@ +import io +import struct +import sys + +from device import Device +from musashi import m68k + +SECTOR_SIZE = 512 + +STATUS_ERR = 0x01 +STATUS_DRQ = 0x08 +STATUS_DF = 0x20 +STATUS_DRDY = 0x40 +STATUS_BSY = 0x80 + +ERROR_ABORT = 0x04 +ERROR_ID_NOT_FOUND = 0x10 +ERROR_UNCORRECTABLE = 0x40 + +DRH_LBA_EN = 0x40 +DRH_HEAD_MASK = 0x0f + +CMD_READ_SECTORS = 0x20 +CMD_WRITE_SECTORS = 0x30 +CMD_IDENTIFY_DEVICE = 0xec + +AMODE_READ = 'R' +AMODE_WRITE = 'W' +AMODE_IDENTIFY = 'I' +AMODE_NONE = 'N' + + +class CompactFlash(Device): + """ + Memory-mapped CompactFlash emulation. + + Reference: XT13/2008D + """ + + def __init__(self, args, **options): + super(CompactFlash, self).__init__(args=args, + name='CF', + required_options=['address', 'register_arrangement'], + **options) + if options['register_arrangement'] == '16-bit': + self.add_registers([ + ('DATA16', 0x00, m68k.MEM_SIZE_16, m68k.MEM_READ, self._read_data16), + ('DATA8', 0x01, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_data8), + ('ERROR', 0x03, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_error), + ('SECTOR_COUNT', 0x05, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_sector_count), + ('SECTOR_NUMBER', 0x07, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_sector_number), + ('CYLINDER_LOW', 0x09, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_cylinder_low), + ('CYLINDER_HIGH', 0x0b, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_cylinder_high), + ('DRIVE/HEAD', 0x0d, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_drive_head), + ('STATUS', 0x0f, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_status), + + ('DATA16', 0x00, m68k.MEM_SIZE_16, m68k.MEM_WRITE, self._write_data16), + ('DATA8', 0x01, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_data8), + ('FEATURE', 0x03, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_feature), + ('SECTOR_COUNT', 0x05, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_sector_count), + ('SECTOR_NUMBER', 0x07, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_sector_number), + ('CYLINDER_LOW', 0x09, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_cylinder_low), + ('CYLINDER_HIGH', 0x0b, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_cylinder_high), + ('DRIVE/HEAD', 0x0d, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_drive_head), + ('COMMAND', 0x0f, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_command), + ]) + elif options['register_arrangement'] == '8-bit': + self.add_registers([ + ('DATA8', 0x00, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_data8), + ('ERROR', 0x01, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_error), + ('SECTOR_COUNT', 0x02, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_sector_count), + ('SECTOR_NUMBER', 0x03, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_sector_number), + ('CYLINDER_LOW', 0x04, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_cylinder_low), + ('CYLINDER_HIGH', 0x05, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_cylinder_high), + ('DRIVE/HEAD', 0x06, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_drive_head), + ('STATUS', 0x07, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_status), + + ('DATA8', 0x00, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_data8), + ('FEATURE', 0x01, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_feature), + ('SECTOR_COUNT', 0x02, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_sector_count), + ('SECTOR_NUMBER', 0x03, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_sector_number), + ('CYLINDER_LOW', 0x04, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_cylinder_low), + ('CYLINDER_HIGH', 0x05, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_cylinder_high), + ('DRIVE/HEAD', 0x06, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_drive_head), + ('COMMAND', 0x07, m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_command), + ]) + else: + raise RuntimeError(f'register_arrangement {options["register_arrangement"]} not recognized') + + # open the backing file + if args.diskfile is not None: + self._file_handle = io.open(args.diskfile, mode='r+b', buffering=0) + self._file_handle.seek(0, io.SEEK_END) + self._file_size = self._file_handle.tell() + if (self._file_size % SECTOR_SIZE) != 0: + raise RuntimeError('disk file {} size {} is not a multiple of the sector size'.format( + args.diskfile, self._file_size)) + + self._r_status = STATUS_DRDY + else: + self._file_handle = None + self._file_size = 0 + self._r_status = STATUS_DF + + drive_size = int(self._file_size / SECTOR_SIZE) + self._identify_data = struct.pack( + ''.join(('>', # little endian + 'H', # 0: general config flags + 'H', # 1: #cylinders + '2x', # 2: - + 'H', # 3: #heads + '4x', # 4-5: - + 'H', # 6: #sectors + '6x', # 7-9: - + '20s', # 10-19: serial number + '4x', # 20-21: - + 'H', # 22: vendor bytes on READ/WRITE LONG + '8s', # 23-26: firmware version + '40s', # 27-46: model number + 'H', # 47: max sectors per READ/WRITE MULTIPLE + '2x', # 48: - + 'H', # 49: capabilities + '2x', # 50: - + 'H', # 51: PIO timing mode + '2x', # 52: - + 'H', # 53: 54-58, 64-70 validity flags + '10x', # 54-58: - + 'H', # 59: max sectors / interrupt R/W multiple + 'H', # 60: number of addressible sectors (LBA:low) + 'H', # 61: number of addressible sectors (LBA:high) + '2x', # 62: - + 'H', # 63: DMA modes supported + '32x', # 64-79: - + 'H', # 80: ATA major version + 'H', # 81: ATA minor version + 'H', # 82: command sets supported + 'H', # 83: command sets supported + '88x', # 84-127: - + 'H', # 128: security status + '254x')), # 129-255: - + 0, # 0 + 16383, # 1 + 16, # 3 + 63, # 6 + '00000000'.encode('latin-1'), # 10-19 + 0, # 22 + '00000000'.encode('latin-1'), # 23-26 + 'py68k emulated CF '.encode('latin-1'), # 27-46 + 1, # 47 + 0, # 49 + 0, # 51 + 0, # 53 + 0, # 59 + drive_size & 0xffff, # 60 + drive_size >> 16, # 61 + 0, # 63 + 0, # 80 + 0, # 81 + 0, # 82 + 0, # 83 + 0) # 128 + + if len(self._identify_data) != SECTOR_SIZE: + raise RuntimeError(f'IDENTIFY DEVICE data length wrong, {len(self._identify_data)} != 512') + + self._r_error = 0 + self._r_feature = 0 + self._r_sector_count = 0 + self._r_sector_number = 0 + self._r_cylinder = 0 + self._r_drive_head = 0 + + self._current_mode = AMODE_NONE + self._bytes_remaining = 0 + + @classmethod + def add_arguments(cls, parser): + """add argument definitions for args passed to __init__""" + parser.add_argument('--diskfile', + type=str, + default=None, + help='CF disk image file') + + def _read_data16(self): + return self._io_read(m68k.MEM_SIZE_16) + + def _read_data8(self): + return self._io_read(m68k.MEM_SIZE_8) + + def _read_error(self): + return self._r_error + + def _read_sector_count(self): + return self._r_sector_count + + def _read_sector_number(self): + return self._r_sector_number + + def _read_cylinder_low(self): + return self._r_cylinder & 0xff + + def _read_cylinder_high(self): + return self._r_cylinder >> 8 + + def _read_drive_head(self): + return self._r_drive_head + + def _read_status(self): + return self._r_status + + def _write_data16(self, value): + self._io_write(m68k.MEM_SIZE_16, value) + + def _write_data8(self, value): + self._io_write(m68k.MEM_SIZE_8, value) + + def _write_feature(self, value): + self._r_feature = value + + def _write_sector_count(self, value): + self._r_sector_count = value + + def _write_sector_number(self, value): + self._r_sector_number = value + + def _write_cylinder_low(self, value): + self._r_cylinder = (self._r_cylinder & 0xff00) | value + + def _write_cylinder_high(self, value): + self._r_cylinder = (self._r_cylinder & 0x00ff) | (value << 8) + + def _write_drive_head(self, value): + self._r_drive_head = value + + def _write_command(self, value): + if value == CMD_READ_SECTORS: + self._trace_io('READ') + self._do_io(AMODE_READ) + + elif value == CMD_WRITE_SECTORS: + self._trace_io('WRITE') + self._do_io(AMODE_WRITE) + + elif value == CMD_IDENTIFY_DEVICE: + self._trace_io('IDENTIFY') + self._do_identify() + + else: + self._r_status = STATUS_ERR + self._r_error = ERROR_ABORT + self.trace(info=f'ERROR: command {value:02x} not supported') + + def _trace_io(self, action): + self.trace(info=f'{action} count {self._r_sector_count} LBA {self._r_lba}') + + def _do_io(self, mode): + # if we're in the FAULT state (no file, eg.) all I/O fails + if self._r_status & STATUS_DF: + self.trace(info=f'ERROR: no device') + self._r_status |= STATUS_ERR + self._r_error = ERROR_UNCORRECTABLE + return + + self._r_status &= ~(STATUS_ERR | STATUS_DRQ) + self._r_error = 0 + + file_byte_offset = self._r_lba * SECTOR_SIZE + self._bytes_remaining = self._r_sector_count * SECTOR_SIZE + if self._bytes_remaining == 0: + self._bytes_remaining = 256 * SECTOR_SIZE + + if (file_byte_offset + self._bytes_remaining) > self._file_size: + self.trace(info=f'ERROR: access beyond end of device') + self._r_status |= STATUS_ERR + self._r_error = ERROR_UNCORRECTABLE + + self._file_handle.seek(file_byte_offset, io.SEEK_SET) + self._r_status |= STATUS_DRQ + self._current_mode = mode + + def _do_identify(self): + self._r_status = STATUS_DRQ + self._r_error = 0 + self._bytes_remaining = SECTOR_SIZE + self._current_mode = AMODE_IDENTIFY + + def _io_read(self, width): + if self._current_mode == AMODE_IDENTIFY: + pass + elif self._current_mode == AMODE_READ: + pass + else: + self.trace(info=f'ERROR: data read when not reading / identifying') + return 0 + + if width == m68k.MEM_SIZE_8: + count = 1 + else: + count = 2 + if self._bytes_remaining < count: + self.trace(info=f'ERROR: read beyond sector buffer') + return 0 + + if self._current_mode == AMODE_READ: + data = self._file_handle.read(count) + if len(data) != count: + raise RuntimeError('unexpected disk file read error') + + self._bytes_remaining -= count + if count == 1: + return data[0] + else: + return (data[1] << 8) + data[0] + + elif self._current_mode == AMODE_IDENTIFY: + + index = SECTOR_SIZE - self._bytes_remaining + self._bytes_remaining -= count + if count == 1: + return self._identify_data[index] + else: + return (self._identify_data[index + 1] << 8) + self._identify_data[index] + + else: + raise RuntimeError('oops') + + def _io_write(self, width, value): + if self._current_mode != AMODE_WRITE: + self.trace(info=f'ERROR: data write when not writing') + return 0 + + data = bytearray() + data.append(value & 0xff) + if width == m68k.MEM_SIZE_16: + data.append(value >> 8) + + if self._bytes_remaining < len(data): + self.trace(info=f'ERROR: write beyond sector buffer') + return + + self._file_handle.write(data) + self._bytes_remaining -= len(data) + + @property + def _r_lba(self): + if self._r_drive_head & DRH_LBA_EN: + lba = self._r_sector_number + lba += self._r_cylinder << 8 + lba += (self._r_drive_head & DRH_HEAD_MASK) << 24 + return lba + + raise RuntimeError('CHS mode not supported') diff --git a/devices/ft245rl.py b/devices/ft245rl.py new file mode 100644 index 0000000..a084c26 --- /dev/null +++ b/devices/ft245rl.py @@ -0,0 +1,87 @@ +from collections import deque +import sys + +from device import Device +from musashi import m68k + + +# @see https://www.bigmessowires.com/68-katy/ +ADDR_SERIN = 0x078000 +ADDR_SEROUT = 0x07A000 +ADDR_SERSTATUS_RXF = 0x07C000 +ADDR_SERSTATUS_TXE = 0x07D000 +ADDR_DOUT = 0x07E000 + +class FT245RL(Device): + + SERSTATUS_RXF = 0b11111110 # b0 is 0 when fifo ready to read + SERSTATUS_TXE = 0b11111110 # b0 is 0 when fifo is empty + + + def __init__(self, args, **options): + super(FT245RL, self).__init__(args=args, + name='FT245RL', + required_options=['address', 'interrupt'], + **options) + + self.add_registers([ + ('SERIN', 0x00, m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_serin), + ('SERSTATUS_RXF', (ADDR_SERSTATUS_RXF - ADDR_SERIN), m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_sr_rxf), + ('SERSTATUS_TXE', (ADDR_SERSTATUS_TXE - ADDR_SERIN), m68k.MEM_SIZE_8, m68k.MEM_READ, self._read_sr_txe), + + ('SEROUT', (ADDR_SEROUT - ADDR_SERIN), m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_serout), + ('DOUT', (ADDR_DOUT - ADDR_SERIN), m68k.MEM_SIZE_8, m68k.MEM_WRITE, self._write_dout), + ]) + + self.register_console_input_handler(self._handle_console_input) + self.reset() + self.trace(info='init done') + + def reset(self): + self._rxfifo = deque() + self._vr = 0 + + def _read_sr_rxf(self): + value = 0b11111111 + if len(self._rxfifo) > 0: + value = FT245RL.SERSTATUS_RXF + return value + + def _read_sr_txe(self): + value = FT245RL.SERSTATUS_TXE + return value + + + def _read_serin(self): + if len(self._rxfifo) > 0: + return self._rxfifo.popleft() + self._update_ipl() + return 0 + + def _write_serout(self, value): + #print("OUTPUT=" + str(chr(value).encode('latin-1'))) + self.console_handle_output(chr(value).encode('latin-1')) + + def _write_dout(self, value): + self.trace(info='LED=' + str(value)) + + def get_vector(self, interrupt): + if self._vr > 0: + return self._vr + #return m68k.IRQ_AUTOVECTOR + return m68k.IRQ_SPURIOUS + + def _update_ipl(self): + if (len(self._rxfifo) > 0): + self.assert_ipl() + else: + self.deassert_ipl() + + def _handle_console_input(self, input): + for c in input: + self._rxfifo.append(c) + self._update_ipl() + + + + diff --git a/devices/ne555.py b/devices/ne555.py new file mode 100644 index 0000000..d931e71 --- /dev/null +++ b/devices/ne555.py @@ -0,0 +1,55 @@ +from collections import deque +import sys + +from device import Device +from musashi import m68k + +class NE555Ticker(Device): + def __init__(self, args, **options): + super().__init__(args=args, + name='NE555Ticker', + required_options=['interrupt'], + **options) + + # no registers, + # self.size = 0x1 + # core clock @ 24MHz, 100Hz tick rate + self._tick_cycles = int(self.emu.cycle_rate / 100) + self.reset() + self._start() + self.trace(info='init done') + + def reset(self): + self._vr = 0 + self._stop() + self._tick_fired = False + + def access(self, operation, offset, size, value): + if value > 0: + self._start() + else: + self._stop() + + def _stop(self): + self.trace(info=f'ticker stopped') + self.callback_cancel('tick') + self._ticker_on = False + + def _start(self): + if not self._ticker_on: + self.trace(info=f'ticker start every {self._tick_cycles}') + self.callback_every(self._tick_cycles, 'tick', self._tick) + self._ticker_on = True + + def _tick(self): + if self._ticker_on: + self._tick_fired = True + self.assert_ipl() + + def get_vector(self, interrupt): + if self._tick_fired: + self._tick_fired = False + if self._vr > 0: + return self._vr + return m68k.IRQ_AUTOVECTOR + return m68k.IRQ_SPURIOUS diff --git a/targets/68katy.py b/targets/68katy.py new file mode 100644 index 0000000..e11176d --- /dev/null +++ b/targets/68katy.py @@ -0,0 +1,52 @@ +from emulator import Emulator +#from devices.compactflash import CompactFlash +from devices.ne555 import NE555Ticker +from devices.ft245rl import FT245RL +from musashi import m68k + + +# @see https://www.bigmessowires.com/68-katy/ + +# /> cat /proc/interrupts +# auto 2: 21 L FT245 console +# auto 5: 6017 L timer +# auto 7: 0 L timer-and-serial +# 68000 autovector interrupts + +rom_base = 0 +rom_size = 0x077FFF + 1 # 512 * 1024 but not all the flash is decoded + +ram_base = 0x080000 +ram_size = 1024 * 512 # 0x0FFFFF - 0x080000 + 1 # 512 * 1024 + +io_base = 0x078000 + + +def add_arguments(parser): + parser.add_argument('--rom', + type=str, + help='ROM image') + #CompactFlash.add_arguments(parser) + + NE555Ticker.add_arguments(parser) + FT245RL.add_arguments(parser) + + +def configure(args): + """create and configure an emulator""" + + emu = Emulator(args, + cpu='68000', + frequency=8 * 1000 * 1000) + + emu.add_memory(base=rom_base, size=rom_size, writable=False, from_file=args.rom) + emu.add_memory(base=ram_base, size=ram_size) + emu.add_device(args, + NE555Ticker, + interrupt=m68k.IRQ_5) + emu.add_device(args, + FT245RL, + address=io_base, + interrupt=m68k.IRQ_2) + + return emu