Skip to content

Commit

Permalink
hp.mmfm: New codec for HP MMFM as used by 9885/9895 disk controllers
Browse files Browse the repository at this point in the history
New formats: hp.mmfm.9885, hp.mmfm.9895
  • Loading branch information
keirf committed Jul 4, 2024
1 parent 91653b2 commit 01ca2c3
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 13 deletions.
3 changes: 3 additions & 0 deletions src/greaseweazle/codec/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ def read_diskdef_file_lines(filename: Optional[str]) -> Tuple[List[str], str]:
from greaseweazle.codec.macintosh import mac_gcr
from greaseweazle.codec.commodore import c64_gcr
from greaseweazle.codec.apple2 import apple2_gcr
from greaseweazle.codec.hp import hp_mmfm

def mk_trackdef(format_name: str) -> TrackDef:
if format_name in ['amiga.amigados']:
Expand All @@ -159,6 +160,8 @@ def mk_trackdef(format_name: str) -> TrackDef:
return mac_gcr.MacGCRDef(format_name)
if format_name in ['c64.gcr']:
return c64_gcr.C64GCRDef(format_name)
if format_name in ['hp.mmfm']:
return hp_mmfm.HPMMFMDef(format_name)
if format_name in ['apple2.gcr']:
return apple2_gcr.Apple2GCRDef(format_name)
if format_name in ['bitcell']:
Expand Down
Empty file.
258 changes: 258 additions & 0 deletions src/greaseweazle/codec/hp/hp_mmfm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# greaseweazle/codec/hp/hp_mmfm.py
#
# Written & released by Keir Fraser <[email protected]>
#
# This is free and unencumbered software released into the public domain.

# From github:brouhaha/fluxtoimd/modulation.py:
#
# See the file COPYING for more details, or visit <http://unlicense.org>.
# An HP-proprietary M2FM floppy format, used by the HP 7902, 9885,
# and 9895 Flexible Disc Drives.
# Documentation:
# 9885 Flexible Disk Drive Service Manual
# Hewlett-Packard, September 1976, part number 09885-90030
# 7902A Disc Drive Preliminary Service Manual
# Hwelett Packard, May 1979, part number 07902-90060
# 7902A & C/9895K Flexible Disc Drive Service Documentation
# Hewlett-Packard, January 1981, part number 07902-90030
# 9895A Flexible Disc Memory Service Manual,
# Hewlett-Packard, February 1981, part number 09895-90030
# 9885: single-sided, 67 track, M2FM format only
# 7902: double-sided, 77 track, M2FM or IBM 3740 FM formats
# 9895: double-sided, 77 track, M2FM or IBM 3740 FM formats

from typing import List, Optional

import struct
from bitarray import bitarray
import crcmod.predefined

from greaseweazle import error
from greaseweazle.codec import codec
from greaseweazle.codec.ibm.ibm import decode, encode, sec_map
from greaseweazle.track import MasterTrack, PLL, PLLTrack
from greaseweazle.flux import HasFlux

default_revs = 1.1

bad_sector = b'-=[BAD SECTOR]=-' * 16

# Syncs have an extra clock bit:
# ff 0e -> 55 55 22 54 -> 55 55 2a 54
sector_sync_bytes = b'\x55\x55\x2a\x54'
# ff 0a -> 55 55 22 44 -> 55 55 2a 44
data_sync_bytes = b'\x55\x55\x2a\x44'

sector_sync = bitarray(endian='big')
sector_sync.frombytes(sector_sync_bytes)

data_sync = bitarray(endian='big')
data_sync.frombytes(data_sync_bytes)

crc16 = crcmod.predefined.Crc('crc-ccitt-false')

rev_list = bytearray()
for x in range(256):
y = 0
for i in range(8):
y <<= 1
y |= x & 1
x >>= 1
rev_list.append(y)

def bitrev(x):
return rev_list[x]

# MMFM:
# Write clock bit iff
# 1. No preceding clock or data
# 2. No following data
mmfm_list = bytearray()
for x in range(512):
for i in [7,5,3,1]:
if x & (0xf << (i-1)) == 0:
x |= 1<<i
mmfm_list.append(x & 255)

