12
12
* Author(s): Jeff Epler
13
13
"""
14
14
15
+ import struct
15
16
import floppyio
16
17
from digitalio import DigitalInOut , Pull
17
18
from micropython import const
@@ -55,7 +56,7 @@ class Floppy: # pylint: disable=too-many-instance-attributes
55
56
56
57
_track : typing .Optional [int ]
57
58
58
- def __init__ (
59
+ def __init__ ( # pylint: disable=too-many-locals
59
60
self ,
60
61
* ,
61
62
densitypin : microcontroller .Pin ,
@@ -72,6 +73,7 @@ def __init__(
72
73
wrdatapin : typing .Optional [microcontroller .Pin ] = None ,
73
74
wrgatepin : typing .Optional [microcontroller .Pin ] = None ,
74
75
floppydirectionpin : typing .Optional [microcontroller .Pin ] = None ,
76
+ floppyenablepin : typing .Optional [microcontroller .Pin ] = None ,
75
77
) -> None :
76
78
self ._density = DigitalInOut (densitypin )
77
79
self ._density .pull = Pull .UP
@@ -102,6 +104,10 @@ def __init__(
102
104
if self ._floppydirection :
103
105
self ._floppydirection .switch_to_output (True )
104
106
107
+ self ._floppyenable = _optionaldigitalinout (floppyenablepin )
108
+ if self ._floppyenable :
109
+ self ._floppyenable .switch_to_output (False )
110
+
105
111
self ._track = None
106
112
107
113
def _do_step (self , direction , count ):
@@ -156,10 +162,12 @@ def track(self, track: int) -> None:
156
162
raise ValueError ("Invalid seek to negative track number" )
157
163
158
164
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 )
163
171
164
172
self ._track = track
165
173
self ._check_inpos ()
@@ -222,7 +230,8 @@ def flux_readinto(self, buf: "circuitpython_typing.WritableBuffer") -> int:
222
230
class FloppyBlockDevice : # pylint: disable=too-many-instance-attributes
223
231
"""Wrap an MFMFloppy object into a block device suitable for `storage.VfsFat`
224
232
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"
226
235
227
236
In the current implementation, the floppy is read-only.
228
237
@@ -243,30 +252,75 @@ class FloppyBlockDevice: # pylint: disable=too-many-instance-attributes
243
252
def __init__ ( # pylint: disable=too-many-arguments
244
253
self ,
245
254
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 ,
251
264
):
252
265
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
261
272
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"""
264
304
265
305
self ._track_read (self .track0side0_cache , self .track0side0_validity , 0 , 0 )
266
306
267
307
self .cached_track = - 1
268
308
self .cached_side = - 1
269
309
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
+
270
324
def deinit (self ):
271
325
"""Deinitialize this object"""
272
326
self .floppy .deinit ()
@@ -311,22 +365,25 @@ def _get_track_data(self, track, side):
311
365
return self .track_cache , self .track_validity
312
366
313
367
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 )
316
369
self .floppy .track = track
317
370
self .floppy .side = side
318
371
self ._mfm_readinto (track_data , validity )
319
- self .floppy .spin = False
320
- self .floppy .selected = False
372
+ self ._select_and_spin (False )
321
373
self .cached_track = track
322
374
self .cached_side = side
323
375
324
376
def _mfm_readinto (self , track_data , validity ):
377
+ n = 0
378
+ exc = None
325
379
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
328
385
n = floppyio .mfm_readinto (
329
- track_data ,
386
+ track_data [: 512 * self . sectors ] ,
330
387
self .flux_buffer ,
331
388
self ._t2_5_max ,
332
389
self ._t3_5_max ,
@@ -335,3 +392,89 @@ def _mfm_readinto(self, track_data, validity):
335
392
)
336
393
if n == self .sectors :
337
394
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