瀏覽代碼

AmigaDOS: Allow read/write/verify of ADF images files.

Keir Fraser 4 年之前
父節點
當前提交
41083c8db0

+ 33 - 18
scripts/greaseweazle/bitcell.py

@@ -5,46 +5,56 @@
 # This is free and unencumbered software released into the public domain.
 # See the file COPYING for more details, or visit <http://unlicense.org>.
 
-import binascii, itertools
+import binascii
+import itertools as it
 from bitarray import bitarray
 
 class Bitcell:
 
-    def __init__(self):
-        self.clock = 2 / 1000000
+    def __init__(self, clock = 2e-6, flux = None):
+        self.clock = clock
         self.clock_max_adj = 0.10
         self.pll_period_adj = 0.05
         self.pll_phase_adj = 0.60
+        self.bitarray = bitarray(endian='big')
+        self.timearray = []
+        self.revolutions = []
+        if flux is not None:
+            self.from_flux(flux)
 
+        
     def __str__(self):
         s = ""
-        rev = 0
-        for b, _ in self.revolution_list:
+        for rev in range(len(self.revolutions)):
+            b, _ = self.get_revolution(rev)
             s += "Revolution %u: " % rev
             s += str(binascii.hexlify(b.tobytes())) + "\n"
-            rev += 1
         return s[:-1]
 
+    
+    def get_revolution(self, nr):
+        start = sum(self.revolutions[:nr])
+        end = start + self.revolutions[nr]
+        return self.bitarray[start:end], self.timearray[start:end]
+
+    
     def from_flux(self, flux):
 
-        index_list, freq = flux.index_list, flux.sample_freq
+        freq = flux.sample_freq
 
         clock = self.clock
         clock_min = self.clock * (1 - self.clock_max_adj)
         clock_max = self.clock * (1 + self.clock_max_adj)
         ticks = 0.0
 
-        # Per-revolution list of bitcells and bitcell times.
-        self.revolution_list = []
+        index_iter = iter(map(lambda x: x/freq, flux.index_list))
 
-        # Initialise bitcell lists for the first revolution.
         bits, times = bitarray(endian='big'), []
-        to_index = index_list[0] / freq
-        index_list = index_list[1:]
+        to_index = next(index_iter)
 
         # Make sure there's enough time in the flux list to cover all
         # revolutions by appending a "large enough" final flux value.
-        for x in itertools.chain(flux.list, [sum(flux.index_list)]):
+        for x in it.chain(flux.list, [sum(flux.index_list)]):
 
             # Gather enough ticks to generate at least one bitcell.
             ticks += x / freq
@@ -58,12 +68,15 @@ class Bitcell:
                 # Check if we cross the index mark.
                 to_index -= clock
                 if to_index < 0:
-                    self.revolution_list.append((bits, times))
-                    if not index_list:
+                    self.bitarray += bits
+                    self.timearray += times
+                    self.revolutions.append(len(times))
+                    assert len(times) == len(bits)
+                    try:
+                        to_index += next(index_iter)
+                    except StopIteration:
                         return
                     bits, times = bitarray(endian='big'), []
-                    to_index = index_list[0] / freq
-                    index_list = index_list[1:]
 
                 ticks -= clock
                 times.append(clock)
@@ -88,7 +101,9 @@ class Bitcell:
             times[-1] += ticks - new_ticks
             ticks = new_ticks
 
-        self.revolution_list.append((bits, times))
+        # We can't get here: We should run out of indexes before we run
+        # out of flux.
+        assert False
 
 # Local variables:
 # python-indent: 4

+ 151 - 0
scripts/greaseweazle/codec/amiga/amigados.py