def mmfm_encode(dat):
y, out = 0, bytearray()
for x in dat:
if (x & 0xaa) == 0:
x = mmfm_list[(y<<8) | x]
out.append(x)
y = 0 if (x & 3) == 0 else 1
return bytes(out)

class HPMMFM(codec.Codec):

time_per_rev = 60 / 360
clock = 1e-6

verify_revs = default_revs

def __init__(self, cyl: int, head: int, config):
self.cyl, self.head = cyl, head
self.config = config
self.sector: List[Optional[bytes]]
self.sector = [None] * self.nsec

@property
def nsec(self) -> int:
return self.config.secs

def summary_string(self) -> str:
nsec, nbad = self.nsec, self.nr_missing()
s = "HP MMFM (%d/%d sectors)" % (nsec - nbad, nsec)
return s

# private
def add(self, sec_id, data) -> None:
assert not self.has_sec(sec_id)
self.sector[sec_id] = data

def has_sec(self, sec_id: int) -> bool:
return self.sector[sec_id] is not None

def nr_missing(self) -> int:
return len([sec for sec in self.sector if sec is None])

def get_img_track(self) -> bytearray:
tdat = bytearray()
for sec in self.sector:
tdat += sec if sec is not None else bad_sector
return tdat

def set_img_track(self, tdat: bytes) -> int:
totsize = self.nsec * 256
if len(tdat) < totsize:
tdat += bytes(totsize - len(tdat))
for sec in range(self.nsec):
self.sector[sec] = tdat[sec*256:(sec+1)*256]
return totsize

def decode_flux(self, track: HasFlux, pll: Optional[PLL]=None) -> None:
raw = PLLTrack(time_per_rev = self.time_per_rev,
clock = self.clock, data = track, pll = pll)
bits, _ = raw.get_all_data()

for offs in bits.itersearch(sector_sync):

if self.nr_missing() == 0:
break

offs += 2*16
idam = decode(bits[offs:offs+4*16].tobytes())
if len(idam) != 4:
continue
if crc16.new(idam).crcValue != 0:
continue
cyl = bitrev(idam[0])
sec_id = bitrev(idam[1])
head = (sec_id & 128) == 128
sec_id &= 127
if cyl != self.cyl or head != self.head or sec_id > self.nsec:
print('T%d.%d: Ignoring unexpected sector C:%d H:%d R:%d'
% (self.cyl, self.head, cyl, head, sec_id))
continue
if self.has_sec(sec_id):
continue

# Find data
offs += 8*16
dat_offs = bits[offs:offs+50*16].search(data_sync)
if len(dat_offs) != 1:
continue
offs += dat_offs[0] + 2*16

sec = decode(bits[offs:offs+258*16].tobytes())
if len(sec) != 258:
continue
if crc16.new(sec).crcValue != 0:
continue

# bit swap, and byte swap
sec = bytes(map(lambda x: bitrev(x), sec[:256]))
sec = struct.pack('<128H', *struct.unpack('>128H', sec))

self.add(sec_id, sec)


def master_track(self) -> MasterTrack:

# Post-index track gap.
t = encode(bytes(100))

for sec_id in sec_map(self.nsec, self.config.interleave,
self.config.cskew, self.config.hskew,
self.cyl, self.head):
sector = self.sector[sec_id]
data = bad_sector if sector is None else sector
# Header
t += encode(bytes([0xff]*3)) + sector_sync_bytes
idam = bytes(map(lambda x: bitrev(x),
[self.cyl, sec_id | (self.head << 7)]))
idam += struct.pack('>H', crc16.new(idam).crcValue)
t += encode(idam)
t += encode(bytes(1 + 16 + 4))
# Data
t += encode(bytes([0xff]*3)) + data_sync_bytes
data = struct.pack('<128H', *struct.unpack('>128H', data))
data = bytes(map(lambda x: bitrev(x), data))
data += struct.pack('>H', crc16.new(data).crcValue)
t += encode(data)
t += encode(bytes(1 + 34 + 4))

