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 retDer Funktionstest dieser Firmware steht noch aus.