@@ -0,0 +1,151 @@
+# greaseweazle/codec/amiga/amigados.py
+#
+# Written & released by Keir Fraser <keir.xen@gmail.com>
+#
+# This is free and unencumbered software released into the public domain.
+# See the file COPYING for more details, or visit <http://unlicense.org>.
+
+import struct
+import itertools as it
+from bitarray import bitarray
+
+from greaseweazle.bitcell import Bitcell
+
+sync_bytes = b'\x44\x89\x44\x89'
+sync = bitarray(endian='big')
+sync.frombytes(sync_bytes)
+
+class AmigaDOS:
+
+    DDSEC = 11
+
+    def __init__(self, tracknr, nsec=DDSEC):
+        self.tracknr = tracknr
+        self.nsec = nsec
+        self.sector = [None] * nsec
+        self.map = [None] * nsec
+
+    def exists(self, sec_id, togo):
+        return ((self.sector[sec_id] is not None)
+                or (self.map[self.nsec-togo] is not None))
+
+    def nr_missing(self):
+        return len([sec for sec in self.sector if sec is None])
+
+    def add(self, sec_id, togo, label, data):
+        assert not self.exists(sec_id, togo)
+        self.sector[sec_id] = label, data
+        self.map[self.nsec-togo] = sec_id
+
+    def get_adf_track(self):
+        tdat = bytearray()
+        for sec in self.sector:
+            tdat += sec[1] if sec is not None else bytes([0] * 512)
+        return tdat
+
+    def set_adf_track(self, tdat):
+        self.map = list(range(self.nsec))
+        for sec in self.map:
+            self.sector[sec] = bytes([0x00] * 16), tdat[sec*512:(sec+1)*512]
+
+    def flux_for_writeout(self):
+        return self.flux()
+
+    def flux(self):
+        return self
+
+    def bits(self):
+        next_bad_sec_id = 0
+        t = bytearray(64)
+        for nr in range(self.nsec):
+            sec_id = self.map[nr]
+            if sec_id is None:
+                while self.sector[next_bad_sec_id] is not None:
+                    next_bad_sec_id += 1
+                sec_id = next_bad_sec_id
+                label, data = bytes([0x00] * 16), bytes([0x00] * 512)
+            else:
+                label, data = self.sector[sec_id]
+            t += sync_bytes
+            header = bytes([0xff, self.tracknr, sec_id, self.nsec-nr])
+            t += encode(header)
+            t += encode(label)
+            t += encode(struct.pack('>I', checksum(header + label)))
+            t += encode(struct.pack('>I', checksum(data)))
+            t += encode(data)
+            t += encode([0x00] * 2)
+        tlen = 101376 if self.nsec == 11 else 202752
+        t += bytes(tlen//8-len(t))
+        return mfm_encode(t)
+
+    def verify_track(self, flux):
+        cyl = self.tracknr // 2
+        head = self.tracknr & 1
+        readback_track = decode_track(cyl, head, flux)
+        return readback_track.nr_missing() == 0
+
+def mfm_encode(dat):
+    y = 0
+    out = bytearray()
+    for x in dat:
+        y = (y<<8) | x
+        if (x & 0xaa) == 0:
+            y |= ~((y>>1)|(y<<1)) & 0xaaaa
+        y &= 255
+        out.append(y)
+    return bytes(out)
+    
+def encode(dat):
+    return bytes(it.chain(map(lambda x: (x >> 1) & 0x55, dat),
+                          map(lambda x: x & 0x55, dat)))
+
+def decode(dat):
+    length = len(dat)//2
+    return bytes(map(lambda x, y: (x << 1 & 0xaa) | (y & 0x55),
+                     it.islice(dat, 0, length),
+                     it.islice(dat, length, None)))
+
+def checksum(dat):
+    csum = 0
+    for i in range(0, len(dat), 4):
+        csum ^= struct.unpack('>I', dat[i:i+4])[0]
+    return (csum ^ (csum>>1)) & 0x55555555
+
+def decode_track(cyl, head, flux):
+
+    bc = Bitcell(clock = 2e-6, flux = flux)
+    bits, times = bc.bitarray, bc.timearray
+    tracknr = cyl*2 + head
+    ados = AmigaDOS(tracknr)
+    
+    sectors = bits.search(sync)
+    for offs in bits.itersearch(sync):
+        sec = bits[offs:offs+544*16].tobytes()
+        header = decode(sec[4:12])
+        format, track, sec_id, togo = tuple(header)
+        if format != 0xff or track != tracknr \
+           or not(sec_id < ados.nsec and 0 < togo <= ados.nsec) \
+           or ados.exists(sec_id, togo):
+            continue
+
+        label = decode(sec[12:44])
+        hsum, = struct.unpack('>I', decode(sec[44:52]))
+        if hsum != checksum(header + label):
+            continue
+
+        dsum, = struct.unpack('>I', decode(sec[52:60]))
+        data = decode(sec[60:1084])
+        gap = decode(sec[1084:1088])
+        if dsum != checksum(data):
+            continue;
+
+        ados.add(sec_id, togo, label, data)
+        if ados.nr_missing() == 0:
+            break
+
+    return ados
+
+
+# Local variables:
+# python-indent: 4
+# End:

+ 12 - 0
scripts/greaseweazle/flux.py

@@ -25,8 +25,20 @@ class Flux:
 
 
     def flux_for_writeout(self):
+        return self.flux()
+
+    def flux(self):
         return self
 
+    def scale(self, factor):
+        """Scale up all flux and index timings by specified factor."""
+        self.sample_freq /= factor
+
+    @property
+    def mean_index_time(self):
+        """Mean time between index pulses, in seconds (float)"""
+        return sum(self.index_list) / (len(self.index_list) * self.sample_freq)
+
  
 # Local variables:
 # python-indent: 4

+ 93 - 0
scripts/greaseweazle/image/adf.py

@@ -0,0 +1,93 @@
+# greaseweazle/image/adf.py
+#
+# Written & released by Keir Fraser <keir.xen@gmail.com>
+#
+# This is free and unencumbered software released into the public domain.
+# See the file COPYING for more details, or visit <http://unlicense.org>.
+
+import struct
+
+from greaseweazle import error
+from greaseweazle.track import MasterTrack
+from greaseweazle.bitcell import Bitcell
+import greaseweazle.codec.amiga.amigados as amigados
+from bitarray import bitarray
+
+class ADF:
+
+    default_format = 'amiga.amigados'
+
+    def __init__(self, start_cyl, nr_sides):
+        error.check(nr_sides == 2, "ADF: Must be double-sided")
+        self.bitrate = 253
+        self.sec_per_track = 11
+        self.track_list = [None] * start_cyl
+
+
+    @classmethod
+    def to_file(cls, start_cyl, nr_sides):
+        adf = cls(start_cyl, nr_sides)
+        return adf
+
+
+    @classmethod
+    def from_file(cls, dat):
+
+        adf = cls(0, 2)
+
+        nsec = adf.sec_per_track
+        error.check((len(dat) % (2*nsec*512)) == 0, "Bad ADF image")
+        ncyl = len(dat) // (2*nsec*512)
+        if ncyl > 90:
+            ncyl //= 2
+            nsec *= 2
+            adf.bitrate *= 2
+            adf.sec_per_track = nsec
+
+        for i in range(ncyl*2):
+            ados = amigados.AmigaDOS(tracknr=i, nsec=nsec)
+            ados.set_adf_track(dat[i*nsec*512:(i+1)*nsec*512])
+            adf.track_list.append(ados)
+
+        return adf
+
+
+    def get_track(self, cyl, side, writeout=False):
+        off = cyl * 2 + side
+        if off >= len(self.track_list):
+            return None
+        rawbytes = self.track_list[off].bits()
+        tdat = bitarray(endian='big')
+        tdat.frombytes(rawbytes)
+        track = MasterTrack(
+            bits = tdat,
+            time_per_rev = 0.2)
+        track.verify = self.track_list[off]
+        return track
+
+
+    def append_track(self, track):
+        
+        self.track_list.append(track)
+
+
+    def get_image(self):
+
+        tlen = self.sec_per_track * 512
+        tdat = bytearray()
+
+        for t in self.track_list:
+            if t is None or not hasattr(t, 'get_adf_track'):
+                tdat += bytes(tlen)
+            else:
+                tdat += t.get_adf_track()
+
+        if len(self.track_list) < 160:
+            tdat += bytes(tlen * (160 - len(self.track_list)))
+
+        return tdat
+
+
+# Local variables:
+# python-indent: 4
+# End:

+ 2 - 4
scripts/greaseweazle/image/hfe.py

@@ -77,10 +77,8 @@ class HFE:
 
 
     def append_track(self, flux):
-        bc = Bitcell()
-        bc.clock = 0.0005 / self.bitrate
-        bc.from_flux(flux)
-        bits = bc.revolution_list[0][0]
+        bc = Bitcell(clock = 5e-4 / self.bitrate, flux = flux)
+        bits, _ = bc.get_revolution(0)
         bits.bytereverse()
         self.track_list.append((len(bits), bits.tobytes()))
 

+ 5 - 1
scripts/greaseweazle/tools/erase.py

@@ -50,7 +50,11 @@ def main(argv):
 
     try:
         usb = util.usb_open(args.device)
-        util.with_drive_selected(erase, usb, args)
+        try:
+            util.with_drive_selected(erase, usb, args)
+        except:
+            print()
+            raise
     except USB.CmdError as error:
         print("Command Failed: %s" % error)
 

+ 17 - 3
scripts/greaseweazle/tools/read.py

@@ -10,6 +10,7 @@
 description = "Read a disk to the specified image file."
 
 import sys
+import importlib
 
 from greaseweazle.tools import util
 from greaseweazle import error
@@ -59,7 +60,7 @@ def normalise_rpm(flux, rpm):
     return Flux([norm_to_index]*len(flux.index_list), norm_flux, freq)
 
 
-def read_to_image(usb, args, image):
+def read_to_image(usb, args, image, decoder=None):
     """Reads a floppy disk and dumps it into a new image file.
     """
 
@@ -70,7 +71,8 @@ def read_to_image(usb, args, image):
             flux = usb.read_track(args.revs)
             if args.rpm is not None:
                 flux = normalise_rpm(flux, args.rpm)
-            image.append_track(flux)
+            dat = flux if decoder is None else decoder(cyl, side, flux)
+            image.append_track(dat)
 
     print()
 
@@ -85,6 +87,7 @@ def main(argv):
     parser.add_argument("--device", help="greaseweazle device name")
     parser.add_argument("--drive", type=util.drive_letter, default='A',
                         help="drive to read (A,B,0,1,2)")
+    parser.add_argument("--format", help="disk format")
     parser.add_argument("--revs", type=int, default=3,
                         help="number of revolutions to read per track")
     parser.add_argument("--scyl", type=int, default=0,
@@ -108,7 +111,18 @@ def main(argv):
     try:
         usb = util.usb_open(args.device)
         image = open_image(args)
-        util.with_drive_selected(read_to_image, usb, args, image)
+        if not args.format and hasattr(image, 'default_format'):
+            args.format = image.default_format
+        decoder = None
+        if args.format:
+            mod = importlib.import_module('greaseweazle.codec.' + args.format)
+            decoder = mod.__dict__['decode_track']
+        try:
+            util.with_drive_selected(read_to_image, usb, args, image,
+                                     decoder=decoder)
+        except:
+            print()
+            raise
     except USB.CmdError as error:
         print("Command Failed: %s" % error)
 

+ 2 - 1
scripts/greaseweazle/tools/util.py

@@ -16,6 +16,7 @@ from greaseweazle import usb as USB
 from greaseweazle.image.scp import SCP
 from greaseweazle.image.hfe import HFE
 from greaseweazle.image.ipf import IPF
+from greaseweazle.image.adf import ADF
 
 
 class CmdlineHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
@@ -65,7 +66,7 @@ def split_opts(seq):
 
 
 def get_image_class(name):
-    image_types = { '.scp': SCP, '.hfe': HFE, '.ipf': IPF }
+    image_types = { '.adf': ADF, '.scp': SCP, '.hfe': HFE, '.ipf': IPF }
     _, ext = os.path.splitext(name)
     error.check(ext.lower() in image_types,
                 "%s: Unrecognised file suffix '%s'" % (name, ext))

+ 56 - 3
scripts/greaseweazle/tools/write.py

@@ -12,6 +12,7 @@ description = "Write a disk from the specified image file."
 import sys
 
 from greaseweazle.tools import util
+from greaseweazle import error
 from greaseweazle import usb as USB
 
 
@@ -25,6 +26,17 @@ def open_image(args):
             image = image_class.from_file(f.read())
     return image
 
+class Formatter:
+    def __init__(self):
+        self.length = 0
+    def print(self, s):
+        self.erase()
+        self.length = len(s)
+        print(s, end="", flush=True)
+    def erase(self):
+        l = self.length
+        print("\b"*l + " "*l + "\b"*l, end="", flush=True)
+        self.length = 0
 
 # write_from_image:
 # Writes the specified image file to floppy disk.
@@ -36,6 +48,8 @@ def write_from_image(usb, args, image):
     drive_ticks = (flux.index_list[0] + flux.index_list[1]) / 2
     del flux
 
+    verified_count, not_verified_count = 0, 0
+
     for cyl in range(args.scyl, args.ecyl+1):
         for side in range(0, args.nr_sides):
 
@@ -44,7 +58,8 @@ def write_from_image(usb, args, image):
                 continue
 
             print("\r%sing Track %u.%u..." %
-                  ("Writ" if track is not None else "Eras", cyl, side), end="")
+                  ("Writ" if track is not None else "Eras", cyl, side),
+                  end="", flush=True)
             usb.seek((cyl, cyl*2)[args.double_step], side)
             
             if track is None:
@@ -67,9 +82,41 @@ def write_from_image(usb, args, image):
                 flux_list.append(val)
 
             # Encode the flux times for Greaseweazle, and write them out.
-            usb.write_track(flux_list, flux.terminate_at_index)
+            formatter = Formatter()
+            verified = False
+            for retry in range(3):
+                usb.write_track(flux_list, flux.terminate_at_index)
+                try:
+                    no_verify = args.no_verify or track.verify is None
+                except AttributeError: # track.verify undefined
+                    no_verify = True
+                if no_verify:
+                    not_verified_count += 1
+                    verified = True
+                    break
+                v_revs = 1 if track.splice == 0 else 2
+                v_flux = usb.read_track(v_revs)
+                v_flux.scale(flux.mean_index_time / v_flux.mean_index_time)
+                verified = track.verify.verify_track(v_flux)
+                if verified:
+                    verified_count += 1
+                    break
+                formatter.print(" Retry %d" % (retry + 1))
+            formatter.erase()
+            error.check(verified, "Failed to write Track %u.%u" % (cyl, side))
 
     print()
+    if not_verified_count == 0:
+        print("All tracks verified")
+    else:
+        if verified_count == 0:
+            s = "No tracks verified "
+        else:
+            s = ("%d tracks verified; %d tracks *not* verified "
+                 % (verified_count, not_verified_count))
+        s += ("(Reason: Verify %s)"
+              % ("unavailable", "disabled")[args.no_verify])
+        print(s)
 
 
 def main(argv):
@@ -88,6 +135,8 @@ def main(argv):
                         help="double-step drive heads")
     parser.add_argument("--erase-empty", action="store_true",
                         help="erase empty tracks (default: skip)")
