Skip to content

Commit 9840f36

Browse files
committed
Update for floppsy rev b & add DOS floppy archiver
1 parent 7cc730a commit 9840f36

File tree

2 files changed

+275
-28
lines changed

2 files changed

+275
-28
lines changed

adafruit_floppy.py

+171-28
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* Author(s): Jeff Epler
1313
"""
1414

15+
import struct
1516
import floppyio
1617
from digitalio import DigitalInOut, Pull
1718
from micropython import const
@@ -55,7 +56,7 @@ class Floppy: # pylint: disable=too-many-instance-attributes
5556

5657
_track: typing.Optional[int]
5758

58-
def __init__(
59+
def __init__( # pylint: disable=too-many-locals
5960
self,
6061
*,
6162
densitypin: microcontroller.Pin,
@@ -72,6 +73,7 @@ def __init__(
7273
wrdatapin: typing.Optional[microcontroller.Pin] = None,
7374
wrgatepin: typing.Optional[microcontroller.Pin] = None,
7475
floppydirectionpin: typing.Optional[microcontroller.Pin] = None,
76+
floppyenablepin: typing.Optional[microcontroller.Pin] = None,
7577
) -> None:
7678
self._density = DigitalInOut(densitypin)
7779
self._density.pull = Pull.UP
@@ -102,6 +104,10 @@ def __init__(
102104
if self._floppydirection:
103105
self._floppydirection.switch_to_output(True)
104106

107+
self._floppyenable = _optionaldigitalinout(floppyenablepin)
108+
if self._floppyenable:
109+
self._floppyenable.switch_to_output(False)
110+
105111
self._track = None
106112

107113
def _do_step(self, direction, count):
@@ -156,10 +162,12 @@ def track(self, track: int) -> None:
156162
raise ValueError("Invalid seek to negative track number")
157163

158164
delta = track - self.track
159-
if delta < 0:
160-
self._do_step(_STEP_OUT, -delta)
161-
elif delta > 0:
162-
self._do_step(_STEP_IN, delta)
165+
if delta:
166+
if delta < 0:
167+
self._do_step(_STEP_OUT, -delta)
168+
elif delta > 0:
169+
self._do_step(_STEP_IN, delta)
170+
_sleep_ms(_STEP_DELAY_MS)
163171

164172
self._track = track
165173
self._check_inpos()
@@ -222,7 +230,8 @@ def flux_readinto(self, buf: "circuitpython_typing.WritableBuffer") -> int:
222230
class FloppyBlockDevice: # pylint: disable=too-many-instance-attributes
223231
"""Wrap an MFMFloppy object into a block device suitable for `storage.VfsFat`
224232
225-
The default heads/sectors/tracks setting are for 3.5", 1.44MB floppies.
233+
The default is to autodetect the data rate and the geometry of an inserted
234+
floppy using the floppy's "BIOS paramter block"
226235
227236
In the current implementation, the floppy is read-only.
228237
@@ -243,30 +252,75 @@ class FloppyBlockDevice: # pylint: disable=too-many-instance-attributes
243252
def __init__( # pylint: disable=too-many-arguments
244253
self,
245254
floppy,
246-
heads=2,
247-
sectors=18,
248-
tracks=80,
249-
flux_buffer=None,
250-
t1_nom_ns: float = 1000,
255+
*,
256+
max_sectors=18,
257+
autodetect: bool = True,
258+
heads: int | None = None,
259+
sectors: int | None = None,
260+
tracks: int | None = None,
261+
flux_buffer: circuitpython_typing.WritableBuffer | None = None,
262+
t1_nom_ns: float | None = None,
263+
keep_selected: bool = False,
251264
):
252265
self.floppy = floppy
253-
self.heads = heads
254-
self.sectors = sectors
255-
self.tracks = tracks
256-
self.flux_buffer = flux_buffer or bytearray(sectors * 12 * 512)
257-
self.track0side0_cache = memoryview(bytearray(sectors * 512))
258-
self.track0side0_validity = bytearray(sectors)
259-
self.track_cache = memoryview(bytearray(sectors * 512))
260-
self.track_validity = bytearray(sectors)
266+
self.flux_buffer = flux_buffer or bytearray(max_sectors * 12 * 512)
267+
self.track0side0_cache = memoryview(bytearray(max_sectors * 512))
268+
self.track_cache = memoryview(bytearray(max_sectors * 512))
269+
self._keep_selected = keep_selected
270+
self.cached_track = -1
271+
self.cached_side = -1
261272