# Add the pre-index gap.
tlen = int((self.time_per_rev / self.clock)) & ~31
t += encode(bytes((tlen//8-len(t))//2))

t = mmfm_encode(t)

track = MasterTrack(bits = t, time_per_rev = self.time_per_rev)
track.verify = self
return track


def verify_track(self, flux):
readback_track = self.__class__(self.cyl, self.head, self.config)
readback_track.decode_flux(flux)
return (readback_track.nr_missing() == 0
and self.sector == readback_track.sector)


class HPMMFMDef(codec.TrackDef):

default_revs = default_revs

def __init__(self, format_name: str):
self.interleave = 1
self.cskew, self.hskew = 0, 0
self.secs: Optional[int] = None
self.finalised = False

def add_param(self, key: str, val) -> None:
if key == 'secs':
val = int(val)
self.secs = val
elif key in ['interleave', 'cskew', 'hskew']:
n = int(val)
error.check(0 <= n <= 255, '%s out of range' % key)
setattr(self, key, n)
else:
raise error.Fatal('unrecognised track option %s' % key)

def finalise(self) -> None:
if self.finalised:
return
error.check(self.secs is not None,
'number of sectors not specified')
self.finalised = True

def mk_track(self, cyl: int, head: int) -> HPMMFM:
return HPMMFM(cyl, head, self)


# Local variables:
# python-indent: 4
# End:
29 changes: 16 additions & 13 deletions src/greaseweazle/codec/ibm/ibm.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ def decode(dat):

crc16 = crcmod.predefined.Crc('crc-ccitt-false')

# Create logical sector map in rotational order
def sec_map(nsec: int, interleave: int, cskew: int, hskew: int,
cyl: int, head: int) -> List[int]:
sec_map, pos = [-1] * nsec, 0
if nsec != 0:
pos = (cyl*cskew + head*hskew) % nsec
for i in range(nsec):
while sec_map[pos] != -1:
pos = (pos + 1) % nsec
sec_map[pos] = i
pos = (pos + interleave) % nsec
return sec_map

def sec_sz(n):
return 128 << n if n <= 7 else 128 << 8

Expand Down Expand Up @@ -762,16 +775,6 @@ def sec_n(i):
t.time_per_rev = 60 / rpm
t.clock = t.time_per_rev / tracklen_bc

# Create logical sector map in rotational order
sec_map, pos = [-1] * nsec, 0
if nsec != 0:
pos = (cyl*config.cskew + head*config.hskew) % nsec
for i in range(nsec):
while sec_map[pos] != -1:
pos = (pos + 1) % nsec
sec_map[pos] = i
pos = (pos + config.interleave) % nsec

pos = gap4a
if gap1 is not None:
pos += t.gap_presync
Expand All @@ -780,11 +783,11 @@ def sec_n(i):

id0 = config.id
h = head if config.h is None else config.h
for i in range(nsec):
sec = sec_map[i]
for sec in sec_map(nsec, config.interleave,
config.cskew, config.hskew, cyl, head):
pos += t.gap_presync
idam = IDAM(pos*16, (pos+synclen+4+2)*16, 0xffff,
c = cyl, h = h, r= id0+sec, n = sec_n(sec))
c = cyl, h = h, r = id0+sec, n = sec_n(sec))
pos += synclen + 4 + 2 + gap2 + t.gap_presync
size = 128 << idam.n
datsz = size*2 if mark_dam == Mark.DAM_DEC_MMFM else size
Expand Down
24 changes: 24 additions & 0 deletions src/greaseweazle/data/diskdefs.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,30 @@ disk gem.1600
end
end

# HP 9885 Flexible Disk Drive
disk hp.mmfm.9885
cyls = 67
heads = 1
tracks * hp.mmfm
secs = 30
interleave = 4
cskew = 4
hskew = 1
end
end

# HP 9895 (and 7902) Flexible Disk Drive
disk hp.mmfm.9895
cyls = 77
heads = 2
tracks * hp.mmfm
secs = 30
interleave = 4
cskew = 4
hskew = 1
end
end

disk ibm.160
cyls = 40
heads = 1
Expand Down

0 comments on commit 01ca2c3

Please sign in to comment.