+    parser.add_argument("--no-verify", action="store_true",
+                        help="disable verify")
     parser.add_argument("file", help="input filename")
     parser.description = description
     parser.prog += ' ' + argv[1]
@@ -97,7 +146,11 @@ def main(argv):
     try:
         usb = util.usb_open(args.device)
         image = open_image(args)
-        util.with_drive_selected(write_from_image, usb, args, image)
+        try:
+            util.with_drive_selected(write_from_image, usb, args, image)
+        except:
+            print()
+            raise
     except USB.CmdError as error:
         print("Command Failed: %s" % error)
 

+ 30 - 24
scripts/misc/sw_test.sh

@@ -2,37 +2,43 @@
 
 # Creates a random Amiga ADF, writes the first three cylinders of a disk,
 # dumps those cylinders back, and checks against original ADF.
+dd if=/dev/urandom of=b.adf bs=512 count=1760
+disk-analyse -e 2 b.adf a.adf
+rm -f b.adf
+
+# Write and verify ADF, Read ADF
+./gw --bt write --ecyl=2 a.adf
+./gw --bt read --revs=1 --ecyl=2 b.adf
+disk-analyse -e 2 b.adf c.adf
+diff a.adf c.adf
+md5sum a.adf c.adf
+rm -f b.adf c.adf
 
 # Write SCP, Read SCP
