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 ()
@@ -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 .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 = } { 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