262-
self._t2_5_max = round(2.5 * t1_nom_ns * floppyio.samplerate * 1e-9)
263-
self._t3_5_max = round(3.5 * t1_nom_ns * floppyio.samplerate * 1e-9)
273+
if autodetect:
274+
self.autodetect()
275+
else:
276+
self.setformat(heads, sectors, tracks, t1_nom_ns)
277+
278+
if keep_selected:
279+
self.floppy.selected = True
280+
self.floppy.spin = True
281+
282+
@property
283+
def keep_selected(self) -> bool:
284+
"""Whether to keep the drive selected & spinning between operations
285+
286+
This can make operations faster by avoiding spin up time"""
287+
return self._keep_selected
288+
289+
@keep_selected.setter
290+
def keep_selected(self, value: bool):
291+
self.floppy.selected = value
292+
self.floppy.spin = value
293+
294+
def _select_and_spin(self, value: bool):
295+
if self.keep_selected:
296+
return
297+
self.floppy.selected = value
298+
self.floppy.spin = value
299+
300+
def on_disk_change(self):
301+
"""This function (or autodetect or setformat) must be called after a disk is changed
302+
303+
Flushes the cached floppy data"""
264304

265305
self._track_read(self.track0side0_cache, self.track0side0_validity, 0, 0)
266306

267307
self.cached_track = -1
268308
self.cached_side = -1
269309

310+
def setformat(self, heads, sectors, tracks, t1_nom_ns):
311+
"""Set the floppy format details
312+
313+
This also calls on_disk_change to flush cached floppy data."""
314+
self.heads = heads
315+
self.sectors = sectors
316+
self.tracks = tracks
317+
self._t1_nom_ns = t1_nom_ns
318+
self._t2_5_max = round(2.5 * t1_nom_ns * floppyio.samplerate * 1e-9)
319+
self._t3_5_max = round(3.5 * t1_nom_ns * floppyio.samplerate * 1e-9)
320+
self.track0side0_validity = bytearray(sectors)
321+
self.track_validity = bytearray(sectors)
322+
self.on_disk_change()
323+
270324
def deinit(self):
271325
"""Deinitialize this object"""
272326
self.floppy.deinit()
@@ -311,22 +365,25 @@ def _get_track_data(self, track, side):
311365
return self.track_cache, self.track_validity
312366

313367
def _track_read(self, track_data, validity, track, side):
314-
self.floppy.selected = True
315-
self.floppy.spin = True
368+
self._select_and_spin(True)
316369
self.floppy.track = track
317370
self.floppy.side = side
318371
self._mfm_readinto(track_data, validity)
319-
self.floppy.spin = False
320-
self.floppy.selected = False
372+
self._select_and_spin(False)
321373
self.cached_track = track
322374
self.cached_side = side
323375

324376
def _mfm_readinto(self, track_data, validity):
377+
n = 0
378+
exc = None
325379
for i in range(5):
326-
self.floppy.flux_readinto(self.flux_buffer)
327-
print("timing bins", self._t2_5_max, self._t3_5_max)
380+
try:
381+
self.floppy.flux_readinto(self.flux_buffer)
382+
except RuntimeError as error:
383+
exc = error
384+
continue
328385
n = floppyio.mfm_readinto(
329-
track_data,
386+
track_data[: 512 * self.sectors],
330387
self.flux_buffer,
331388
self._t2_5_max,
332389
self._t3_5_max,
@@ -335,3 +392,89 @@ def _mfm_readinto(self, track_data, validity):
335392
)
336393
if n == self.sectors:
337394
break
395+
if n == 0 and exc is not None:
396+
raise exc
397+
398+
def _detect_diskformat_from_flux(self):
399+
sector = self.track_cache[:512]
400+
# The first two numbers are HD and DD rates. The next two are the bit
401+
# rates for 300RPM media read in a 360RPM drive.
402+
for t1_nom_ns in [1_000, 2_000, 8_33, 1_667]:
403+
t2_5_max = round(2.5 * t1_nom_ns * floppyio.samplerate * 1e-9)
404+
t3_5_max = round(3.5 * t1_nom_ns * floppyio.samplerate * 1e-9)
405+
406+
n = floppyio.mfm_readinto(
407+
sector,
408+
self.flux_buffer,
409+
t2_5_max,
410+
t3_5_max,
411+
)
412+
413+
if n == 0:
414+
continue
415+
416+
if sector[510] != 0x55 or sector[511] != 0xAA:
417+
print("did not find boot signature 55 AA")
418+
print(
419+
"First 16 bytes in sector:",
420+
" ".join("%02x" % c for c in sector[:16]),
421+
)
422+
print(
423+
"Final 16 bytes in sector:",
424+
" ".join("%02x" % c for c in sector[-16:]),
425+
)
426+
continue
427+
428+
n_sectors_track = sector[0x18]
429+
n_heads = sector[0x1A]
430+
if n_heads != 2:
431+
print(f"unsupported head count {n_heads=}")
432+
continue
433+
n_sectors_total = struct.unpack("<H", sector[0x13:0x15])[0]
434+
n_tracks = n_sectors_total // (n_heads * n_sectors_track)
435+
f_tracks = n_sectors_total % (n_heads * n_sectors_track)
436+
if f_tracks != 0:
437+
# pylint: disable=line-too-long
438+
print(
439+
f"Dubious geometry! {n_sectors_total=} {n_sectors_track=} {n_heads=} is {n_tracks=}+{f_tracks=}"
440+
)
441+
n_tracks += 1
442+
443+
return {
444+
"heads": n_heads,
445+
"sectors": n_sectors_track,
446+
"tracks": n_tracks,
447+
"t1_nom_ns": t1_nom_ns,
448+
}
449+
450+
def autodetect(self):
451+
"""Detect an inserted DOS floppy
452+
453+
The floppy must have a standard MFM data rate & DOS 2.0 compatible Bios
454+
Parameter Block (BPB). Almost all FAT formatted floppies for DOS & Windows
455+
should autodetect in this way.
456+
457+
This also flushes the cached data.
458+
"""
459+
self._select_and_spin(True)
460+
self.floppy.track = 1
461+
self.floppy.track = 0
462+
self.floppy.side = 0
463+
exc = None
464+
try:
465+
for _ in range(5): # try repeatedly to read track 0 side 0 sector 0
466+
try:
467+
self.floppy.flux_readinto(self.flux_buffer)
468+
except RuntimeError as error:
469+
exc = error
470+
continue
471+
diskformat = self._detect_diskformat_from_flux()
472+
if diskformat is not None:
473+
break
474+
finally:
475+
self._select_and_spin(False)
476+
477+
if diskformat is not None:
478+
self.setformat(**diskformat)
479+
else:
480+
raise OSError("Failed to detect floppy format") from exc

