USB-Floppy: MFM lesen

Ausgangspunkt ist ein ATmega32U4 mit 16-MHz-Quarz, verbaut in Arduino Leonardo und Nachfolgern, etwa dem sehr preiswerten Pro Micro. Für die zeitkritischen Routinen kommt nur Assembler in Frage. Zur Kombination mit gcc wird der Gnu-Assembler-Dialekt gas verwendet, nicht avrasm.

Via SPI

Zu umständlich mit zwei Mikrocontrollern. Hier der alte Text zum Aufklappen:

Das Problem hier ist die Synchronisation in zwei Stufen:

Eine Bitsynchronisation in Software überfordert einen ATmega32U4 bei weitem! Ein variabler RC-Oszillator verbietet sich außerdem für die USB-Kommunikation. Normalerweise würde man jetzt eine PLL-Schaltung aus aufbauen. Und mit dessen Ausgang den SPI-Takt SCK asynchron speisen. Viel Aufwand!

Das mit dem SCK-Eingang klingt aber gar nicht schlecht, damit würde erst mal Bitsynchronität mit dem 1-MBaud-Takt vorliegen. Die Bytesynchronisation und die restliche Dekodierung schafft der ATmega32U4 dann auch noch. Geradeso und mit Tabellen und Assembler, aber es geht. Sein Flash-Speicher ist ja groß genug.

