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.
Das Problem hier ist die Synchronisation in zwei Stufen:
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
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.