examples/dos_archiver.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import os
2+
import sdcardio
3+
# SPDX-FileCopyrightText: Copyright (c) 2024 Jeff Epler for Adafruit Industries
4+
#
5+
# SPDX-License-Identifier: Unlicense
6+
7+
"""DOS floppy archiver for Adafruit Floppsy
8+
9+
Insert an SD card & hook up your floppy drive.
10+
Open the REPL / serial connection
11+
Insert a floppy and press Enter to archive it
12+
Do this for as many floppies as you like."""
13+
14+
import board
15+
import storage
16+
import adafruit_floppy
17+
18+
floppy = adafruit_floppy.Floppy(
19+
densitypin=board.DENSITY,
20+
indexpin=board.INDEX,
21+
selectpin=board.SELECT,
22+
motorpin=board.MOTOR,
23+
directionpin=board.DIRECTION,
24+
steppin=board.STEP,
25+
track0pin=board.TRACK0,
26+
protectpin=board.WRPROT,
27+
rddatapin=board.RDDATA,
28+
sidepin=board.SIDE,
29+
readypin=board.READY,
30+
wrdatapin=board.WRDATA,
31+
wrgatepin=board.WRGATE,
32+
floppydirectionpin=board.FLOPPY_DIRECTION,
33+
floppyenablepin=board.FLOPPY_ENABLE,
34+
)
35+
36+
_image_counter = 0
37+
38+
39+
def open_next_image(extension="img"):
40+
"""Return an opened numbered file on the sdcard, such as "img01234.jpg"."""
41+
global _image_counter # pylint: disable=global-statement
42+
try:
43+
os.stat("/sd")
44+
except OSError as exc: # no SD card!
45+
raise RuntimeError("No SD card mounted") from exc
46+
while True:
47+
filename = "/sd/dsk%04d.%s" % (_image_counter, extension)
48+
_image_counter += 1
49+
try:
50+
os.stat(filename)
51+
except OSError:
52+
break
53+
print("Writing to", filename)
54+
return open(filename, "wb")
55+
56+
57+
sdcard = sdcardio.SDCard(board.SPI(), board.SD_CS)
58+
vfs = storage.VfsFat(sdcard)
59+
storage.mount(vfs, "/sd")
60+
61+
dev = None
62+
blockdata = bytearray(512)
63+
baddata = b"BADDATA0" * 64
64+
65+
while True:
66+
if dev is not None:
67+
dev.floppy.keep_selected = False
68+
input("Insert disk and press ENTER")
69+
70+
try:
71+
if dev is None:
72+
dev = adafruit_floppy.FloppyBlockDevice(floppy, keep_selected=True)
73+
else:
74+
dev.floppy.keep_selected = True
75+
dev.autodetect()
76+
except OSError as e:
77+
print(e)
78+
continue
79+
80+
bad_blocks = good_blocks = 0
81+
total_blocks = dev.count()
82+
pertrack = dev.sectors * dev.heads
83+
with open_next_image() as f:
84+
for i in range(total_blocks):
85+
if i % pertrack == 0:
86+
print(end=f"{i//pertrack:02d}")
87+
try:
88+
dev.readblocks(i, blockdata)
89+
print(end=".")
90+
f.write(blockdata)
91+
good_blocks += 1
92+
except Exception as e: # pylint: disable=broad-exception-caught
93+
bad_blocks += 1
94+
print(end="!")
95+
f.write(baddata)
96+
if i % pertrack == (pertrack // 2 - 1):
97+
print(end="|")
98+
if i % pertrack == (pertrack - 1):
99+
print()
100+
101+
print(
102+
f"{good_blocks} good + {bad_blocks} bad blocks",
103+
f"out of {total_blocks} ({total_blocks//2}KiB)",
104+
)

0 commit comments

Comments
 (0)