1212* Author(s): Jeff Epler
1313"""
1414
15+ import struct
1516import floppyio
1617from digitalio import DigitalInOut , Pull
1718from 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 ()
@@ -205,7 +213,7 @@ def side(self) -> int:
205213 def side (self , head : int ) -> None :
206214 self ._side .value = head == 0
207215
208- def flux_readinto (self , buf : "circuitpython_typing.WritableBuffer " ) -> int :
216+ def flux_readinto (self , buf : "circuitpython_typing.WriteableBuffer " ) -> int :
209217 """Read flux transition information into the buffer.
210218
211219 The function returns when the buffer has filled, or when the index input
@@ -222,7 +230,8 @@ def flux_readinto(self, buf: "circuitpython_typing.WritableBuffer") -> int:
222230class 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 .WriteableBuffer | 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
0 commit comments