-dd if=/dev/urandom of=a.adf bs=512 count=1760
-disk-analyse -e 2 a.adf b.adf
 disk-analyse a.adf a.scp
-./gw write --ecyl=2 a.scp
-./gw read --revs=1 --ecyl=2 b.scp
-disk-analyse -e 2 b.scp c.adf
-diff b.adf c.adf
-md5sum b.adf c.adf
-rm -f a.adf b.adf c.adf a.scp b.scp
+./gw --bt write --ecyl=2 a.scp
+./gw --bt read --revs=1 --ecyl=2 b.scp
+disk-analyse -e 2 b.scp b.adf
+diff a.adf b.adf
+md5sum a.adf b.adf
+rm -f b.adf a.scp b.scp
 
 # Write IPF, Read HFE
-dd if=/dev/urandom of=a.adf bs=512 count=1760
-disk-analyse -e 2 a.adf b.adf
 disk-analyse a.adf a.ipf
-./gw write --ecyl=2 a.ipf
-./gw read --revs=1 --ecyl=2 b.hfe
-disk-analyse -e 2 b.hfe c.adf
-diff b.adf c.adf
-md5sum b.adf c.adf
-rm -f a.adf b.adf c.adf a.ipf b.hfe
+./gw --bt write --ecyl=2 a.ipf
+./gw --bt read --revs=1 --ecyl=2 b.hfe
+disk-analyse -e 2 b.hfe b.adf
+diff a.adf b.adf
+md5sum a.adf b.adf
+rm -f b.adf a.ipf b.hfe
 
 # Write HFE, Read HFE
-dd if=/dev/urandom of=a.adf bs=512 count=1760
-disk-analyse -e 2 a.adf b.adf
 disk-analyse a.adf a.hfe
-./gw write --ecyl=2 a.hfe
-./gw read --revs=1 --ecyl=2 b.hfe
-disk-analyse -e 2 b.hfe c.adf
-diff b.adf c.adf
-md5sum b.adf c.adf
-rm -f a.adf b.adf c.adf a.hfe b.hfe
+./gw --bt write --ecyl=2 a.hfe
+./gw --bt read --revs=1 --ecyl=2 b.hfe
+disk-analyse -e 2 b.hfe b.adf
+diff a.adf b.adf
+md5sum a.adf b.adf
+rm -f b.adf c.adf a.hfe b.hfe
 
+rm -f a.adf