Die Phasenrastschleife kann recht bequem von einem vorgeschalteten 8-beinigen ATtiny13 realisiert werden. (Ein 6-beiniger ATtiny4 tut's übrigens auch.) Dessen ISR, die dem Pegelwechsel 4-5 Takte hinterherläuft, stellt OSCCAL einfach so ein, dass auf einen bestimmten Zählerwert des einzigen Zählers TCNT0 „geregelt“ wird.

// Pin 2 = frei
// Pin 3 = Dateneingang PCINT4
// Pin 5 = Stillsetz-Eingang INT0
// Pin 6 = Taktausgang OC0B
// Pin 7 = Kontrollausgang PLL-Regler

// Sinnvollen Stellbereich für OSCCAL eingrenzen
#define min 72		; siehe Datenblatt, ca. 10 MHz anstreben, Fangbereich ±20 % sollte genügen
#define max 108

// Diese Interruptserviceroutine kehrt nie zurück,
// um Zeit zu sparen (RETI ist lahm).
// Um Stacküberläufe zu vermeiden wird dieser
// einfach immer wieder zurückgestellt.
// Die Zeit zwischen zwei Interrupts ist bei MFM mindestens 2 µs,
// also bei auf 10 MHz „gezogenem“ RC-Oszillator 20 Takte
	rjmp	main
	rjmp	Int0_ISR	;INT0
PinChange_ISR:			;4
	in	r16,TCNT0	;1 Timer0-Capture
	cpi	r16,5		;1 Vergleich mit Mitte des Zählerstandes
	brcs	isr2		;1
	cpi	r17,min		;1
	brcs	isre		;1
	dec	r17		;1
	out	OSCCAL,r17	;1
	cbi	PORTB,2		;1
isre:	out	SPL,18		;1
	sei			;1
	sleep			;1 => 15 Takte
isr2:				;zu langsam: Taktfrequenz aufregeln
	cpi	r17,max
	brcc	isre
	inc	r17
	out	OSCCAL,r17
	sbi	PORTB,2
	out	SPL,r18
	sei
	sleep			;=> 16 Takte
	
Int0_ISR:
	sbic	PINB,0		;Pegel an Pin 5?
	 rjmp	down
up:				;hochfahren
	ldi	r16,0b00100011
	out	MCUCR,r16	;Idle, Interrupt bei High-Pegel an Pin 5
	ldi	r16,0b01100000
	out	GIMSK,r16	;alle Interrupts
	rjmp	isre
down:				;runterfahren
	ldi	r16,0b00110000
	out	MCUCR,r16	;Power-Down, aufwecken mit Low-Pegel an Pin 5
	ldi	r16,0b01000000
	out	GIMSK,r16	;kein Pegelwechsel-Interrupt
	rjmp	isre

main:
	ldi	r18,RAMEND
	out	SPL,r18		;R18 bleibt fortan immer == RAMEND
	ldi	r17,(max+min)>>1;R17 bleibt fortan immer == OSCCAL
	sbi	DDRB,1		;Taktausgang
	sbi	DDRB,2		;Kontrollausgang (zum Debuggen mit Oszilloskop und Laufwerk)
	sbi	PORTB,0		;Pullup aktivieren an INT0
	sbi	ACSR,7		;kein Analogvergleicher
	ldi	r16,0b00100011
	out	MCUCR,r16	;SLEEP aktivieren
// Pegelwechsel-Interrupt an Pin 3
	sbi	PCMSK,4
	ldi	r16,0b01100000
	out	GIMSK,r16
// Timer0 im CTC-Modus und als PWM-Generator initialisieren
	ldi	r16,0b00010011
	out	TCCR0A,r16
	ldi	r16,0b00001001
	out	TCCR0B,r16
	ldi	r16,9
	out	OCR0A,r16	;Zählweise 0-1-2-3-4-5-6-7-8-9-0-1 usw.
	ldi	r16,4		;Steigende Flanke: Ausprobieren auf Datenmitte
	out	OCR0B,r16	;Inverse PWM an Pin 6 generieren (ca. 1 MHz)
	sei
	sleep

Via Input Capture und Zeitmessung

So dicht kommen ja die Impulse nicht, man hat ja von Puls zu Puls (bei HD) mindestens 32 Takte Zeit. Vermutlich ist die Low-Impulsbreite vom Diskettenlaufwerk so groß, dass man das Impulsglättungsfeature des AVRs nutzen kann. Durch die Verwendung der Input-Capture-Hardware ist die Zeitmessung auf 1 CPU-Takt (0,0625 µs) genau, unabhängig vom Programmablauf.

Abhängig von der gemessenen Zeit gilt folgende Fallunterscheidung:

Der folgende Quelltext-Schnipsel erlaubt variable Bitraten. Interrupts müssen weitestgehend gesperrt sein.

Es fehlt ein Abbruch bei fehlenden Flanken; hier bietet sich ein Watchdog an, der auch dann greift, wenn kein Synchronwort gefunden wird. Es muss nicht unbedingt der Watchdog sein; ein zweiter Timer, der genau auf die Dauer der Aufgabe abgestimmt ist, eignet sich besser: Zum Lesen des Sektor-Headers reichlich eine Sektorlänge, zum Lesen des Datenbereichs die Länge jener Daten.

// Daten (Sektor oder Vorblock) einlesen
// Die Synchronisation erfolgt nicht an den Nullen, sondern nur am Synchronwort 0xA1.
// Das letzte davon genügt sogar, die Mehrfachheit wird an der Verletzung der MFM-Kodierung erkannt.
// Unerwarteter Abbruch wenn R27:R26 am Ende nicht Null ist:
// Zu kleiner oder zu großer Impulsabstand.
// Kein Abbruch bei nicht auffindbarem Synchronwort: Watchdog benutzen!
// Kein Abbruch bei fehlenden Impulsen: Watchdog benutzen!
// Kein Abbruch bei nur (also umlaufendem) Synchronwort: Watchdog benutzen!
// Keine CRC-Prüfung, das muss der Aufrufer erledigen
// R22 = Datenrate: 0 = 1 MBit/s (ED), 1 = 500 kBit/s (HD), 2 = 250 kBit/s (DD), 3 = 125 kBit/s (SD)
// R27:R26 = Datensatzlänge (typ. 515 für DOS-Datensektoren)
// R31:R30 = Pufferbereich (RAM)
// Veränderte Register: Parameter-Register (außer R22) sowie
// R0:	Letzter Timer-Wert für Differenzbildung
// R1 = 0 (wird für Interrupt-Acknowledge benötigt)
// R23:	Datenbyte
// R24:	Capture-Ereignis-Zeitdifferenz in CPU-Takten
// R25:	Datenbitzähler (8..1) / im Suchmodus (negativ)
// Timer1 muss initialisiert sein (Zählerlänge auf Vielfaches von 256, also 8..16 Bit)
// Input-Capture muss auf fallende Flanke konfiguriert sein
// Interrupts gesperrt, außer Watchdog (falls via Interrupt)
// Für ED (R22=0) ist eine gesonderte Programmlogik unten, mit einigen Vereinfachungen
inBlockMFM:
	ldi	r25,1<<ICF1
	mov	r1,r25		;1 Takt für Interrupt-ACK sparen (out vs. sbi)
	out	TIFR1,r1	;Interrupt löschen
	clt			;Im Synchronfeld Nullen detektieren lassen
0:	sbic	TIFR1,ICF1
	 rjmp	0b
	lds	r0,ICR1L	;Ersten Impuls hernehmen
	out	TIFR1,r1	;Interrupt quittieren
	ldi	r25,-1		;Bitzähler im Synchronisiermodus
	tst	r23		;Sonderfall ED?
	brne	6f
	rjmp	12f		;Zur Sonderroutine springen
// Unterprogramme
0:	clt			;0 merken
	rjmp	1f		;"0" ausgeben
// Mittlere Schleife für mittleren Impulsabstand (48 CPU-Takte)
3:	brts	0b
	set			;1 merken, "01" ausgeben
// Vorlaufendes Null-Bit ausgeben,
// nicht auf Synchronwort 0xA1 gucken (kann gar nicht passieren!)
4:	lsl	r23		;Null-Bit
	dec	r25		;Bitzähler
	brne	1f
	st	Z+,r23		;Byte abspeichern wenn Bitzähler Null
	sbiw	r26,1
	breq	9f
	ldi	r25,8
// Innere Schleife für kürzesten Impulsabstand (32 CPU-Takte)
// R25 hält die gesammelten Bits des aktuellen Bytes
// 10 Takte im Abspeichermodus (+ 4 Takte alle 8 Bits)
// Max. 12 Takte im Synchronwortsuchmodus
1:	lsl	r23		;Datenbyte
	bld	r23,0		;T nach Bit 0 kopieren
2:	dec	r25		;Bitzähler
	brpl	9f		;Im Synchronmodus wenn negativ
	ori	r25,0xC0	;Nicht positiv werden lassen
	cpi	r23,0xA1	;Synchronwort 0xA1 vorhanden?
	brne	6f
	cpi	r25,-4		;Mit der speziellen Null, vier Bits vorher detektiert?
	brne	6f
	rjmp	8f		;Mit Datenbytes starten
9:	brne	6f
	st	Z+,r23
	sbiw	r26,1
	breq	9f		;geschafft
8:	ldi	r25,8
// Schleife zum Abholen des Input-Capture-Wertes
// Problem: Blockiert ohne Signal ewig. Watchdog verwenden!
// 7 Takte
6:	sbic	TIFR1,ICF1
	 rjmp	6b
	lds	r24,ICR1L
	out	TIFR1,r1
	sub	r24,r0		;Differenz zum vorherigen Wert
	add	r0,r24		;nächster vorheriger Wert
// Ergebnis zurechtschieben
// 3 Takte (bei HD)
	cpi	r22,2
	brcs	7f		;500 kBit/s (HD) — Wert belasssen
	breq	8f		;250 kBit/s (DD) — Wert halbieren
	lsr	r24		;125 kBit/s (SD) — Wert vierteln
8:	lsr	r24
// Input-Capture-Differenz bewerten
// MFM ist RLL(1,3), es gibt daher nur Lauflängen von 2, 3 oder 4.
7:	cpi	r24,24		;Diese Vergleichswerte sind CPU-taktfrequenzabhängig!
	brcs	8f		;Fehler: Abstand zu klein
	cpi	r24,40
	brcs	1b		;5 {Σ 25 Takte} 2 Zeiten: 1 Bit: Sende T Merke T
// Ende der inneren Schleife
	cpi	r24,56
	brcs	3b		;3 Zeiten: T==1 ? Sende 0 Merke 0 : Sende 01 Merke 1
// Ende der mittleren Schleife
	cpi	r24,72
	brcc	8f		;Fehler: Abstand zu groß
// Fall Zeiten = 4
	brts	4b		;4 Zeiten: Sende 0T Merke T
	ldi	r25,0		;Der Fall T==0 darf nur beim A1-Syncbyte passieren
	rjmp	4b
// Ende der äußeren Schleife
// Im Fehlerfall abbrechen oder Synchronbyte weitersuchen
8:	clt
	tst	r25
	brmi	6b
9:	clr	r1
	ret
Der Funktionstest dieser Firmware steht noch aus.

Verflixtes Interleave

Diskettensektoren sind in der Regel nicht interleaved (verzahnt) angeordnet, sondern der Reihe nach. Der DMA-Controller eines Uralt-PCs hatte damit keine Probleme, und das mit der CRC hat die Hardware gemacht. Mit USB gibt's das Problem, dass die 512-Byte-Sektoren in Full-Speed-USB-Häppchen von 64 Byte abgeschickt werden müssen, aber erst wenn die CRC stimmt. Inzwischen flutscht der nächste Sektor am Lesekopf vorbei. Zum Glück hat der ATmega32U4 (auch der ATmega16U4) genügend RAM für einen zweiten Sektorpuffer. Während des Sektorlesens kann — außer bei ED — der USB-Endpoint während des Wartens auf die Flanke bedient werden. Nur ein Byte, aber das genügt. Muss noch ausprogrammiert werden.