tinyDisk2: un emulatore di floppy
Come avevo accennato in precedenza, l’Apple II della RetrOfficina ha il lettore floppy guasto. Il che è un gran dispiacere, visto che buona parte del software per questa macchina circolava su dischetto. Non solo, alcuni nastri necessitano di versioni specifiche del BASIC o del DOS caricate in memoria… da dischetto. Mi sono guardato in giro per valutare l’acquisto di uno di quegli onnipotenti emulatori di floppy, come il floppyEmu o il wDrive. Sono belli ma costosi. Il che mi ha spinto a realizzare la mia personale versione di emulatore floppy, con l’intenzione di rendere tutto il progetto disponibile online.
Prima di reinventare l’acqua calda ho dato un’occhiata sul web alla ricerca di un qualche progetto libero, ed in effetti mi sono imbattuto in un certo SDISK II, di cui sono disponibili online sia gli schemi che il sorgente. Ho provato a realizzarlo senza successo, ma non ci ho messo molto impegno: l’hardware non mi convinceva del tutto. In ogni caso il firmware è stato un ottimo ed utilissimo punto di partenza su cui lavorare per il mio progetto.
In tutto questo non avevo la minima idea di come funzionasse un floppy, men che mai quello dell’Apple II. Quindi, per prima cosa ho iniziato a documentarmi per studiare l’elettronica ed il protocollo.
Il protocollo - Organizzazione fisica dei dati
L’involucro morbido dei floppy da 5¼" protegge un dischetto di materiale plastico, ricoperto di un sottile strato magnetizzabile. Le informazioni sono registrate digitalmente applicando un campo magnetico localizzato ad un’area ristretta, che vi resta impresso. La lettura avviene con il processo inverso, captando i cambiamenti nella magnetizzazione del dischetto.
Lettura e scrittura avvengono mediante una coppia di testine, poste a distanza ravvicinata dal dischetto, mentre quest’ultimo è in rotazione.
Immaginando la superficie suddivisa in intervalli regolari, si utilizzata una particolare codifica per mappare i bit su disco: il bit 1
si rappresenta con una inversione di polarità e lo 0
con l’assenza di transizioni di polarità.
Il campo magnetico impresso nel dischetto sarebbe troppo debole per poter essere direttamente digitalizzato: per questa ragione è necessario un amplificatore che mantenga costante l’ampiezza del segnale analogico, così da poter leggere sia un disco appena scritto che uno più rovinato.
Sfortunatamente, una lunga sequenza di zeri dà un output pressoché costante ed il Drive II tenderà ad “alzare il volume”… arrivando a captare le minuscole variazioni dovute al rumore di fondo.
È per questa ragione che la scrittura di una sequenza di bit non può mai contenere più di due zeri consecutivi.
Questa è una grossa limitazione che ci impedisce, per esempio, di trascrivere un testo direttamente sul disco: la lettera A
maiuscola, codice ASCII 01000001
, sarebbe proibita!
Ma i problemi non sono finiti qui.
Durante la lettura, la testina capta un flusso continuo di bit e il Drive II non prevede un mezzo meccanico per segnalare l’inizio di un segmento valido.
Per ovviare a questo inconveniente, il Drive II fu progettato imponendo che un byte valido debba sempre avere un bit 1
in testa.
La lettera A
maiuscola è a maggior ragione proibita.
Inoltre, per garantire un corretto allineamento, prima di ogni segmento dati valido è anteposta una particolare sequenza di 12 bit, pari a 1111111100
, che porta il sistema di lettura ad allinearsi sempre sul primo 1
.
Questo codice viene anche chiamato “self-synced FF”.
Come sui più recenti floppy da 3.5" e sui dischi rigidi, il disco è suddiviso in tracce concentriche e per ogni traccia i dati sono segmentati in settori, così da poter gestire letture e scritture in gruppi più piccoli. Il contenuto informativo di una traccia sarà qualcosa di simile alla figura di seguito.
La particolarità dell’Apple Drive II, che lo differenzia da altri modelli dell’epoca, sta nella estrema semplicità dell’hardware. Per contro, la gestione della geometria del disco è interamente a carico del software.
Dunque, è necessario in qualche modo distinguere un settore da un altro. Per questa ragione ciascun a settore è associato un campo di descrizione, detto address field.
L’address field contiene le informazioni di identificazione del settore all’interno del disco, ad esempio “traccia numero 2, settore numero 7”.
Questi valori richiedono un byte ciascuno, ma non possono essere trascritti direttamente perché potrebbero non essere rappresentabili su disco.
Per questo, ciascun numeretto viene prima codificato con un approccio odd-even: data la sequenza 10101010
, si sostituiscono agli 0
i bit dispari del byte da codificare.
A partire dalla stessa sequenza si fa lo stesso con i bit pari, ottenendo due byte distinti.
È evidente che, così facendo, il primo bit sarà sempre 1
e non si potranno mai avere 0
consecutivi.
All’address field segue il data field, che contiene l’informazione vera e propria. I dati non sono codificati con la tecnica odd-even perché comporterebbe un raddoppio nello spazio occupato.
Sono state preferite altre codifiche ad efficienza maggiore, dette 5-and-3 e 6-and-2, ma la loro implementazione trascende dallo scopo di questo racconto, per cui rimando direttamente al manuale Beneath Apple DOS per approfondire. Ci è sufficiente sapere che in un settore possiamo immagazzinare 256 byte, che adottando la codifica più efficiente lievitano a 342 byte.
È importante sottolineare che entrambi i fields hanno un prologo univoco di tre caratteri, importanti per distinguerli e decodificarli correttamente. I primi due byte di entrambi i campi, D5 AA, sono codici riservati che non possono essere utilizzati altrove e fungono da ulteriore sistema di allineamento.
Come accennato, ciascun field è preceduto dalle sequenze di self-sync FF. Oltre a servire da pattern di sincronizzazione, questi gap fungono da buffer per la scrittura.
Siccome l’operazione di scrittura è implementata in software ed avviene “alla cieca”, queste aree di buffer servono ad avere margine sufficiente per evitare che la riscrittura di un field sovrascriva porzioni di settori adiacenti.
Il sistema del floppy drive dell’Apple II è fondamentalmente composto di due schede: il drive ne contiene una, denominata analog card, che si occupa di effettuare la conversione dal mondo analogico al mondo digitale; la seconda scheda è contenuta all’interno dell’unità centrale, detta interface card, e si occupa del coordinamento ad alto livello. Include, fra le altre cose, una ROM per il codice di bootstrap ed una serie di chip necessari a serializzare e deserializzare i byte da scambiare con il processore. Di fatto, l’emulatore di floppy dovrà spacciarsi per la analog card.
Il drive resta in attesa finché il segnale drive enable è disattivato.
Attivare il drive significa portarlo in modalità lettura, e da quel momento lungo il filo di read inizieranno a scendere i dati letti dalla testina.
La logica di digitalizzazione ripulisce tutto quel che la testina è in grado di captare e restituisce un segnale squadrato costituito da un treno di impulsi, uno per ciascun bit 1
rilevato, e con spaziatura di 4 µs.
Il drive commuta in scrittura non appena si attiva anche il segnale di write request.
In questa fase viene accesa la testina di scrittura che col proprio campo magnetico va a sovrascrivere qualsiasi cosa ci fosse in precedenza.
Commutare o lasciare invariato il write determina il nuovo contenuto del disco, ovvero se scrivere un bit 1
oppure uno 0
.
Resta la gestione del posizionamento della testina sulla traccia, di fatto tramite un motore passo passo controllato direttamente tramite le quattro fasi con attivazione sequenziale. Per cambiare traccia in realtà è necessario attivare due fasi consecutive, probabilmente per garantire una maggiore precisione nell’allineamento motore-traccia. Le “tracce intermedie” venivano utilizzate per particolari meccanismi di protezione anticopia, che però non sono stato interessato ad emulare.
L’emulatore
Per implementare l’emulatore ho seguito le orme del progetto SDISK II ed ho utilizzato soltanto un microcontrollore. Le immagini disco da servire vengono prelevate da una comune SD card formattata in FAT e servite sull’interfaccia disco.
Per scegliere l’immagine e monitorare lo stato del drive ho predisposto in prima battuta una interfaccia a riga di comando via seriale, poi una interfaccia semi grafica tramite display OLED e selettore, che non richiede la presenza di un secondo computer.
La scelta del microcontrollore è ricaduta sull’architettura AVR - per intendersi, la stessa della prima serie Arduino - perché avevo una scheda di sviluppo con una variante più performante da utilizzare per le prime prove. Inoltre, anche l’SDISK II è basato su AVR ed il suo codice è stato un buon punto di partenza.
Tutta la sezione “moderna” opera a 3.3V, un vincolo imposto dalla scheda SD e dal display. L’interfaccia con l’Apple II richiede particolare cautela, non solo per le differenti tensioni operative (5V vs 3.3V), ma anche per gli standard logici non compatibili (LVCMOS vs TTL): anche riscalando le tensioni, le due parti potrebbero non capirsi alla perfezione.
Nella prima versione sperimentale mi sono accontentato di usare quel che avevo: traslatori di livello economici ma lenti per scendere verso l’emulatore e qualche buffer per rigenerare il segnale dell’emulatore. Fortunatamente esistono dei circuiti integrati adatti allo scopo che ho inserito nella versione realizzata su PCB.
Ho approfittato anche dei LED presenti sulla scheda di sviluppo per avere un feedback visivo dello stato del lettore, per esempio riportando la posizione della testina.
Lettura
Esistono più formati con cui possono essere digitalizzate le immagini disco dell’Apple II, ma per mantenere decenti le prestazioni dell’emulatore ho mantenuto il formato .NIC
utilizzato sull’SDISK II.
Si tratta di un file binario che memorizza i bit grezzi fisicamente letti dalla testina del lettore.
Il file è strutturato per settori e tracce crescenti.
Ho riportato un paio di estratti nella coppia di figure che seguono, evidenziando la struttura dati ed il sopra citato address field.
Comprendendo i gap, la dimensione di ciascun settore nel .NIC è di 416 byte. A questi, sono aggiunti 96 byte di fill a zero, così da mantenere ciascun settore allineato a blocchi di 512 byte. Questa scelta è dettata dalla specifica di funzionamento di lettura e scrittura da scheda SD, che richiede di operare a multipli di un blocco, proprio pari a 512 byte.
Data la memoria limitata presente sul microcontrollore, non è possibile scaricare tutta l’immagine .NIC in RAM all’inizio della lettura.
Piuttosto, noti la traccia ed il settore che devono essere forniti al computer, è sufficiente posizionare l’indirizzo di lettura della scheda SD nel punto desiderato ed, ogni 4 microsecondi, “tirare fuori” un bit, generando o meno un impulso.
Questo può essere svolto interamente a controllo di programma, cosa effettivamente fatta nel progetto originale dell’SDISK II mediante l’uso di alcune routine in assembly.
Nella mia implementazione ho voluto delegare la generazione del segnale di lettura ad una periferica hardware presente nel microcontrollore, detta timer PWM. Tale periferica permette di generare autonomamente un segnale ad onda quadra, programmandone il periodo e la frazione di periodo in cui il segnale deve “rimanere basso”.
Questa periferica non è altro che un contatore con un singolo pin di uscita associato.
Se ne possono programmare il valore massimo - il periodo - ed un valore di soglia.
Finché il valore del contatore è minore del valore di soglia, il pin rimane a 0
.
Superato il valore intermedio, l’uscita viene portata ad 1
.
Volendo generare una sequenza di 1
è sufficiente programmare la periferica con un periodo di 4 µs e un tempo di attesa di 3 µs.
Impostando il valore di soglia ad un valore superiore al periodo, non avverrà mai il “match” e l’uscita resterà nulla, generando uno 0
.
Al termine di ogni periodo il software va ad aggiornare il valore di soglia in accordo con il bit effettivo da trasmettere. Non è più necessario effettuare una attesa attiva perché la periferica PWM è programmata per richiedere una interruzione al termine di ogni periodo. Ad ogni interruzione il programma ha poco meno di 3 microsecondi per leggere un bit dalla scheda SD e decidere se abilitare l’uscita del PWM per l’impulso.
Il Drive II non ha coscienza della suddivisione in settori del disco, per cui la traccia corrente viene comunque letta per intero e trasferita alla scheda di interfaccia. Le operazioni su un settore specifico sono effettuate in software, leggendo ed interpretando ciascun address field per decidere se scartare i dati provenienti da altri settori. Per l’emulatore, questo si traduce in una specifica di funzionamento molto semplice: finché il segnale di drive enable è attivo, il firmware dell’emulatore cicla la lettura di tutti i settori della stessa traccia. Il controllo della traccia avviene attraverso i quattro fili di fase PHI0-PHI3, che nel drive originale controllavano direttamente il motore passo-passo di posizionamento della testina. Nell’emulatore, il cambio della traccia è gestito dal codice principale, non c’è necessità di scomodare interruzioni poiché si tratta di segnali molto lenti, tanto che si riescono ad osservare ad occhio nudo tramite dei LED.
La routine di controllo traccia monitora i quattro segnali di fase e incrementa opportunamente il contatore interno di traccia in base alla direzione impartita. Il cambio traccia potrebbe avvenire in qualunque momento, anche nel bel mezzo della lettura di un settore. Per non complicare troppo la logica, ho deciso di non interrompere mai la lettura di un settore e di applicare il cambio traccia solo quando viene fatto il caricamento di un nuovo blocco da SD. Tanto, nel mondo reale, i dati letti durante il cambio di traccia sarebbero comunque invalidi e da scartare.
Il controllo di traccia è incrementale e senza alcun feedback: difatti, il computer effettua, ad ogni accensione, un azzeramento forzato della posizione della testina muovendo il motore alla cieca. È questo il motivo del rumore che fa il disk drive all’accensione. È stato interessante un primo esperimento di lettura in cui non avevo implementato la gestione delle tracce: la prima traccia veniva letta con successo ma, passando alla successiva, il software si accorgeva dell’incongruenza tramite i campi dell’address field. Anche in questo caso veniva tentato l’azzeramento della testina nella speranza di riallinearsi alla traccia giusta, purtroppo senza successo.
Il vincolo temporale più stringente in lettura è imposto dai 4 µs di periodo di bit all’interno dello stesso settore. Invece, non c’è nessuna fretta di servire un nuovo settore. Sia chiaro, le routine software dell’Apple II implementano un timeout che termina la lettura se il settore richiesto non è leggibile entro un certo tempo. Ma questo intervallo di tempo è sufficiente per svolgere un po’ di burocrazia. Per servire un nuovo settore è necessario indirizzare correttamente il blocco corretto dalla scheda SD, operazione effettuata a controllo di programma e che richiede qualche decina di µs.
Come dicevo, nel primo approccio non ho fatto uso di buffering di settore. I dati sono letti bit per bit dalla scheda SD ogni 4 µs e serviti immediatamente sulla linea read. La scheda SD impone che i 512 byte di blocco debbano essere letti per intero, per cui al termine del settore restano comunque 96 byte da ignorare, che aggiungono un ulteriore overhead prima di poter iniziare a leggere il settore successivo. Per abbattere questi tempi morti ho sfruttato la presenza della periferica di DMA.
Il DMA, o Direct Memory Access, è un componente del microcontrollore che consente di effettuare scambio dati fra la memoria ed una periferica senza l’intervento diretto del processore. In questo caso la periferica è la SPI che gestisce la comunicazione con la scheda SD.
Con il DMA, l’overhead si riduce al tempo tecnico necessario per impostare sul DMA l’indirizzo del blocco SD da leggere, dopodiché questo verrà caricato in RAM in circa 300 µs. Non importa attendere che il buffer DMA sia stato caricato per intero prima di iniziare a servire i dati sulla linea read, tanto la scheda SD è molto più veloce.
Non mi è sembrato utile ottimizzare ulteriormente la fase di lettura, la scelta di usare il DMA si è rivelata utile più che altro per scrittura e formattazione.
Scrittura
Bisogna differenziare la scrittura di un floppy in due casi distinti, ovvero la sovrascrittura di un settore e la formattazione. Sebbene si tratti in entrambi i casi di operazioni di sovrascrittura, nel primo si va ad alterare il contenuto di un singolo data field, mentre nel secondo caso viene riscritta l’intera traccia, compreso l’address field. La formattazione opera quindi su settori multipli ed ha dei vincoli temporali molto più stringenti, tanto che il primo prototipo di tinyDisk non era in grado di supportare.
Considerando il caso più semplice, la sovrascrittura di un settore, si possono fare le seguenti assunzioni:
- prima di iniziare la scrittura di un settore, la routine Apple attende sempre di ricevere l’address field corretto per potersi allineare sul settore corretto, quindi inizia a sovrascrivere il data field che segue;
- l’address field non viene mai sovrascritto, serve per allinearsi al settore corretto;
0xD5
è un byte univoco e riservato, che può essere usato come marcatore di allineamento nella decodifica del segnale write;- durante la scrittura di un settore non verrà mai cambiata traccia, sarebbe alquanto controproducente.
La strategia per convertire il segnale di write in uno stream binario è semplice: per completare un byte è necessario aver decodificato almeno 8 simboli.
Ogni transizione rappresenta un 1
.
Lo 0
, ovvero la mancanza di una transizione in uno slot di 4 µs, richiede che sia predisposto un timer con periodo leggermente maggiore di 4 µs, per tenere conto delle minime fluttuazioni del clock dell’Apple II.
La routine software dell’Apple II inizia sempre riscrivendo la sequenza di self-synced FF, che può essere scartata.
La prima informazione utile, e fondamentale per potersi allineare, è il prologo del data field, ovvero il byte 0xD5
.
Rilevato quello, tutti i byte che seguono possono essere salvati in un buffer che sarà opportunamente caricato nel blocco corretto della SD, sovrascrivendo il file .NIC
originario.
Al termine della scrittura, l’Apple II torna in lettura per allinearsi, eventualmente, con un altro settore da sovrascrivere.
In caso di più settori da sovrascrivere, il mio firmware non è in grado di gestire una nuova scrittura finché non è terminato il write back sulla scheda SD. Fortunatamente, questo tempo non è così lungo e l’Apple II ha sufficiente pazienza e non va in errore. Ovviamente è previsto un timeout nel caso in cui non dovesse essere mai rilevato l’address field da sovrascrivere.
Storia ben diversa riguarda la formattazione. Poiché la formattazione va a ridefinire la geometria del disco, in questo caso la traccia viene interamente sovrascritta senza alcuna attesa fra un data field e l’address field del settore successivo. Purtroppo il microcontrollore non ha abbastanza RAM per per memorizzare una traccia - già un singolo settore porta via 512 byte dei 2kB disponibili.
La soluzione che mi è sembrata più ragionevole è stata tentare di implementare una pipeline con l’unico buffer di settore disponibile. Il principio è il seguente: registrata la richiesta di scrittura, il buffer viene popolato decodificando il segnale write, come per la sovrascrittura del settore. Al termine, c’è un breve periodo di grazia rappresentato dalla scrittura del gap fra settori, che il firmware sfrutta per istruire la periferica DMA a trasferire il buffer in scheda SD. A questo punto la scrittura in SD Card può procedere autonomamente senza l’intervento della CPU, che è libera di decodificare il nuovo settore in arrivo.
Pur avendo un singolo buffer è raro che la lettura del nuovo settore sovrascriva una parte non ancora trasferita sulla memoria di massa, perché sussiste un fattore x8 fra il data rate dell’SPI in uscita e del Drive II in ingresso. La criticità temporale, in effetti, risiede nel tempo di finalizzazione della scrittura in scheda SD: queste memorie, dopo aver immesso il buffer tramite SPI, richiedono un tempo ulteriore per effettuare la cancellazione fisica e la sovrascrittura, tempo che varia in base al modello e all’età del supporto di memoria. Questo ritardo aggiuntivo ha dato qualche grattacapo, ma si può mitigare forzando il salvataggio dei settori di una stessa traccia in aree fisicamente contigue della scheda SD - di fatto imponendo una blanda non frammentazione dei file .NIC - e permettendo una ottimizzazione nelle operazioni fisiche di cancellazione e sovrascrittura.
Giudizio finale e riferimenti
L’obiettivo di sostituire il drive originale con un emulatore è stato raggiunto con successo. L’ho validato sul campo provando una serie di immagini disco di software da esposizione: giochi, applicativi, demo… in lettura non ho avuto problemi degni di nota. Lo stesso vale per la scrittura, testata sia salvando codice BASIC dal DOS, sia modificando arbitrariamente il contenuto dei settori tramite il software CopyII+. La formattazione meriterebbe ancora delle indagini perché, sporadicamente, fallisce quando si ha a che fare con schede SD particolarmente lente o vissute.
Nonostante le semplificazioni, che limitano la fedeltà di emulazione, il progetto si è rivelato una sfida ardua ed averlo realizzato in casa è stato utile per imparare qualcosa di nuovo. Ho provato a riassumere i tratti più salienti di questo progetto senza scendere eccessivamente nel dettaglio sperando di suscitare curiosità ed interesse sull’argomento. Lascio ai più volenterosi la possibilità di approfondire direttamente sul codice del progetto rilasciato con licenza libera, che sia con l’obiettivo di realizzarne una propria versione o proporre modifiche e miglioramenti.
- Repository del progetto
- Retromagazine, rivista online dove questo articolo è stato pubblicato
- SDISK II, home page del progetto
- Beneath Apple DOS
- The Amazing Disk II Controller Card, descrizione del controller disco a cura del progettista del “Floppy Emu”
- Understanding the Apple II