From 48b77deda38a345ceb81007061c4aedce0c9e656 Mon Sep 17 00:00:00 2001 From: Rob McMullen Date: Tue, 9 May 2017 17:53:28 -0700 Subject: [PATCH 1/5] Added standard delivery boot loader --- README.rst | 1 + atrcopy/parsers.py | 12 +++ atrcopy/standard_delivery.py | 149 +++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 atrcopy/standard_delivery.py diff --git a/README.rst b/README.rst index 0b19f93..4ad05df 100644 --- a/README.rst +++ b/README.rst @@ -453,3 +453,4 @@ Turns out there are a ton of Apple ][ disk imaging tools! I was pointed to the l * `dos33fsprogs `_ (C) * `apple2-disk-util `_ (Ruby) * `dsk2nib `_ (C) +* `standard-delivery `_ (6502 assembly) Apple II single-sector fast boot-loader diff --git a/atrcopy/parsers.py b/atrcopy/parsers.py index ec42a8a..6e3bfd7 100644 --- a/atrcopy/parsers.py +++ b/atrcopy/parsers.py @@ -10,6 +10,7 @@ from .spartados import SpartaDosDiskImage from .cartridge import AtariCartImage, get_known_carts from .mame import MameZipImage from .dos33 import Dos33DiskImage, ProdosDiskImage, Dos33BinFile +from .standard_delivery import StandardDeliveryImage from .errors import * import logging @@ -174,6 +175,17 @@ def iter_parsers(r): return None, None +def parsers_for_filename(name): + matches = [] + for mime in mime_parse_order: + parsers = mime_parsers[mime] + found = None + for parser in parsers: + print("parser: %s = %s" % (mime, parser)) + matches.append(StandardDeliveryImage) + return matches + + mime_parsers = { "application/vnd.atari8bit.atr": [ KBootSegmentParser, diff --git a/atrcopy/standard_delivery.py b/atrcopy/standard_delivery.py new file mode 100644 index 0000000..bf1459e --- /dev/null +++ b/atrcopy/standard_delivery.py @@ -0,0 +1,149 @@ +from __future__ import absolute_import +from __future__ import division +import numpy as np + +from .errors import * +from .segments import SegmentData +from .diskimages import BaseHeader, DiskImageBase + +import logging +log = logging.getLogger(__name__) + + +class StandardDeliveryHeader(BaseHeader): + file_format = "Apple ][ Standard Delivery" + + def __init__(self, bytes=None, sector_size=256, create=False): + BaseHeader.__init__(self, sector_size, create=create) + if bytes is None: + return + + data = bytes[0:5] + if np.all(data == (0x01, 0xa8, 0xee, 0x06, 0x08)): + log.debug("Found 48k loader") + else: + raise InvalidDiskImage("No %s boot header" % self.file_format) + + def __str__(self): + return "Standard Delivery Boot Disk (size=%d (%dx%dB)" % (self.file_format, self.image_size, self.max_sectors, self.sector_size) + + def check_size(self, size): + if size != 143360: + raise InvalidDiskImage("Incorrect size for Standard Delivery image") + self.image_size = size + self.tracks_per_disk = 35 + self.sectors_per_track = 16 + self.max_sectors = self.tracks_per_disk * self.sectors_per_track + + +class StandardDeliveryImage(DiskImageBase): + def __str__(self): + return str(self.header) + + def read_header(self): + self.header = StandardDeliveryHeader(self.bytes[0:256]) + + @classmethod + def new_header(cls, diskimage, format="DSK"): + if format.lower() == "dsk": + header = StandardDeliveryHeader(create=True) + header.check_size(diskimage.size) + else: + raise RuntimeError("Unknown header type %s" % format) + return header + + def check_size(self): + pass + + def get_boot_sector_info(self): + pass + + def get_vtoc(self): + pass + + def get_directory(self, directory=None): + pass + + @classmethod + def create_boot_image(cls, segments, run_addr=None): + raw = SegmentData(np.zeros([143360], dtype=np.uint8)) + dsk = cls(raw, create=True) + if run_addr is None: + run_addr = segments[0].start_addr + + chunks = [] + + for s in segments: + # find size in 256 byte chunks that start on a page boundary + # since the loader only deals with page boundaries + origin = s.start_addr + chunk_start, padding = divmod(origin, 256) + size = ((len(s) + padding + 255) // 256) * 256 + chunk = np.zeros([size], dtype=np.uint8) + chunk[padding:padding + len(s)] = s[:] + chunks.append((chunk_start, chunk)) + print("segment: %s, chunk=%s" % (str(s), str(chunks[-1]))) + + # break up the chunks into sectors + + # NOTE: fstbt implied that the sector order was staggered, but in + # AppleWin, by trial and error, works with the following order + + # index = 1 # on the first track, sector 0 is reserved for boot sector + # sector_order = [0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15] + + index = 1 + sector_order = [0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 15] + track = 0 + count = 0 + + sector_list = [] + address_list = [] + + boot_sector = dsk.header.create_sector() + boot_sector.sector_num = 0 + boot_sector.track_num = 1 + sector_list.append(boot_sector) + + for chunk_start, chunk_data in chunks: + i = 0 + hi = chunk_start + while i < len(chunk_data): + sector = dsk.header.create_sector(chunk_data[i:i+256]) + sector.sector_num = dsk.header.sector_from_track(track, sector_order[index]) + count += 1 + #sector.sector_num = count + sector_list.append(sector) + address_list.append(hi) + # sector.data[0] = sector.sector_num + # sector.data[1] = hi + # sector.data[2:16] = 0xff + print("%s at %02x00: %s ..." % (sector_list[-1], address_list[-1], " ".join(["%02x" % h for h in chunk_data[i:i + 16]]))) + index += 1 + if index >= len(sector_order): + index = 0 + track += 1 + i += 256 + hi += 1 + + print("address list %s" % str(address_list)) + boot_code = get_fstbt_code(boot_sector.data, address_list, run_addr) + + dsk.write_sector_list(sector_list) + + return dsk + + +# fast boot code minus final jump address and sector address table +fstbt_64k = b'\x01\xa8\xee\x06\x08\xadM\x08\xc9\xc0\xf0?\x85\x27\xc8\xc0\x10\x90\t\xf0\x05 .\x08\xa8,\xa0\x01\x84=\xc8\xa5\x27\xf0\xdf\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ 6\x08\x18 ;\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL' + +fstbt_64k_hgr = b'\x01\xa8,P\xc0,R\xc0,W\xc0\xee\x0f\x08\xadV\x08\xc9\xc0\xf0?\x85\x27\xc8\xc0\x10\x90\t\xf0\x05 7\x08\xa8,\xa0\x01\x84=\xc8\xa5\x27\xf0\xdf\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ ?\x08\x18 D\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL' + +def get_fstbt_code(data, address_list, run_addr): + code = fstbt_64k_hgr + pointer = len(code) + data[0:pointer] = np.fromstring(code, dtype=np.uint8) + hi, lo = divmod(run_addr, 256) + data[pointer:pointer + 2] = (lo, hi) + address_list.append(0xc0) # last sector flag + data[pointer + 2:pointer + 2 + len(address_list)] = address_list From 0d7645f2439cd651fddec3244b3a91ec97133bad Mon Sep 17 00:00:00 2001 From: Rob McMullen Date: Tue, 9 May 2017 17:58:16 -0700 Subject: [PATCH 2/5] Added framework and 'boot' command to create boot images --- atrcopy/__init__.py | 30 ++++++++++++++++++++++++++++-- atrcopy/diskimages.py | 13 ++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/atrcopy/__init__.py b/atrcopy/__init__.py index 3eeafba..1a590c1 100644 --- a/atrcopy/__init__.py +++ b/atrcopy/__init__.py @@ -22,7 +22,7 @@ from .kboot import KBootImage, add_xexboot_header from .segments import SegmentData, SegmentSaver, DefaultSegment, EmptySegment, ObjSegment, RawSectorsSegment, SegmentedFileSegment, user_bit_mask, match_bit_mask, comment_bit_mask, data_style, selected_bit_mask, diff_bit_mask, not_user_bit_mask, interleave_segments, SegmentList, get_style_mask, get_style_bits from .spartados import SpartaDosDiskImage from .cartridge import A8CartHeader, AtariCartImage -from .parsers import SegmentParser, DefaultSegmentParser, guess_parser_for_mime, guess_parser_for_system, iter_parsers, iter_known_segment_parsers, mime_parse_order +from .parsers import SegmentParser, DefaultSegmentParser, guess_parser_for_mime, guess_parser_for_system, iter_parsers, iter_known_segment_parsers, mime_parse_order, parsers_for_filename from .utils import to_numpy, text_to_int @@ -178,7 +178,7 @@ def crc_files(image, files): print("%s: %08x" % (dirent.filename, crc)) -def assemble(image, source_files, data_files, obj_files, run_addr=""): +def assemble_segments(source_files, data_files, obj_files, run_addr=""): if source_files: try: import pyatasm @@ -238,6 +238,10 @@ def assemble(image, source_files, data_files, obj_files, run_addr=""): except ValueError: run_addr = None + return segments, run_addr + +def assemble(image, source_files, data_files, obj_files, run_addr=""): + segments, run_addr = assemble_segments(source_files, data_files, obj_files, run_addr) file_data, filetype = image.create_executable_file_image(segments, run_addr) print("total file size: $%x (%d) bytes" % (len(file_data), len(file_data))) changed = save_file(image, options.output, filetype, file_data) @@ -245,6 +249,14 @@ def assemble(image, source_files, data_files, obj_files, run_addr=""): image.save() +def boot_image(image_name, source_files, data_files, obj_files, run_addr=""): + image_cls = parsers_for_filename(image_name)[0] + segments, run_addr = assemble_segments(source_files, data_files, obj_files, run_addr) + image = image_cls.create_boot_image(segments, run_addr) + print("saving boot disk %s" % (image_name)) + image.save(image_name) + + def shred_image(image, value=0): print("shredding: free sectors from %s filled with %d" % (image, value)) if not options.dry_run: @@ -373,6 +385,7 @@ def run(): "extract": ["x"], "add": ["a"], "create": ["c"], + "boot": ["b"], "assemble": ["s", "asm"], "delete": ["rm", "del"], "vtoc": ["v"], @@ -434,6 +447,14 @@ def run(): assembly_parser.add_argument("-r", "--run-addr", "--brun", action="store", default="", help="run address of binary file if not the first byte of the first segment") assembly_parser.add_argument("-o", "--output", action="store", default="", required=True, help="output file name in disk image") + command = "boot" + boot_parser = subparsers.add_parser(command, help="Create a bootable disk image", aliases=command_aliases[command]) + boot_parser.add_argument("-f", "--force", action="store_true", default=False, help="allow file overwrites in the disk image") + boot_parser.add_argument("-s", "--asm", nargs="*", action="append", help="source file(s) to assemble using pyatasm") + boot_parser.add_argument("-d","--data", nargs="*", action="append", help="binary data file(s) to add to assembly, specify as file@addr. Only a portion of the file may be included; specify the subset using standard python slice notation: file[subset]@addr") + boot_parser.add_argument("-b", "--obj", "--bload", nargs="*", action="append", help="binary file(s) to add to assembly, either executables or labeled memory dumps (e.g. BSAVE on Apple ][), parsing each file's binary segments to add to the resulting disk image at the load address for each segment") + boot_parser.add_argument("-r", "--run-addr", "--brun", action="store", default="", help="run address of binary file if not the first byte of the first segment") + command = "delete" delete_parser = subparsers.add_parser(command, help="Delete files from the disk image", aliases=command_aliases[command]) delete_parser.add_argument("-f", "--force", action="store_true", default=False, help="remove the file even if it is write protected ('locked' in Atari DOS 2 terms), if write-protect is supported disk image") @@ -511,6 +532,11 @@ def run(): if command == "create": create_image(options.template[0], disk_image_name) + elif command == "boot": + asm = options.asm[0] if options.asm else [] + data = options.data[0] if options.data else [] + obj = options.obj[0] if options.obj else [] + boot_image(disk_image_name, asm, data, obj, options.run_addr) else: parser = find_diskimage(disk_image_name) if parser and parser.image: diff --git a/atrcopy/diskimages.py b/atrcopy/diskimages.py index 26dd1a7..6ba9828 100644 --- a/atrcopy/diskimages.py +++ b/atrcopy/diskimages.py @@ -20,7 +20,7 @@ class BaseHeader(object): file_format = "generic" # text descriptor of file format sector_class = WriteableSector - def __init__(self, sector_size=256, initial_sectors=0, vtoc_sector=0, starting_sector_label=0): + def __init__(self, sector_size=256, initial_sectors=0, vtoc_sector=0, starting_sector_label=0, create=False): self.image_size = 0 self.sector_size = sector_size self.payload_bytes = sector_size @@ -96,7 +96,7 @@ class BaseHeader(object): class DiskImageBase(object): - def __init__(self, rawdata, filename=""): + def __init__(self, rawdata, filename="", create=False): self.rawdata = rawdata self.bytes = self.rawdata.get_data() self.style = self.rawdata.get_style() @@ -109,7 +109,10 @@ class DiskImageBase(object): self.segments = [] self.all_sane = True self.default_filetype = "" - self.setup() + if create: + self.header = self.new_header(self) + else: + self.setup() def __len__(self): return len(self.rawdata) @@ -307,6 +310,10 @@ class DiskImageBase(object): def create_executable_file_image(self, segments, run_addr=None): raise NotImplementedError + @classmethod + def create_boot_image(self, segments, run_addr=None): + raise NotImplementedError + # file writing methods def begin_transaction(self): From 38b05e6dbd908a2be4513829422392fd521dc7ba Mon Sep 17 00:00:00 2001 From: Rob McMullen Date: Wed, 10 May 2017 05:18:46 -0700 Subject: [PATCH 3/5] Added trigger version of fstbt that uses dummy values of d1 and d2 to set hgr1 and hgr2 --- atrcopy/standard_delivery.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/atrcopy/standard_delivery.py b/atrcopy/standard_delivery.py index bf1459e..0e67f64 100644 --- a/atrcopy/standard_delivery.py +++ b/atrcopy/standard_delivery.py @@ -98,7 +98,7 @@ class StandardDeliveryImage(DiskImageBase): count = 0 sector_list = [] - address_list = [] + address_list = [0xd0] boot_sector = dsk.header.create_sector() boot_sector.sector_num = 0 @@ -125,6 +125,7 @@ class StandardDeliveryImage(DiskImageBase): track += 1 i += 256 hi += 1 + address_list.append(0xd0) print("address list %s" % str(address_list)) boot_code = get_fstbt_code(boot_sector.data, address_list, run_addr) @@ -139,8 +140,10 @@ fstbt_64k = b'\x01\xa8\xee\x06\x08\xadM\x08\xc9\xc0\xf0?\x85\x27\xc8\xc0\x10\x90 fstbt_64k_hgr = b'\x01\xa8,P\xc0,R\xc0,W\xc0\xee\x0f\x08\xadV\x08\xc9\xc0\xf0?\x85\x27\xc8\xc0\x10\x90\t\xf0\x05 7\x08\xa8,\xa0\x01\x84=\xc8\xa5\x27\xf0\xdf\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ ?\x08\x18 D\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL' +fstbt_64k_trigger = b'\x01\xa8\x8dP\xc0\x8dR\xc0\x8dW\xc0\xee\x0f\x08\xadl\x08\xc9\xc0\xf0U\xc9\xd0\xf0\xf2\xc9\xd1\xd0\x05\x8dT\xc0\xf0\xe9\xc9\xd2\xd0\x05\x8dU\xc0\xf0\xe0\x85\x27\xc8\xc0\x10\x90\t\xf0\x05 M\x08\xa8,\xa0\x01\x84=\xc8\xa5\x27\xf0\xc9\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ U\x08\x18 Z\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL' + def get_fstbt_code(data, address_list, run_addr): - code = fstbt_64k_hgr + code = fstbt_64k_trigger pointer = len(code) data[0:pointer] = np.fromstring(code, dtype=np.uint8) hi, lo = divmod(run_addr, 256) From dc2224a191374da2e9b59a0889d63c1c8a876af7 Mon Sep 17 00:00:00 2001 From: Rob McMullen Date: Wed, 10 May 2017 05:44:47 -0700 Subject: [PATCH 4/5] Added interesting load effect for first HGR screen --- atrcopy/standard_delivery.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/atrcopy/standard_delivery.py b/atrcopy/standard_delivery.py index 0e67f64..1ecacfe 100644 --- a/atrcopy/standard_delivery.py +++ b/atrcopy/standard_delivery.py @@ -98,7 +98,7 @@ class StandardDeliveryImage(DiskImageBase): count = 0 sector_list = [] - address_list = [0xd0] + address_list = [0xd1] boot_sector = dsk.header.create_sector() boot_sector.sector_num = 0 @@ -106,15 +106,21 @@ class StandardDeliveryImage(DiskImageBase): sector_list.append(boot_sector) for chunk_start, chunk_data in chunks: - i = 0 - hi = chunk_start - while i < len(chunk_data): + count = len(chunk_data) // 256 + if chunk_start == 0x20 and count == 32: + # Assume this is an HGR screen, use interesting load effect, + # not the usual venetian blind + chunk_hi = [0x20, 0x24, 0x28, 0x2c, 0x30, 0x34, 0x38, 0x3c, 0x21, 0x25, 0x29, 0x2d, 0x31, 0x35, 0x39, 0x3d, 0x22, 0x26, 0x2a, 0x2e, 0x32, 0x36, 0x3a, 0x3e, 0x23, 0x27, 0x2b, 0x2f, 0x33, 0x37, 0x3b, 0x3f] + else: + chunk_hi = range(chunk_start, chunk_start + count) + for n in range(count): + i = (chunk_hi[n] - chunk_start) * 256 sector = dsk.header.create_sector(chunk_data[i:i+256]) sector.sector_num = dsk.header.sector_from_track(track, sector_order[index]) count += 1 #sector.sector_num = count sector_list.append(sector) - address_list.append(hi) + address_list.append(chunk_hi[n]) # sector.data[0] = sector.sector_num # sector.data[1] = hi # sector.data[2:16] = 0xff @@ -123,9 +129,8 @@ class StandardDeliveryImage(DiskImageBase): if index >= len(sector_order): index = 0 track += 1 - i += 256 - hi += 1 - address_list.append(0xd0) + if chunk_start == 0x40: + address_list.append(0xd2) print("address list %s" % str(address_list)) boot_code = get_fstbt_code(boot_sector.data, address_list, run_addr) From b7f7a7dd8122e01e3285d57f449179aad23069d6 Mon Sep 17 00:00:00 2001 From: Rob McMullen Date: Wed, 10 May 2017 15:16:02 -0700 Subject: [PATCH 5/5] Moved fstbt code into separate file that can be autogenerated --- atrcopy/fstbt.py | 4 ++++ atrcopy/standard_delivery.py | 13 +++---------- 2 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 atrcopy/fstbt.py diff --git a/atrcopy/fstbt.py b/atrcopy/fstbt.py new file mode 100644 index 0000000..25943f5 --- /dev/null +++ b/atrcopy/fstbt.py @@ -0,0 +1,4 @@ +# generated file - recompile fstbt.s to make changes + +# append jump target (lo, hi) and sector address list before saving to disk +fstbt = b"\x01\xa8\x8dP\xc0\x8dR\xc0\x8dW\xc0\xee\x0f\x08\xadl\x08\xc9\xc0\xf0U\xc9\xd0\xf0\xf2\xc9\xd1\xd0\x05\x8dT\xc0\xf0\xe9\xc9\xd2\xd0\x05\x8dU\xc0\xf0\xe0\x85'\xc8\xc0\x10\x90\t\xf0\x05 M\x08\xa8,\xa0\x01\x84=\xc8\xa5'\xf0\xc9\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ U\x08\x18 Z\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL" diff --git a/atrcopy/standard_delivery.py b/atrcopy/standard_delivery.py index 1ecacfe..43a06d6 100644 --- a/atrcopy/standard_delivery.py +++ b/atrcopy/standard_delivery.py @@ -139,18 +139,11 @@ class StandardDeliveryImage(DiskImageBase): return dsk - -# fast boot code minus final jump address and sector address table -fstbt_64k = b'\x01\xa8\xee\x06\x08\xadM\x08\xc9\xc0\xf0?\x85\x27\xc8\xc0\x10\x90\t\xf0\x05 .\x08\xa8,\xa0\x01\x84=\xc8\xa5\x27\xf0\xdf\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ 6\x08\x18 ;\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL' - -fstbt_64k_hgr = b'\x01\xa8,P\xc0,R\xc0,W\xc0\xee\x0f\x08\xadV\x08\xc9\xc0\xf0?\x85\x27\xc8\xc0\x10\x90\t\xf0\x05 7\x08\xa8,\xa0\x01\x84=\xc8\xa5\x27\xf0\xdf\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ ?\x08\x18 D\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL' - -fstbt_64k_trigger = b'\x01\xa8\x8dP\xc0\x8dR\xc0\x8dW\xc0\xee\x0f\x08\xadl\x08\xc9\xc0\xf0U\xc9\xd0\xf0\xf2\xc9\xd1\xd0\x05\x8dT\xc0\xf0\xe9\xc9\xd2\xd0\x05\x8dU\xc0\xf0\xe0\x85\x27\xc8\xc0\x10\x90\t\xf0\x05 M\x08\xa8,\xa0\x01\x84=\xc8\xa5\x27\xf0\xc9\x8a {\xf8\t\xc0H\xa9[H`\xe6A\x06@ U\x08\x18 Z\x08\xe6@\xa5@)\x03*\x05+\xa8\xb9\x80\xc0\xa90L\xa8\xfcL' +from . fstbt import fstbt def get_fstbt_code(data, address_list, run_addr): - code = fstbt_64k_trigger - pointer = len(code) - data[0:pointer] = np.fromstring(code, dtype=np.uint8) + pointer = len(fstbt) + data[0:pointer] = np.fromstring(fstbt, dtype=np.uint8) hi, lo = divmod(run_addr, 256) data[pointer:pointer + 2] = (lo, hi) address_list.append(0xc0) # last sector flag