Obligement - L'Amiga au maximum

Mercredi 01 décembre 2021 - 14:12  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in english


Twitter

Suivez-nous sur Twitter




Liste des jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z,
ALL


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Sites de téléchargements
Associations
Pages Personnelles
Matériel
Réparateurs
Revendeurs
Presse et médias
Programmation
Logiciels
Jeux
Scène démo
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Bidouille : Développement du lecteur de disquette Arduino Amiga, alias DrawBridge
(Article écrit par Robert Smith et extrait d'amiga.robsmithdev.co.uk - juin 2021)


Note : traduction par David Brunet.

Ce qui suit est la description et le journal du développement de ma solution de lecteur de disquette Arduino Amiga, alias DrawBridge.

Plan de l'article

Mon parcours sur Amiga

Je dois ma carrière à l'Amiga, plus précisément à l'A500+ que mes parents m'ont acheté pour Noël, quand j'avais 10 ans. Au début, je l'utilisais pour les jeux, mais à un moment donné, j'ai commencé à vouloir voir ce qu'il pouvait faire d'autre. J'ai bidouillé avec Deluxe Paint 3 et j'ai appris à connaître le Workbench.

Amiga 500+
L'Amiga 500 Plus

Chaque mois, j'achetais le populaire magazine Amiga Format. Un mois, j'ai eu une copie gratuite d'AMOS. J'ai participé au concours d'Amiga Format intitulé "Write A Game In AMOS" (programmer un jeu en AMOS) lorsqu'AMOS Professional a été publié sur une disquette de couverture, et j'ai été l'un des douze (je crois) gagnants avec mon jeu In The Pipe Line. Et il fallut vraiment leur courir après pour recevoir les prix !

AMOS
AMOS - The Creator

Plus tard, j'ai utilisé mon A500+ dans le cadre de mes projets de GCSE (NDLR : équivalent du brevet en France) et du A-Level (NDLR : équivalent du Bac en France) grâce à Highspeed Pascal, qui était compatible avec Turbo Pascal sur PC.

Quoi qu'il en soit, c'était il y a longtemps, et je dispose encore des boîtes de disquettes, et d'un A500+ qui ne fonctionne plus. J'ai alors voulu sauvegarder ces disquettes sur mon ordinateur, pour la préservation et la nostalgie.

Le site Amiga Forever a une excellente liste d'options qui incluent le matériel, et l'utilisation abusive de deux lecteurs de disquette dans un PC. Malheureusement, aucune de ces options n'était possible avec le matériel moderne, et les contrôleurs KryoFlux/Catweasel sont trop chers. J'ai été vraiment surpris de voir que le code source de la plupart des logiciels ne soit pas ouvert au public.

Passionné d'électronique et ayant bidouillé avec des appareils Atmel (AT89C4051) à l'université, j'ai décidé de jeter un coup d'oeil à l'Arduino (merci à GreatScott pour son inspiration qui m'a montré à quel point il est facile de se lancer). Je me suis demandé si cela était possible.

J'ai donc cherché sur Google "Arduino floppy drive reading code" (code de lecture de disquette Arduino), et après avoir ignoré tous les projets qui utilisaient le lecteur pour jouer de la musique, je n'ai pas vraiment trouvé de solutions. J'ai trouvé quelques discussions dans certains groupes suggérant que ce ne serait pas possible. J'ai trouvé un projet basé sur un FPGA qui était très intéressant à lire, mais ce n'était pas la direction que je voulais prendre, donc la seule option était de construire moi-même une solution.

Recherche

Lorsque j'ai commencé ce projet, je n'avais pas la moindre idée du fonctionnement d'un lecteur de disquette, et encore moins de la manière dont les données étaient codées sur celles-ci. Les sites Web suivants m'ont été d'une aide précieuse pour comprendre ce qu'il se passe et comment ils fonctionnent :
Hypothèses

Sur la base des recherches effectuées, je savais maintenant théoriquement comment les données étaient écrites sur la disquette et comment la disquette tournait.

J'ai commencé à calculer quelques chiffres. Sur la base de la vitesse de rotation de la disquette double densité (300 tr/min) et de la façon dont les données sont stockées (80 pistes, 11 secteurs par piste et 512 octets par secteur, codées à l'aide de la MFM), pour lire les données avec précision, je devais être capable d'échantillonner les données à 500 kHz ; c'est assez rapide si l'on considère que l'Arduino ne tourne qu'à 16 kHz.

Dans les sept tentatives qui suivent, je ne parle que du côté Arduino. Passez au chapitre décodage.

Tentative 1

Tout d'abord, j'ai dû trouver le matériel et l'interface avec le lecteur de disquette. J'ai pris le lecteur de disquette d'un vieux PC au travail, et j'ai récupéré son câble IDE par la même occasion.

Ci-dessous, une photo du lecteur de disquette provenant d'un vieux PC :

lecteur de disquette PC

En étudiant le brochage du lecteur, j'ai réalisé que je n'avais besoin que de quelques fils, et après avoir regardé le lecteur, j'ai réalisé qu'il n'utilisait pas non plus l'entrée 12 V.

Pour faire tourner le lecteur, il a fallu sélectionner le lecteur et activer le moteur. Le déplacement de la tête de lecture est simple. Il suffisait d'activer ou de désactiver la broche /DIR, puis d'envoyer une impulsion à la broche /STEP. Vous pouviez savoir si la tête avait atteint la piste 0 (la première piste) en surveillant la broche /TRK00.

J'étais curieux à propos de la broche /INDEX. Elle émet une impulsion à chaque rotation. Comme l'Amiga ne l'utilise pas pour trouver le début de la piste, je n'en avais pas besoin et je pouvais l'ignorer. Après cela, il suffit de choisir le côté de la disquette à lire (/SIDE1) et de connecter /RDATA.

Avec l'exigence d'un taux de transfert de données élevé, ma première pensée a été de trouver un moyen de rendre cela moins problématique en essayant de réduire les exigences sur ce taux.

Le plan était d'utiliser deux registres à décalage de 8 bits (SN74HC594N) pour réduire la fréquence d'échantillonnage requise par un facteur de 8. J'ai utilisé ce qu'Ebay appelle "Pro Mini atmega328 Board 5V 16M Arduino Compatible Nano" (donc je ne sais pas ce que c'est officiellement, mais cela fonctionne sur l'Uno !) pour mettre en mémoire tampon ces données parallèles et les envoyer au PC en utilisant son interface série/USART. Je savais que cela devait fonctionner plus vite que 500 kbauds (avec toutes les surcharges série impliquées aussi).

Après avoir abandonné la bibliothèque série standard de l'Arduino, j'ai été très heureux de constater que je pouvais configurer l'USART sur l'Arduino à 2 Mbauds, et avec l'une de ces cartes d'extension F2DI (sur Ebay, elle est nommée "Basic Breakout Board For FTDI FT232RL USB to Serial IC For Arduino" - voir ci-dessous), je pouvais envoyer et recevoir des données à cette vitesse (62,5 kHz) mais je devais le faire avec précision.

Voici la carte de dérivation FTDI qui s'adapte parfaitement à l'interface de la carte Arduino :

FTDI
Carte FTDI

Tout d'abord, j'ai utilisé l'Arduino pour configurer l'un des registres à décalage de 8 bits, un seul des 8 bits étant cadencé à fréquence élevée. L'autre recevait une impulsion directement du lecteur de disquette (fournissant ainsi une conversion série vers parallèle).

L'image suivante est une image folle de la platine d'expérimentation sur laquelle j'ai construit ceci à l'époque :

tentative 1

J'ai utilisé un des chronomètres de l'Arduino pour générer un signal de 500 kHz sur une de ses broches de sortie et comme le matériel gère cela, il est très précis ! En tout cas, mon multimètre l'a mesuré comme étant exactement 500 kHz.

Le code a fonctionné, j'ai cadencé 8 bits de données à 62,5 kHz, laissant le processeur de l'Arduino à peine utilisé. Cependant, je ne recevais rien de significatif. A ce stade, j'ai réalisé que je devais regarder de plus près les données réelles qui sortaient du lecteur de disquette. J'ai donc acheté un vieil oscilloscope bon marché sur Ebay (Gould OS300 20Mhz Oscilloscope) pour vérifier ce qu'il se passait.

En attendant que l'oscilloscope arrive, j'ai décidé d'essayer autre chose.

Voici un bout du code utilisé pour lire les données des registres à décalage :

void readTrackData() {
    byte op;
    
    for (int a=0; a<5632; a++) {
        // We'll wait for the "byte" start marker
        while (digitalRead(PIN_BYTE_READ_SIGNAL)==LOW) {};
        // Read the byte
        op=0;
        if (digitalRead(DATA_LOWER_NIBBLE_PB0)==HIGH) op|=1;
        if (digitalRead(DATA_LOWER_NIBBLE_PB1)==HIGH) op|=2;
        if (digitalRead(DATA_LOWER_NIBBLE_PB2)==HIGH) op|=4;
        if (digitalRead(DATA_LOWER_NIBBLE_PB3)==HIGH) op|=8;
        if (digitalRead(DATA_UPPER_NIBBLE_A0)==HIGH) op|=16;
        if (digitalRead(DATA_UPPER_NIBBLE_A1)==HIGH) op|=32;
        if (digitalRead(DATA_UPPER_NIBBLE_A2)==HIGH) op|=64;
        if (digitalRead(DATA_UPPER_NIBBLE_A3)==HIGH) op|=128;

        writeByteToUART(op);
        
        // Wait for high to drop again 
        while (digitalRead(PIN_BYTE_READ_SIGNAL)==HIGH) {};
    }
}

Tentative 2

J'ai décidé que les registres à décalage, bien qu'étant une bonne idée, n'étaient probablement pas utiles. J'étais capable de lire facilement 8 bits en une seule fois, mais je me suis rendu compte que je ne pouvais pas être sûr que tous les bits étaient correctement synchronisés en premier lieu. En lisant la documentation, il était suggéré que les données étaient plutôt des impulsions courtes plutôt que des impulsions haut et bas.

J'ai retiré les registres à décalage et je me suis demandé ce qu'il se passerait si j'essayais de vérifier une impulsion du lecteur dans une interruption (ISR) en utilisant le signal 500 kHz précédemment configuré. J'ai reconfiguré l'Arduino pour qu'il génère l'ISR, et après avoir résolu le problème des bibliothèques de l'Arduino qui m'empêchaient d'utiliser l'ISR que je voulais, je suis passé au Timer2 (l'un des trois chronomètres du microcontrôleur).

J'ai écrit un court ISR qui décalait d'un bit vers la gauche un octet unique global, puis si la broche connectée à la ligne de données du lecteur de disquette était "LOW" ("BAS", les impulsions sont basses), j'y plaçais un 1 par "OR" (OU). Toutes les huit fois que j'ai fait cela, j'ai écrit l'octet complet sur l'USART.

Cela ne s'est pas passé comme prévu ! L'Arduino a commencé à se comporter de manière très erratique et étrange. Je me suis vite rendu compte que l'ISR prenait plus de temps pour s'exécuter que le temps entre les appels à ce celui-ci. Je pouvais recevoir une impulsion toutes les deux microsecondes et en me basant sur la vitesse de l'Arduino et en supposant que chaque instruction C correspondait à un cycle de code machine, j'ai réalisé que je pouvais avoir au maximum 32 instructions. Malheureusement, la plupart seraient plus d'une instruction, et après une recherche sur Google, j'ai réalisé que la surcharge sur le démarrage d'un ISR était de toute façon massive. Sans compter que les fonctions digitalRead étaient très lentes.

J'ai abandonné la fonction digitalRead pour accéder directement aux broches du port. Cela ne m'a toujours pas aidé et n'était pas assez rapide. N'étant pas prêt à abandonner, j'ai mis de côté cette approche et j'ai décidé d'aller de l'avant et d'essayer autre chose.

C'est alors que l'oscilloscope que j'avais acheté est arrivé, et il a fonctionné ! C'était un vieil oscilloscope qui était probablement plus vieux que moi ! Mais il a quand même fait le travail parfaitement. Si vous ne savez pas ce qu'est un oscilloscope, consultez EEVblog #926 - Introduction à l'oscilloscope, et si vous aimez l'électronique, je vous suggère d'en regarder quelques autres et de parcourir le site d'EEVBlog.

Voici mon vieil oscilloscope récemment acheté (Gould OS300 20Mhz) :

Gould OS300

Après avoir connecté le signal 500 kHz à un canal et la sortie du lecteur de disquette à un autre, il était évident que quelque chose n'allait pas. Le signal 500 kHz était une onde carrée parfaite en l'utilisant comme déclencheur (ou bascule), les données de la disquette étaient éparpillées. Je pouvais voir les impulsions, mais c'était un peu flou. De même, si je déclenchais à partir du signal du lecteur de disquette, le signal carré de 500 kHz était éparpillé et non synchronisé avec lui.

Voici des photos des tracés sur l'oscilloscope déclenché par les deux canaux. Vous ne pouvez pas vraiment le voir, mais sur le canal qui n'est pas déclenché, il y a des milliers de lignes fantômes :

tracés

Individuellement, je pouvais mesurer les impulsions des deux signaux à 500 kHz, ce qui n'avait pas de sens, car s'ils fonctionnent tous les deux à la même vitesse mais ne se déclenchent pas pour que vous puissiez voir les deux signaux correctement, alors quelque chose ne va pas.

Après avoir beaucoup expérimenté avec les niveaux de déclenchement, j'ai réussi à comprendre ce qu'il se passait. Mon signal était un 500 kHz parfait, mais en regardant le signal du lecteur de disquette, ils étaient espacés correctement, mais pas tout le temps. Entre les groupes d'impulsions, il y avait une dérive d'erreur, et aussi des trous dans les données qui rendaient le signal totalement désynchronisé.

Si l'on se souvient des recherches précédentes, le lecteur était censé tourner à 300 tours par minute, mais il se peut qu'il ne tourne pas exactement à 300 tours par minute, et que le lecteur qui a écrit les données ne tourne pas non plus exactement à 300 tours par minute. Ensuite, il y a l'espacement entre les secteurs et les écarts entre les secteurs. Il est clair qu'il y avait un problème de synchronisation, et synchroniser le signal de 500 kHz au lecteur de disquette au début d'une lecture n'allait pas fonctionner.

J'ai également découvert que l'impulsion du lecteur de disquette était extrêmement courte, bien que vous puissiez modifier cela en changeant la résistance de rappel, et si la synchronisation n'était pas exactement correcte, l'Arduino pouvait manquer une impulsion.

Quand j'étais à l'université (Université de Leicester), j'ai suivi un module appelé Système Embarqué. Nous avons étudié les microcontrôleurs Atmel 8051. L'un des projets consistait à compter les impulsions d'une station météo simulée (codeur rotatif). A l'époque, j'échantillonnais la broche à intervalles réguliers, mais ce n'était pas très précis.

Le professeur Pont, chargé des cours de ce module, avait suggéré que j'aurais dû utiliser les fonctions de compteur matériel de l'appareil (je ne le savais même pas à l'époque).

J'ai vérifié la fiche technique de l'ATMega328 et, bien sûr, chacun des trois chronomètres pouvait être configuré pour compter les impulsions déclenchées par une entrée externe. La vitesse n'était donc plus un problème. Tout ce dont j'avais besoin était de savoir si une impulsion s'était produite dans une fenêtre de temps de deux microsecondes.

Tentative 3

J'ai ajusté le circuit Arduino pour réinitialiser le chronomètre 500 kHz lorsque la première impulsion était détectée et à chaque fois que le chronomètre 500 kHz était dépassé, je vérifiais la valeur du compteur pour voir si une impulsion avait été détectée. J'ai ensuite effectué la même séquence de décalage de bits et tous les 8 bits, j'ai écrit un octet sur l'USART.

Les données arrivaient et j'ai commencé à les analyser sur le PC. Dans les données, j'ai commencé à voir ce qui ressemblait à des données valides. Un mot de synchronisation bizarre apparaissait, ou des groupes de séquences 0xAAAA, mais rien de fiable. Je savais que j'étais sur une bonne voie, mais il me manquait encore quelque chose.

Tentative 4

J'ai réalisé qu'au fur et à mesure que les données étaient lues, les données du lecteur étaient probablement déphasées par rapport à mon signal de 500 kHz. Je l'ai confirmé en lisant 20 octets à chaque fois que je débutais la lecture.

En cherchant sur la façon de gérer ce problème de synchronisation, je suis tombé sur l'expression "boucle à verrouillage de phase" ou BVP ("PLL" en anglais). En termes très simples, pour ce que nous faisons, la boucle à verrouillage de phase ajuste dynamiquement la fréquence de l'horloge (les 500 kHz) pour compenser la dérive de fréquence et la variance du signal.

La résolution du chronomètre n'était pas assez élevée pour la faire varier par de petites quantités (par exemple : 444 kHz, 470 kHz, 500 kHz, 533 kHz, 571 kHz, etc.) et pour réaliser cela correctement, j'aurais probablement besoin que le code tourne beaucoup plus vite.

Les chronomètres de l'Arduino fonctionnent en comptant jusqu'à un nombre prédéfini (dans ce cas 16 pour 500 kHz) puis ils mettent un registre de débordement et recommencent à partir de 0. La valeur actuelle du compteur peut être lue et écrite à tout moment.

J'ai ajusté le circuit pour qu'il attende dans une boucle jusqu'à ce que le chronomètre déborde, et quand il a débordé, j'ai vérifié pour une impulsion comme avant. La différence cette fois-ci est que lorsqu'une impulsion est détectée dans la boucle, je réinitialise la valeur du compteur du chronomètre à une position de phase prédéfinie, resynchronisant ainsi le chronomètre à chaque impulsion.

J'ai choisi la valeur que j'ai écrite au compteur du chronomètre de façon à ce qu'il déborde à une microseconde de l'impulsion de détection (à mi-chemin) de sorte que la prochaine fois que le chronomètre débordera, l'impulsion aura été espacée de deux microsecondes.

Et cela a fonctionné ! Je pouvais maintenant lire des données presque parfaites sur la disquette. Je recevais encore beaucoup d'erreurs de somme de contrôle, ce qui était ennuyeux. J'ai résolu la plupart d'entre elles en relisant continuellement la même piste sur la disquette jusqu'à ce que j'aie les onze secteurs avec des sommes de contrôle d'en-tête et de données valides.

J'étais intrigué à ce stade, alors j'ai rebranché le tout à l'oscilloscope pour voir ce qu'il se passait maintenant, et comme je l'avais deviné, je pouvais maintenant voir les deux tracés car elles étaient synchronisées l'une avec l'autre :

tracés

J'aimerais bien y voir un peu plus clair, si quelqu'un veut me faire don d'un bel oscilloscope numérique haut de gamme (par exemple un de ces Keysight !), j'apprécierais vraiment !

Tentative 5

Je me suis demandé si je pouvais l'améliorer. En regardant le code, en particulier la boucle de lecture interne (voir ci-dessous), j'avais une boucle "while" qui attendait le débordement et ensuite un "if" interne qui cherchait une impulsion pour se synchroniser.

Voici une partie du code utilisé pour lire les données et se synchroniser avec elles :

register bool done = false;

// Wait for 500khz overflow 
while (!(TIFR2&_BV(TOV2))) {   
    // falling edge detected while waiting for the 500khz pulse.  
    if ((TCNT0) && (!done)) {
        // pulse detected, reset the timer counter to sync with the pulse
        TCNT2=phase;     

        // Wait for the pulse to go high again
        while (!(PIN_RAW_FLOPPYDATA_PORT & PIN_RAW_FLOPPYDATA_MASK)) {};
        
        done = true;
    }
}

// Reset the overflow flag
TIFR2|=_BV(TOV2); 
                      
// Did we detect a pulse from the drive?
if (TCNT0) {                            
    DataOutputByte|=1;
    TCNT0=0;
}

Je me suis rendu compte que selon l'instruction exécutée dans les boucles ci-dessus, le temps entre la détection de l'impulsion et l'écriture de "TCNT2=phase;" pouvait changer en fonction du temps pris pour exécuter quelques instructions.

Réalisant que cela pouvait causer des erreurs ou des fluctuations dans les données et qu'avec la boucle ci-dessus, il était possible que je manque l'impulsion du lecteur (manquant ainsi un bit de resynchronisation), j'ai décidé de prendre l'astuce d'une de mes tentatives précédentes, l'ISR (interruption).

J'ai câblé l'impulsion de données à une deuxième broche sur l'Arduino. Les données étaient maintenant connectées au déclencheur Counter0 et maintenant aussi à la broche INT0. INT0 est l'une des plus hautes priorités d'interruption et devrait donc minimiser les délais entre le déclenchement et l'appel de l'ISR, et comme c'est la seule interruption qui m'intéresse, toutes les autres sont désactivées.

Tout ce que l'interruption devait faire était d'exécuter le code de resynchronisation ci-dessus, ce qui a changé le code pour ressembler à ceci :

// Wait for 500khz overflow 
while (!(TIFR2&_BV(TOV2))) {}   

// Reset the overflow flag
TIFR2|=_BV(TOV2);                       

// Did we detect a pulse from the drive?
if (TCNT0) {                            
    DataOutputByte|=1;
    TCNT0=0;
}

L'ISR ressemblait à ceci (notez que je n'ai pas utilisé "attachInterrupt" car cela ajoute aussi une surcharge à l'appel) :

volatile byte targetPhase;

ISR (INT0_vect) {
  TCNT2=targetPhase;
}

Mais la compilation produisait beaucoup trop de code pour être exécuté suffisamment rapidement. En fait, en désassemblant ce qui précède, on obtient :

push    r1
push    r0
in      r0,     0x3f	; 63
push    r0
eor     r1,     r1
push    r24 
lds     r24,    0x0102	; 0x800102 
sts     0x00B2, r24	; 0x8000b2 <__TEXT_REGION_LENGTH__+0x7e00b2>
pop     r24
pop     r0
out     0x3f,   r0	; 63
pop     r0
pop     r1
reti

En analysant le code, j'ai réalisé qu'il n'y avait que quelques instructions dont j'avais réellement besoin. Notant que le compilateur garderait la trace de tous les registres que j'aurais tapés, j'ai modifié l'ISR comme suit :

volatile byte targetPhase asm ("targetPhase");

ISR (INT0_vect) {
  asm volatile("lds __tmp_reg__, targetPhase");      
  asm volatile("sts %0, __tmp_reg__" : : "M" (_SFR_MEM_ADDR(TCNT2)));
}

Ce qui, désassemblé, a produit les instructions suivantes :

push	r1
push	r0
in	r0, 0x3f	; 63
push	r0
eor	r1, r1
lds	r0, 0x0102	; 0x800102 
sts	0x00B2, r0	; 0x8000b2 <__TEXT_REGION_LENGTH__+0x7e00b2>
pop	r0
out	0x3f, r0	; 63
pop	r0
pop	r1
reti

Il y a toujours trop d'instructions. J'ai remarqué que le compilateur ajoutait beaucoup d'instructions supplémentaires qui, pour mon application, n'avaient pas vraiment besoin d'être présentes. Je me suis donc renseigné sur l'ISR() et je suis tombé sur un deuxième paramètre ISR_NAKED. L'ajout de ce paramètre empêcherait le compilateur d'ajouter du code spécial, mais je serais alors responsable de la gestion des registres, de la pile et du retour correct de l'interruption. Je devais également maintenir le registre SREG, mais comme aucune des commandes que je devais appeler ne le modifiait, je n'avais pas besoin de m'en préoccuper.

Cela a changé le code de l'ISR pour devenir :

ISR (INT0_vect, ISR_NAKED) {
  asm volatile("push __tmp_reg__");                                   // Preserve the tmp_register
  asm volatile("lds __tmp_reg__, targetPhase");                       // Copy the phase value into the tmp_register
  asm volatile("sts %0, __tmp_reg__" : : "M" (_SFR_MEM_ADDR(TCNT2))); // Copy the tmp_register into the mem location where TCNT2 is
  asm volatile("pop __tmp_reg__");                                    // Restore the tmp_register
  asm volatile("reti");                                               // And exit the ISR
}

Ce que le compilateur a converti en :

push	r0
lds	r0, 0x0102	; 0x800102 
sts	0x00B2, r0	; 0x8000b2 <__TEXT_REGION_LENGTH__+0x7e00b2>
pop	r0
reti

Cinq instructions ! Parfait, ou du moins aussi rapide que cela devait l'être, prenant théoriquement 0,3125 microseconde pour s'exécuter ! Cela devrait maintenant signifier que la resynchronisation devrait se produire à des périodes précises après l'impulsion. Vous trouverez ci-dessous un diagramme temporel de ce qu'il se passe. C'est ainsi que l'on récupère les données d'un flux de données série qui n'a pas de signal d'horloge :

synchronisations

Cela a un peu amélioré les résultats. Ce n'est toujours pas parfait. Certaines disquettes se lisent parfaitement à chaque fois, d'autres prennent un temps fou et il faut sans cesse réessayer. Je ne sais pas si c'est parce que certaines disquettes sont restées très longtemps mal stockées, et que le magnétisme s'est dégradé à un niveau tellement faible que les amplificateurs des lecteurs ne peuvent pas le supporter. Je me suis demandé si cela n'avait pas quelque chose à voir avec le lecteur de disquette du PC, alors je l'ai connecté à un lecteur de disquette externe pour Amiga que j'avais, mais les résultats étaient identiques.

Tentative 6

Je me suis demandé s'il y avait autre chose à faire. Peut-être que le signal du lecteur était plus bruyant que je ne le pensais. Après avoir lu d'autres informations, j'ai découvert qu'une résistance de rappel de 1 kiloohm était la norme, alimentée par une bascule de Schmitt.

Après avoir installé une bascule de Schmitt hexagonale SN74HCT14N et reconfiguré le circuit pour qu'il se déclenche sur les fronts montants plutôt que sur les fronts descendants, j'ai fait un essai, mais cela n'a pas vraiment fait de différence notable. Je suppose que comme je cherchais une ou plusieurs impulsions à chaque fois, cela a probablement absorbé tout le bruit de toute façon. Nous allons donc nous en tenir à la méthode de la tentative 5 !

Ma solution finale de platine d'expérimentation ressemblait à ceci :

platine d'expérimentation

Notez que le câblage ci-dessus est légèrement différent de celui du croquis. J'ai réorganisé certaines des broches de l'Arduino pour rendre le schéma de circuit plus simple.

Tentative 7

J'étais un peu insatisfait de certaines disquettes que je n'avais pas pu lire. Parfois, les disquettes ne s'inséraient pas correctement dans le lecteur. Je suppose que le ressort de l'obturateur n'aidait pas.

J'ai commencé à chercher à détecter s'il y avait des erreurs dans les données MFM reçues de la disquette.

A partir des règles de fonctionnement du codage MFM, je me suis rendu compte que quelques règles simples pouvaient être appliquées comme suit :
  • Il ne peut y avoir deux bits "1" côte à côte.
  • Il ne peut y avoir plus de trois bits "0" côte à côte.
Tout d'abord, lorsque je décodais des données MFM, je regardais s'il y avait deux "1" à la suite. Si c'était le cas, je supposais que les données étaient devenues un peu floues avec le temps et j'ignorais le deuxième "1".

Avec cette règle, il y a littéralement trois situations de cinq bits où des erreurs peuvent se produire. C'est un nouveau domaine dans lequel je pourrais chercher à améliorer les données.

J'ai surtout été surpris par le fait qu'il n'y avait pas beaucoup d'erreurs MFM détectées. Je ne comprends pas très bien pourquoi certaines disquettes ne sont pas lues alors qu'aucune erreur n'a été détectée.

C'est une question à approfondir.

Décodage

Après avoir étudié le fonctionnement de la MFM, je n'étais pas tout à fait sûr de la manière dont elle s'alignait correctement.

Au début, je pensais que le lecteur émettait des "1" et des "0" pour les bits d'activation et de désactivation. Ce n'était pas le cas. Le variateur émet une impulsion pour chaque transition de phase, c'est-à-dire chaque fois que les données passent de 0 à 1 ou de 1 à 0.

Après avoir lu cela, je me suis demandé si je devais reconvertir les données en 1 et 0 en les envoyant à une bascule, ou lire les données, rechercher les secteurs, et si aucun n'était trouvé, inverser les données et réessayer !

Il s'avère que ce n'est pas le cas et que c'est beaucoup plus simple. Les impulsions sont en fait les données RAW MFM et peuvent être introduites directement dans les algorithmes de décodage. Maintenant que j'ai compris cela, j'ai commencé à écrire du code pour scruter un tampon du lecteur et rechercher le mot de synchronisation 0x4489. Étonnamment, je l'ai trouvé !

D'après les recherches que j'ai effectuées, j'ai réalisé que je devais en fait chercher 0xAAAAAA44894489. Une note a également suggéré qu'il y avait des bogues dans le code des premiers Amiga qui signifiaient que la séquence ci-dessus n'était pas trouvée. J'ai donc cherché 0x2AAAAAAA44894489 après avoir fait un "AND" (ET) entre les données et 0x7FFFFFFFFFFFFFFF.

Comme prévu, j'en ai trouvé jusqu'à onze sur chaque piste, correspondant au début réel des onze secteurs Amiga. J'ai ensuite commencé à lire les octets qui suivaient pour voir si je pouvais décoder les informations du secteur.

J'ai pris un bout de code d'une des références ci-dessus pour aider au décodage MFM. Pas la peine de réinventer la roue, hein ?

Après avoir lu l'en-tête et les données, j'ai essayé de les écrire sur la disquette comme un fichier ADF. Le format de fichier ADF standard est très simple. C'est littéralement juste les 512 octets de chaque secteur (des deux côtés de la disquette) écrits dans l'ordre. Après l'avoir écrit, j'ai essayé de l'ouvrir avec ADFOpus et j'ai obtenu des résultats mitigés, parfois il ouvrait le fichier, parfois il échouait. Il y avait manifestement des erreurs dans les données. J'ai commencé à regarder les champs de somme de contrôle dans l'en-tête, rejetant les secteurs avec des sommes de contrôle invalides et répétant la lecture jusqu'à ce que j'en ai onze valides.

Pour certaines disquettes, il s'agissait des onze lors de la première lecture, d'autres ont nécessité plusieurs tentatives et des valeurs de phase différentes également.

Finalement, j'ai réussi à écrire des fichiers ADF valides. Certaines disquettes prenaient des heures, alors que d'autres étaient écrites littéralement à la vitesse que l'Amiga les aurait lues. N'ayant plus d'Amiga en état de marche, je n'ai pas pu vérifier si ces disquettes se lisaient correctement, elles ont été stockées dans une boîte dans le grenier pendant des années et peuvent donc s'être dégradées.

Écriture de données - tentative 1

Après avoir réussi à lire des disquettes, je me suis dit que si l'on voulait conserver le support physique d'origine, il fallait pouvoir réécrire des disquettes. Je me suis dit que j'allais procéder à l'inverse, en commençant par le logiciel (c'est-à-dire convertir les fichiers de la disquette ADF en données MFM pour que l'interface puisse les écrire).

J'ai donc commencé par ajouter des classes pour lire une disquette ADF, et coder tous les secteurs en tant que piste unique. Sachant que je pouvais potentiellement tester les données que je créais en les renvoyant dans la partie décodage, j'ai commencé à travailler dessus. Pendant que je travaillais sur ce projet, j'ai décidé d'essayer de trouver ce qui n'allait pas avec mon Amiga. Après tout, je ne peux pas tester les disquettes que je crée si je n'ai rien de réel sur lequel les tester.

En démontant mon A500+, j'ai remarqué qu'il avait souffert d'un des problèmes les plus courants, la batterie de l'horloge avait coulé. J'ai donc dessoudé la batterie de la carte et j'ai commencé à nettoyer la carte. Pendant que j'y étais, j'ai sorti toute la machine et j'ai commencé à nettoyer 20 ans de poussière et de saleté. J'ai même démonté le lecteur de disquette pour le nettoyer.

En le nettoyant, j'ai décidé qu'il était temps d'effacer le jaunissement, j'ai donc suivi les informations sur Retr0brite et je l'ai essayé.

J'ai ensuite vérifié tous les joints de la carte mère principale et j'ai trouvé une connexion perdue près du connecteur d'alimentation, quelques retouches avec le fer à souder et tout était comme neuf. J'ai attendu d'être satisfait du processus Retr0brite avant de réassembler l'ordinateur.

Pendant ce temps, j'ai continué à travailler sur le code d'écriture des disquettes. Je voulais lire l'état de la ligne de protection en écriture, mais peu importe ce que j'y mettais, cela ne semblait pas changer la tension. J'ai donc démonté le lecteur et suivi les tracés des petits interrupteurs qui détectent l'état de la protection en écriture jusqu'à un petit circuit intégré. A ce stade, j'ai supposé que la sortie n'est probablement disponible que lorsque vous voulez réellement écrire des données.

Après de nombreuses expérimentations, j'ai découvert qu'il fallait tirer la broche /WRITE_GATE vers "LOW" (BAS) avant de mettre le lecteur en marche pour permettre l'écriture. A ce stade, vous pouvez obtenir le statut de protection en écriture. J'ai également remarqué que lorsque la broche /WRITE_GATE était sur "LOW" (BAS), le lecteur ne s'éteignait pas comme il le faisait jusqu'à ce que cette broche revienne à son état "HIGH" (HAUT) par défaut.

L'Amiga pouvait écrire une piste entière en une seule fois. Une piste en mémoire est de 11x512 octets (5638 octets), cependant, après codage MFM et mise au format AmigaDOS correct, la piste est de 14 848 octets. Eh bien, il n'y a aucune chance que cela puisse tenir dans les 2 ko de mémoire de l'Arduino, ni dans ses 1 ko d'EEPROM. J'avais besoin d'une méthode alternative.

J'ai décidé d'essayer d'envoyer les données octet par octet dans un fil d'exécution de haute priorité et d'attendre un octet de réponse de l'Arduino avant d'envoyer le suivant. J'ai changé le débit en bauds à 2 Mbauds pour réduire le décalage entre les caractères. Cela signifie qu'il fallait environ 5,5 microsecondes pour envoyer chaque caractère, et 5,5 microsecondes pour en recevoir un en retour. L'Arduino aurait besoin d'écrire 8 bits, à 500 kHz, et donc d'un nouvel octet tous les 16 microsecondes. Il devrait donc avoir le temps, en supposant que la boucle de code soit suffisamment serrée et que le système d'exploitation ne retarde pas trop l'envoi et la réception.

Ce fut un échec total. L'ensemble du cycle de lecture/écriture prenait beaucoup trop de temps, bien au-delà d'une rotation complète de la disquette. Le côté Arduino était probablement assez rapide, mais le système d'exploitation n'était pas assez réactif. La lecture des disquettes fonctionne parce que le système d'exploitation (Windows dans mon cas) mettait en mémoire tampon les données entrantes, mais pour l'écriture, Windows envoyait tout en une seule fois, mais comme le taux d'envoi est beaucoup plus rapide que ce dont l'Arduino a besoin, les données étaient perdues. C'est pourquoi j'ai opté pour ce processus d'accusé de réception bidirectionnel.

Écriture de données - tentative 2

Le contrôle de flux logiciel pour cette application n'était tout simplement pas assez rapide. J'ai décidé d'étudier le contrôle de flux matériel. J'ai remarqué que sur la carte FTDI, il y a des broches CTS et DTR. Elles signifient Clear To Send et Data Terminal Ready. J'ai remarqué que lorsque la carte de dérivation était connectée, la carte Arduino connectait le CTS à GND.

FTDI
La carte FTDI de dérivation avec la broche CTS pliée et un fil attaché

Je ne savais pas non plus dans quelle direction ces broches allaient réellement, mais après quelques expérimentations, j'ai découvert que la broche CTS pouvait être signalée par l'Arduino et utilisée par le PC pour contrôler le flux. Normalement, cela se fait en utilisant un tampon circulaire, mais dans mon cas, je ne pouvais pas le permettre, donc je l'ai simplement mis à "1" quand je ne veux pas de données, et "0" quand j'en veux.

platine d'expérimentation
La carte FTDI de dérivation connectée à l'Arduino sur la platine d'expérimentation

Cela signifie que je pouvais demander au système d'exploitation d'envoyer les octets en une seule fois, en espérant que tout soit géré au niveau du noyau et qu'il ne soit pas interrompu.

J'avais une boucle interne qui sortait chaque bit des 8 bits, mais j'ai décidé qu'il était probablement plus judicieux, du point de vue de la synchronisation, de la décomposer en huit jeux de commandes à la place.

Cela n'a pas fonctionné. Si je laissais le code s'exécuter sans exécuter la partie écriture sur la disquette, tous les octets étaient reçus correctement, mais pas si j'exécutais le code, et les octets reçus étaient perdus.

Je soupçonnais que le changement d'état de la ligne CTX n'arrêtait pas instantanément le flux de données et que l'ordinateur pouvait encore envoyer un caractère ou deux. Il est possible qu'au moment où j'ai signalé la ligne CTX, l'ordinateur était déjà en train d'envoyer le caractère suivant.

Écriture de données - tentative 3

Je ne voulais pas avoir d'interruption série car je ne voulais pas que les temps d'écriture soient faussés. J'ai réalisé qu'entre l'écriture de chaque bit sur le lecteur de disquette, il y aurait un certain nombre de cycles du processeur dans la boucle "while" suivante. J'ai décidé de vérifier entre chaque écriture de bit si un autre octet avait été reçu depuis que CTX était à l'état "HIGH" (HAUT) et de le stocker.

Ma théorie était que lorsque vous avez CTX à l'état "HIGH" (HAUT), l'ordinateur était probablement déjà en train de transmettre le prochain octet et comme vous ne pouvez pas l'arrêter au milieu de la transmission, il serait à la moitié après celui-ci. Cela signifie que je n'ai besoin de vérifier qu'un seul octet supplémentaire pendant la boucle et de l'utiliser s'il est trouvé au lieu de regarder à nouveau le port série.

Cela semble donc fonctionner et l'Arduino a terminé l'écriture sans perdre aucune donnée de l'ordinateur. La seule question qui se pose maintenant est de savoir s'il a réellement écrit des données, et si oui, si elles sont valides !

A ce stade, je n'avais codé qu'une seule piste, j'ai donc décidé de lancer l'algorithme complet pour coder les 80 pistes. Quelque chose d'étrange se produisait. La tête du lecteur ne bougeait plus du tout. Elle bougeait toujours en lecture, mais pas en écriture.

J'ai découvert que pour déplacer la tête du lecteur d'avant en arrière, il fallait d'abord lever la broche /WRITE GATE, je soupçonnais que cela était également nécessaire pour changer la surface. Une fois que j'ai ajouté le code pour faire cela, la tête du lecteur s'est déplacée comme prévu. Cela était logique et permettait d'éviter l'écriture accidentelle de pistes lors du déplacement de la tête.

A ce stade, j'ai écrit une image-disque que j'avais créée précédemment, puis j'ai essayé de la relire. Rien ne pouvait être détecté. Soit les données que j'avais écrites n'étaient pas valides, soit la façon dont je les écrivais était mauvaise.

J'ai décidé d'introduire les données de secteur MFM codées, que je créais dans mon algorithme de décodage de secteur utilisé par le lecteur, pour valider que ce que je générais était correct et valide, et c'était le cas. Il y avait manifestement un problème avec la façon dont j'écrivais les données sur la disquette.

Écriture des données - tentative 4

Comme aucune donnée n'était lue correctement, j'ai décidé d'essayer quelques approches différentes. Je ne savais pas si la broche /WRITE DATA devait être impulsée (et si oui, pendant combien de temps), basculée ou simplement réglée sur la valeur brute des données. Mon implémentation actuelle envoie des impulsions à la broche. Je n'ai pas pu trouver d'informations sur Internet sur la façon dont la broche d'écriture est physiquement censée être manipulée lors de l'écriture.

La tête de lecture nous envoie une impulsion à chaque fois qu'il y a une inversion de flux. J'ai décidé de modifier l'implémentation de sorte que /WRITE DATA soit simplement réglée sur la valeur du bit. Cela n'a pas fonctionné non plus. J'ai donc modifié le code pour basculer l'état actuel de la broche. Toujours aucun résultat.

Il est clair que l'une de ces approches devait être la bonne. J'ai donc décidé de ressortir mon fidèle oscilloscope pour voir ce qu'il se passait. J'ai décidé d'écrire le motif MFM 0xAA sur chaque octet d'une piste en continu. 0xAA en binaire est B10101010, donc cela me donnerait une onde carrée parfaite que je pourrais surveiller pour la fréquence requise.

Si je ne voyais pas d'onde carrée parfaite à la fréquence souhaitée, je savais qu'il devait y avoir un problème de synchronisation.

J'ai branché l'oscilloscope, mais j'ai été surpris de voir que la synchronisation était parfaite. Cependant, comme il s'agit d'un vieil oscilloscope, je ne pouvais pas voir plus que quelques impulsions. L'oscilloscope avait ce merveilleux mode "mag" x10. Lorsqu'on appuie dessus, cela augmente la base de temps de 10, mais plus important encore, cela permet de faire défiler toutes les données comme sur un oscilloscope numérique moderne.

Quelque chose n'était pas correct ici. Il semblait que tous les 12 bits environ, je me retrouvais avec une période de "HIGH" (HAUT).

scopes

Soit les données que j'envoyais n'étaient pas valides, soit il y avait quelque chose qui provoquait une pause dans le processus d'écriture tous les 12 bits environ. Douze étant un nombre étrange considérant qu'il n'y a que 8 bits dans un octet.

Après avoir réfléchi à cela, je me suis demandé si je n'avais pas un problème de contrôle de flux. La façon dont j'avais conçu la boucle était de récupérer tout octet supplémentaire reçu après que nous ayons attendu un octet. Mais ce n'était pas assez malin pour empêcher l'attente d'un octet sur deux. J'avais deux choix, déplacer quelque chose dans une interruption, ou corriger la boucle.

J'ai décidé d'essayer de corriger le fonctionnement de la boucle en premier. Le problème était dû à un délai causé par l'attente de l'octet suivant de l'ordinateur. Si nous abaissions CTX et attendions un octet, le temps que nous rehaussions CTX, un autre octet était déjà en route.

J'ai modifié la boucle de sorte que lorsque le deuxième octet reçu avait été utilisé, l'Arduino tirait momentanément CTS vers le bas puis vers le haut à nouveau pour permettre l'envoi d'un autre caractère. Cela signifie que lors de la prochaine boucle, nous aurons déjà reçu le prochain octet et qu'aucune attente ne sera nécessaire.

Le test a produit une onde carrée parfaite :

scope

Cela signifie que toute la synchronisation pour écrire une piste était parfaite, il ne restait plus qu'à écrire les données réelles. J'ai décidé de laisser fonctionner le système pour quelques pistes et faces, puis de le relire pour voir s'il avait écrit correctement. Je réglais la broche /WRITE DATA sur la valeur binaire correspondante des données reçues.

Lorsque j'ai relu les données, il semblait que rien n'avait été codé, mais je suis ensuite passé à l'autre face de la disquette. Bien sûr, il y avait mon motif. Je ne savais pas pourquoi il n'avait écrit que sur une face de la disquette.

Après avoir réfléchi, je me suis demandé si la broche /WRITE GATE ne fonctionnait pas comme je le pensais. Il m'est apparu qu'en tirant la broche vers le bas, cela pouvait activer la tête d'effacement du lecteur. Si c'était le cas, je ne devais le faire que lorsque j'écrivais, sinon je risquais d'avoir du bruit sur la disquette lorsqu'elle tournait et effaçait.

J'ai modifié l'ensemble du code pour que /WRITE GATE ne soit utilisée que lors du premier démarrage du lecteur, puis uniquement pendant la boucle d'écriture. Cela a fonctionné ! J'écrivais maintenant des données sur les deux faces de la disquette !

J'ai donc réessayé avec une image-disque ADF réelle et j'ai attendu que l'opération se termine. J'ai ensuite utilisé la partie lecteur pour voir si je pouvais la relire. Cela a fonctionné ! Mais pour une raison inconnue, il a fallu un certain temps pour relire cette disquette. Je n'ai pas eu d'erreurs MFM mais il a eu du mal à trouver tous les secteurs.

Il y avait deux possibilités pour moi maintenant. Premièrement, les données avaient-elles été écrites assez rapidement, et deuxièmement, la disquette fonctionnerait-elle dans un vrai Amiga ?

Trop impatient à l'idée que j'avais peut-être écrit une disquette, j'ai démarré l'A500+ qui était à présent fonctionnel et j'ai inséré la disquette. Quelques instants plus tard, la disquette a démarré, puis a affiché le fameux message d'erreur de somme de contrôle. J'écrivais donc quelque chose de valide, mais ce n'était pas cohérent.

J'ai décidé qu'à moins de pouvoir relire les données à un rythme beaucoup plus précis, l'écriture d'une disquette était inutile.

Lecture des données (à nouveau)

Je voulais améliorer la qualité de la lecture car je n'étais pas satisfait de l'implémentation actuelle. L'implémentation actuelle ne permettait pas assez de flexibilité pour que les impulsions arrivent à des moments légèrement différents. J'avais besoin d'une nouvelle approche.

Tout d'abord, j'ai décidé que j'allais synchroniser la lecture avec l'impulsion /INDEX. Ce n'est pas requis par l'Amiga, mais cela peut s'avérer utile plus tard pour tester l'écriture et la lecture.

Plusieurs personnes dans les commentaires de la première moitié de ce projet ont suggéré que je devrais enregistrer la synchronisation entre les impulsions plutôt que la méthode que j'avais implémentée. Le seul problème était de transmettre ces données au PC assez rapidement. Si je devais envoyer un octet pour chaque bit, je pourrais facilement dépasser le maximum de 2 Mbauds.

J'ai décidé que la meilleure chose à faire serait d'essayer de donner un peu de sens à ces données. J'ai donc décidé de laisser le compteur que j'utilisais à l'origine tourner librement, jusqu'à 255. J'ai ensuite placé le code dans une boucle en attendant une impulsion et à ce moment-là, j'ai vu combien de temps s'était écoulé.

Dans une situation idéale, la valeur minimale serait de 32 (correspondant à deux microsecondes). Avec la MFM, il ne peut y avoir qu'un maximum de trois "0" à la suite, donc le maximum que cette valeur devrait atteindre est 128. Cela signifie qu'il y avait un maximum de quatre combinaisons possibles.

J'ai échantillonné plusieurs disquettes pour voir où se trouvait la majorité de ces fréquences, et les résultats sont présentés ci-dessous :

graph

En regardant ceci, je trouve la majorité des points autour d'un compteur de 52, 89 et 120. Cependant, ces valeurs étaient quelque peu spécifiques à ma disquette et ne constituaient donc pas une bonne ligne directrice. Après quelques expérimentations, j'ai utilisé la formule suivante : valeur=(COMPTEUR-16)/32. Lorsqu'elle est coupée entre 0 et 3, cela me donne la sortie dont j'ai besoin. A chaque fois que j'en utilisais 4, je pouvais écrire un octet.

Il m'est apparu qu'étant donné qu'il est impossible d'avoir deux "1" ensemble dans un flux de bits codés MFM, je pouvais supposer sans risque que la première valeur n'était pas valide et pouvait être traitée comme une autre séquence "01". La partie suivante consistait à décompresser ces données une fois reçues par le PC et à les retransformer en données MFM. C'était simple, puisque "00" ne pouvait pas se produire, un "01" signifiait écrire "01", un "10" signifiait écrire "001" et un "11" signifiait écrire "0001". J'ai fait un essai et à ma grande surprise, mes résultats étaient 100% satisfaisants. J'ai essayé avec quelques autres disquettes aussi : résultat de 100% ! J'avais maintenant un lecteur de disquette très fiable.

Cette nouvelle approche étant beaucoup plus tolérante sur les données de la disquette, je n'avais plus besoin d'analyse de phase ou d'autant de tentatives. La plupart de mes disquettes sont maintenant parfaitement lues. Certaines ont nécessité quelques tentatives, mais elles ont fini par fonctionner. La dernière partie consistait à analyser statistiquement les données et à voir si elles pouvaient être réparées, mais dans 99% des cas, les données erronées étaient totalement méconnaissables et n'étaient donc d'aucune utilité.

Écriture des données - tentative 5

Maintenant que je pouvais vérifier ce que j'avais écrit avec une grande précision, cela signifiait que tester le code d'écriture serait beaucoup plus facile.

J'ai commencé à analyser le code pour voir ce qui n'allait pas. J'ai écrit une séquence 0x55 sur une piste entière, puis je l'ai relue. De temps en temps, un bit était décalé dans les données qui revenaient, ce qui signifiait qu'il y avait une sorte de problème de synchronisation dans l'écriture.

Il s'est avéré que cela était en partie dû à la façon dont je manipulais le port série, et en partie dû à l'utilisation du chronomètre. J'attendais que le chronomètre atteigne la valeur 32, j'écrivais le bit, puis je le réinitialisais. Je l'ai changé pour ne pas avoir à modifier la valeur du compteur du chronomètre.

J'écrivais le premier bit quand le compteur atteignait 16, puis le suivant quand il atteignait 48 (16+32), et le suivant quand il atteignait 80 (16+32+32) et ainsi de suite. Le Timer2 n'étant que de 8 bits, il revient à zéro après le 8e bit, exactement quand nous en avons besoin. Cela signifie que tant que nous écrivons le bit à la valeur requise du chronomètre, nous sommes exactement à 500 ko/s.

J'ai aussi regardé comment je lisais les données sur le port série. Elles étaient lues entre chaque bit, mais elles devaient être aussi courtes que possible. Après un peu d'expérimentation, j'ai obtenu le bloc le plus court possible.

Après avoir modifié le code Windows pour gérer la vérification, j'étais prêt à réessayer. Cette fois, je savais que si la vérification de la disquette se faisait correctement, elle devait fonctionner correctement sur l'Amiga.

J'ai donc essayé d'écrire une autre disquette. Avec la vérification, cela a pris plus de temps. Avec le nouvel algorithme, environ 95% des pistes ont passé la vérification du premier coup, et seules les 5% restantes ont dû être réécrites une nouvelle fois. J'étais satisfait de ce résultat et j'ai inséré la disquette dans l'Amiga. Et elle a parfaitement fonctionné !

Écriture de données - tentative 6

Après quelques commentaires de la part de certaines personnes qui ont utilisé ce système, il est apparu clairement que même avec la vérification du lecteur, celui-ci ne produisait pas toujours des disquettes parfaitement lisibles. Le logiciel pouvait les relire parfaitement, mais les ordinateurs Amiga signalaient quelques erreurs de somme de contrôle ici et là.

J'ai jeté un autre coup d'oeil au code, je me suis demandé si ce n'était pas un problème de synchronisation et j'ai cherché à voir s'il ne pouvait pas être piloté par des interruptions, mais malheureusement avec la petite quantité de temps entre chaque bit, il n'y a tout simplement pas assez de temps avec les interruptions pour y parvenir tout en préservant les registres que vous modifiez, etc.

Je me suis ensuite penché sur le code de l'écriture. Il y a une petite chance qu'après l'écriture d'un octet complet, le code ait pu faire une boucle pour commencer à écrire l'octet suivant avant que le chronomètre n'ait débordé à 0, permettant au premier bit d'être écrit plus tôt.

J'ai ajouté une petite boucle pour m'assurer que cela ne pouvait pas se produire, ce qui, je l'espère, résoudra le problème pour tous ceux qui ont ce problème.

Diagnostics

J'ai reçu de nombreux commentaires de personnes qui ont réalisé ce projet avec succès, qu'il fonctionne ou non. J'ai décidé d'intégrer un module de diagnostic dans le code pour aider ceux qui ne parviennent pas à faire fonctionner le leur.

L'option de diagnostic consiste en quelques commandes supplémentaires à traiter par l'Arduino ainsi que toute une série d'événements qui sont exécutés pour s'assurer que tout est câblé correctement.

Écriture des données - tentative 7

Après avoir reçu de nombreux rapports d'erreurs concernant la somme de contrôle pour les disquettes écrites, j'ai commencé à investiguer. J'ai d'abord pensé que j'allais devoir examiner les données MFM de la disquette, mais le problème était en fait beaucoup plus simple...

En regardant X-Copy Pro pour voir les erreurs de somme de contrôle, il a signalé les codes 4 et 6 signifiant des erreurs de somme de contrôle dans les en-têtes de secteur et les zones de données. Si cela n'avait été que la zone de données, j'aurais supposé qu'il s'agissait uniquement de l'écriture des derniers bits de la piste, mais ce n'était pas le cas.

J'ai commencé à regarder le code d'écriture et le bloc que j'avais autour de chaque piste, me demandant si je n'écrasais pas le début d'une piste de temps en temps, alors j'ai réduit massivement le bloc post-piste de 256 octets à 8. À ma grande surprise, ma vérification a alors donné lieu à une tonne d'erreurs.

Cela m'a fait me demander si le problème réel n'était pas que je n'écrivais pas assez de données. J'ai commencé à ajouter une commande d'effacement de piste à l'Arduino qui écrirait le motif 0xAA sur toute la piste et écrirait ensuite ma piste. A ma grande surprise, X-Copy l'a approuvé à 100%. J'espère donc avoir résolu ce problème.

Alors, que s'est-il passé ?

J'ai été en retrait de ce projet pendant un certain temps. Mais j'ai récemment repris le flambeau pour résoudre quelques problèmes et fusionner certaines des excellentes choses que d'autres ont faites dans ce projet.

Par exemple, il existe une version qui lit les disquettes Commodore 1581, ainsi que les disquettes Atari HD.

Lecture des données (à nouveau)

J'avais réfléchi à l'idée du pourquoi les temps que j'avais utilisés pour la lecture étaient ce qu'ils étaient. Bien qu'ils fonctionnent, les chiffres ne suivent pas vraiment un motif exact, et ne suivent pas les plages qu'ils devraient.

Je savais qu'il y avait trois motifs de bits MFM qui devaient se produire, 01, 001 et 0001. J'ai également vu 00001 qui n'est techniquement pas autorisé. En ignorant le dernier, ces motifs devraient prendre 64, 96 et 128 microsecondes. En théorie, les points centraux devraient être (64-96) 80 et (96-128) 112. C'est ce que j'avais. Mais je me suis soudainement rendu compte que je travaillais avec des synchronisations à la vitesse d'horloge de l'Arduino. Cela signifie que chaque instruction compte pour cela.

J'avais une disquette, avec une piste qui se lisait toujours avec des erreurs, mais sur un Amiga, elle se lisait bien. Cela devait avoir quelque chose à voir avec ces synchronisations, car il semblait que l'Amiga devait être très tolérant à la dérive des bits.

J'ai commencé à regarder le code désassemblé et les instructions impliquées. J'ai été surpris de constater que la lecture de la valeur du compteur et sa remise à zéro prenaient ensemble quatre tics d'horloge. J'ai donc fait quelques expériences en appliquant ce décalage aux synchronisations utilisées et à ma grande surprise, la disquette pouvait maintenant être lue !

Afin de mettre de l'ordre dans tout cela pour une utilisation future, tous ces calculs sont maintenant effectués en haut du code.

Écriture des données (à nouveau) - précompensation - échec 1

Un des commentaires que j'ai lus pendant que je travaillais sur ce projet, était que les gens étaient surpris que l'écriture fonctionne aussi bien qu'elle le faisait sans ce qui est connu comme la précompensation (precomp) d'écriture. Après quelques lectures de fond, j'ai eu du mal à trouver si cela était même utilisé sur l'Amiga, bien que l'on puisse voir que cela était géré par le matériel.

La théorie de la précompensation d'écriture est un peu comme ceci : en écrivant sur les pistes extérieures, chaque fois que vous envoyez un "1", vous provoquez une transition de flux sur la surface de la disquette. Cela revient à créer de petits aimants à la surface de la disquette. Sur les pistes extérieures, cela ne pose aucun problème, mais à mesure que vous vous rapprochez des pistes intérieures, ces aimants se rapprochent les uns des autres. Sans tenir compte des détails, vous êtes censé ajuster légèrement les temps entre les bits pour compenser tout cela, ainsi que le temps nécessaire à la création du champ magnétique.

Il est surprenant de constater qu'il existe un certain nombre d'informations contradictoires à ce sujet, mais la base est d'essayer d'ajuster les synchronisations de manière à maintenir les bits "1" aussi éloignés que possible les uns des autres sans les faire tomber dans une autre position.

La partie délicate de tout cela est le montant de la précompensation et le moment où il faut l'utiliser. Certains documents parlent de l'appliquer à partir de la piste 40. D'autres disent après la piste 79. Certains disent les deux, mais avec des quantités différentes.

L'Amiga gère une précompensation de 0, 140, 280 et 560 nanosecondes. Oui, des nanosecondes. C'est une quantité incroyablement petite. L'horloge de base de 16 MHz de l'Arduino est de 62,5 nanosecondes, donc seulement deux instructions de base (comme add) ajouteraient 125 nanosecondes. Je me suis demandé si nous pouvions être aussi précis avec la résolution disponible.

Je me suis rendu compte qu'il n'y avait aucun moyen de calculer cela sur l'Arduino, et qu'il faudrait donc le précalculer sur le PC avant de l'envoyer. J'ai décidé d'utiliser un code qui lirait le flux binaire brut de la MFM et le convertirait comme suit :

Motif MFM Codé en binaire en tant que
01 00
001 01
0001 10
00001 11

Donc, avec ce motif, je pourrais envoyer quatre de ces séquences pour chaque octet. Mais qu'en est-il de la précompensation ? Je devais dire pour chaque séquence si la séquence devait être "EARLY" (c'est-à-dire -125 nanosecondes), "LATE" (+125 nanosecondes) ou normale. Et voilà deux bits de plus.

Ainsi, chaque demi-octet (alias "nybble") contenait maintenant des informations concernant chaque séquence. Mais pouvais-je envoyer cela assez rapidement ? Le débit en bauds est de 2 Mbauds et de 10 bits environ (en tenant compte des bits de départ et d'arrêt, etc.), ce qui signifie que chaque octet arriverait à environ cinq secondes d'intervalle. D'après ce que nous avons appris, cela correspond à environ 88-90 tics d'horloge de l'Arduino. Dans un bon jour, disons 80 instructions. Ça semble beaucoup.

J'avais besoin de savoir que cela allait fonctionner et être précis. Mon fidèle oscilloscope n'aurait pas été capable de suivre le rythme, et même s'il l'avait pu, il n'avait pas d'options de stockage pour la petite quantité de données.

Je me suis décidé pour un analyseur logique, mais ils sont si chers. Jusqu'à ce que je trouve des clones bon marché de 24 MHz pour environ 10 £. Et ça a très bien marché. J'étais capable de capturer les données que j'écrivais et de vérifier avec précision les synchronisations. Ok, avec le théorème d'échantillonnage de Nyquist, j'avais vraiment besoin de quelque chose comme 32 MHz, mais c'était probablement suffisant.

Je l'ai branché, j'ai écrit du code pour le lire, j'ai mis le chronomètre à zéro et j'ai attendu dans une boucle qu'il atteigne la valeur requise basée sur 64+(32xmotif)+/-precomp. A première vue, quelque chose de très bizarre se passait. La séquence était là, mais les données se déplaçaient dans tous les sens. Les impulsions semblaient se décaler d'environ +/-180 nanosecondes partout, de façon apparemment aléatoire. C'était étrange.

Puis je me suis souvenu du problème que j'avais eu avec la lecture, et j'ai désassemblé le code. La boucle ressemblait à quelque chose comme cela :

while (TCNT2 < value) {};

Ça semble assez anodin. Mais ensuite, il faudrait extraire la valeur de TCNT (deux tics d'horloge), puis la comparer à une autre valeur. Un autre tic d'horloge. Puis revenir en arrière si ce n'était pas la valeur. Un autre tic d'horloge. Cette boucle nécessite quatre tics. C'est le quart de notre résolution. Comment étais-je supposé écrire quelque chose au quart de ça avec cette méthode ? La réponse est que vous ne pouvez pas.

Écriture de données (à nouveau) - précompensation - échec 2

J'avais donc besoin de trouver un moyen plus rapide de surveiller le compteur matériel, qui faisait tic-tac à la vitesse de certaines des instructions que j'utilisais. Je savais que TCNT2 (Counter2/Timer2) pouvait faire beaucoup plus que simplement compter.

J'ai commencé à regarder à nouveau la fiche technique de la puce ATMega328. Ce n'est pas pour les âmes sensibles, mais elle explique à peu près tous les aspects du microcontrôleur, des broches de contrôle aux dimensions physiques des puces.

Si vous allez à la section des compteurs, il y a beaucoup d'autres registres qui leur sont associés. Jusqu'à présent, je n'avais pas vraiment essayé de les utiliser, enfin, pas depuis mes toutes premières expérimentations quand j'ai commencé ce projet.

Il y a des registres qui vous permettent de contrôler l'intervalle dans lequel il compte (OCR2A et OCR2B), ainsi que des bits dans les registres qui contrôlent le fonctionnement du chronomètre. Par exemple TCCR2A est utilisé pour contrôler la sortie PWM, ou comme je l'ai configuré, aucune sortie.

Puis il y a son double, TCCR2B, que j'ai utilisé jusqu'à présent pour régler la vitesse du compteur, dans ce cas sur "no pre-scaling" (sans pré-dimensionnement), ce qui signifie qu'il fonctionne aussi vite que l'oscillateur (16 MHz).

Vous avez également TIMSK2 qui est impliqué dans l'activation des interruptions, et enfin TIFR2. C'est un registre intéressant. En regardant de plus près, il y a trois bits que vous pouvez lire. OCF2B, OCF2A et TOV2. Ces "bits" sont définis en fonction des règles établies dans TCCR2A. Par exemple, ces bits peuvent être activés lorsque TCNT2 atteint la valeur de OCR2A, OCR2B ou déborde vers zéro.

Je savais que la seule lecture de TCNT2 utilisait deux cycles d'instruction, mais qu'en est-il de la vérification de ces bits ? Après avoir désassemblé le code, j'ai découvert qu'ils pouvaient être testés en un seul cycle d'instruction. Cela signifiait que la boucle que j'utilisais auparavant pouvait être remplacée par quelque chose ressemblant plus à ceci :

while (!(TIFR2 & bit(OCF2B)) {};

Cette boucle se termine si je configure correctement les registres ci-dessus lorsque TCNT2 atteint OCR2B. Fantastique ! Une note intéressante : vous devez écrire un bit "1" dans ces registres pour les remettre à zéro. Ne me demandez pas, c'est ce que dit la documentation, et c'est correct.

J'ai donc réimplémenté la solution en tenant compte de cela. C'était beaucoup mieux, mais il y avait encore beaucoup de fluctuations. Une partie du problème était que pour maintenir l'écriture sur la disquette, je devais également lire les données du port série.

Le code que j'utilisais actuellement pour lire le port série et conserver des données en mémoire tampon ressemblait à ceci :

if (UCSR0A & bit(RXC0)) {
	SERIAL_BUFFER[serialWritePos++] = UDR0;
	serialBytesInUse++;
}

Le bout de code ci-dessus, encore une fois, semble simple, mais se compile en environ 14 cycles d'instruction ! Alors, quand dois-je exécuter ce code ? Une fois par "bit" ? Rappelez-vous, nous n'écrivons pas de "0", nous n'écrivons que des "1". Donc l'intervalle minimum serait (théoriquement) de 64 microsecondes, le maximum étant de 128 microsecondes. Les données arrivent à raison d'un octet tous les 80 microsecondes. Mais nous devons contrôler cela (c'est à cela que servent les contrôles CTS).

Le microcontrôleur met en mémoire tampon jusqu'à deux caractères pour vous avant que les données ne soient perdues. Il semble donc que tant que je vérifie une fois par cycle, tout devrait bien se passer. Ce code a donc été placé avant la boucle "while".

Quelque chose de bizarre s'est produit. Le PC a fini d'envoyer toutes les données et a attendu la réponse. Mais l'Arduino n'en a jamais envoyé. Le port série a aussi quelques registres intéressants. L'un d'eux vous dira si vous n'avez pas lu les données assez rapidement. Si vous faites :

if (UCSR0A & (bit(FE0)|bit(DOR0))) break;

...alors votre boucle se terminera si le port série a un dépassement de données, ou si une erreur de trame s'est produite. Donc, il s'avère que j'avais des erreurs de dépassement. Je ne lisais pas les données assez rapidement, donc le PC avait envoyé toutes les données, mais l'Arduino avait perdu/écrit certains des octets avant que j'aie eu la chance de les lire.

Je ne pouvais pas mettre le code ci-dessus dans la boucle "while" car cela pouvait potentiellement ajouter une fluctuation supplémentaire, la boucle pouvant potentiellement prendre plus de 14 cycles d'horloge avant de tester à nouveau. J'ai donc décidé d'essayer quelque chose comme ceci :

while (TCNT2

J'activais et désactivais la ligne CTS uniquement lorsqu'il y avait ou non de l'espace dans le tampon, et la théorie avec le code ci-dessus est de permettre jusqu'à 16 cycles d'horloge avant l'impulsion pour vérifier les données série, puis de s'arrêter et d'attendre dans une boucle plus serrée et de définir la sortie. L'impulsion de sortie, selon certains lecteurs, doit être quelque chose comme 0,15 ~ 1,1 microseconde. La méthode d'écriture précédente que j'utilisais était de 4 microsecondes et tout allait bien, de plus, c'est le front descendant (transition de 1 à 0) sur l'impulsion qui déclenche l'écriture, pas l'impulsion qui est basse en soi !

Cela semblait être un bon plan. Mais j'avais toujours des problèmes, cette fois, à savoir que mon code se rendait compte que je ne recevais pas assez de données du port série assez rapidement pour garder mon tampon de sortie plein. Il y a 16 cycles d'horloge dans la deuxième boucle qui ne lit pas les données du port série. Si je réduis ce nombre, le problème disparaît, mais la fluctuation revient.

À ce stade, j'avais presque abandonné. J'étais si proche, mais ce n'était pas assez bien. Je ne pouvais pas avoir de fluctuation, car s'il y en avait, essayer d'utiliser la précompensation serait de toute façon inutile.

Écriture des données (à nouveau) - précomposition - succès

J'avais besoin d'une nouvelle approche. Il devenait évident que le fait d'écrire les données en bits n'allait pas produire le niveau de précision dont j'avais besoin, enfin, pas avec ce microcontrôleur. Un plus rapide peut-être.

J'ai alors repensé à mes premières expériences et aux chronomètres. Ils pouvaient produire des formes d'onde, contrôlées au niveau matériel, généralement pour la PWM. Mais il s'agissait d'un motif répétitif. Je me suis demandé s'il n'y avait pas un moyen d'utiliser cette information, un moyen de faire varier ces formes d'onde pour produire ce dont j'avais besoin, en lui faisant faire les commutations précises et en le programmant simplement pour qu'il les fasse.

Ce dont j'avais vraiment besoin était une impulsion unique. C'est-à-dire : le mettre en marche, après un temps spécifique, il passerait au niveau bas, resterait là, et repasserait au niveau haut. Mais le microcontrôleur Atmel n'est pas conçu pour faire cela, ou du moins n'est pas supposé pouvoir le faire.

J'ai donc commencé à faire des recherches et je suis tombé sur une astuce qui utilisait le chronomètre du microcontrôleur AVR pour créer une impulsion unique. Intéressant. J'ai donc commencé à l'adapter à mes besoins. Il s'agit essentiellement de mettre le chronomètre à l'envers de sorte qu'il se bloque après la fin de l'impulsion. Je doute que cela ait jamais été prévu, mais la logique des chronomètres permet à cela de fonctionner !

J'ai donc commencé à modifier le code pour créer les séquences 01, 001, 0001, etc. en utilisant ce seul coup. Je l'ai démarré au premier "0", et l'ai laissé faire l'impulsion "1" aussi. Comme le chronomètre fonctionne également à 16 MHz, tout ce que j'aurais à faire serait de décaler le point de départ de l'impulsion pour obtenir une précompensation.

Après une analyse plus poussée des registres matériels de Timer2, j'ai fini par les configurer comme suit :

TCCR2A = bit(COM2B1) | bit(WGM20) | bit(WGM21)| bit(WGM22);  
TCCR2B = bit(WGM22) | bit(CS20);
  • TCCR2A : COM2B0 et COM2B1 combinés selon la fiche technique signifient CLEAR OC2B quand TCNT2=OCR2B, et SET OC2B quand TCNT2 atteint le bas (étant TCNT2=0). WGM20, WGM21, WGM22 indiquent que nous voulons utiliser le mode Fast PWM.
  • TCCR2B : WGM22 active la génération de la forme d'onde sur la broche de sortie associée, qui pour nous, heureusement, est la même broche avec laquelle nous avons écrit, et CS20 est la valeur de précale=1 que nous avons utilisé auparavant.
La façon dont le chronomètre fonctionne, est que lorsqu'il atteint BOTTOM (c'est-à-dire 0), il est supposé remettre TCNT2 à la valeur de OCR2A. Mais si nous le réglons également sur zéro, le chronomètre reste à 0 à moins que nous le changions manuellement.

Tout ce que j'avais à faire maintenant était de déterminer la valeur de OCR2B, qui est l'endroit où nous voulons que l'impulsion se déclenche, et la valeur initiale de TCNT2 pour donner la bonne quantité de délai pour cette impulsion.

Maintenant, comme le matériel générait l'impulsion, cela m'a donné plus de temps pour calculer les valeurs et surveiller le port série, laissant jusqu'à la dernière minute pour chronométrer précisément la réinitialisation de la séquence suivante.

Je me suis retrouvé avec un code comme celui-ci :

while (!(TIFR2 & bit(OCF2B))) {
	if (UCSR0A & bit(RXC0)) {
		SERIAL_BUFFER[serialWritePos++] = UDR0;
		serialBytesInUse++;	
	}
}; 

while (!(TIFR2 & bit(TOV2)));

TCNT2 = counter;
OCR2B = pulseStart;

Ainsi, la première boucle "while" s'exécute jusqu'à ce que l'impulsion commence, mais nous n'avons pas besoin de générer l'impulsion, donc nous ne nous soucions pas de sa précision, et la seconde attend précisément la condition de débordement (TCNT atteignant BOTTOM).

Après cela, nous réglons immédiatement TCNT2 (compteur) sur le délai pour la nouvelle impulsion, et nous écrivons où l'impulsion doit commencer.

Mais il y a un problème ici. La deuxième boucle "while" a également causé un petit délai, plus l'écriture de TCNT2 prend du temps. Mais cette fois, tout est fixé, et nous pouvons donc le compenser. J'ai mesuré six cycles d'horloge qui ont donné les synchronisations correctes.

C'est un succès ! J'ai confirmé dans l'analyseur logique que la sortie était bien celle dont j'avais besoin, qu'elle était chronométrée très précisément, et que le début de l'impulsion était décalé d'avant en arrière de deux tics comme je le voulais.

Si vous voulez voir exactement comment cela est fait, le code est disponible sur GitHub, comme le reste du projet. J'étais très satisfait. Au début du projet, je n'étais pas sûr que ce soit possible. Je pense qu'on m'aurait dit que ce n'était pas possible, mais en utilisant les composants intégrés au matériel plutôt que de faire le travail moi-même, cela a été possible.

WinUAE

Je vais résumer cette partie. Le processus réel a été beaucoup plus compliqué : déboguer les données au niveau des bits, voir ce que j'avais fait de travers et comprendre comment WinUAE implémentait l'accès à la disquette. J'avais dans un coin de ma tête l'idée de gérer le lecteur de manière native dans Windows. Cela impliquerait des pilotes de périphériques pour l'accès aux disquettes, puis des pilotes de système de fichiers pour gérer le système de fichiers Amiga. Ce serait fantastique, mais cela demanderait beaucoup de travail.

J'ai ensuite été distrait par une idée qui m'était venue à l'esprit à plusieurs reprises, et après avoir reçu un courriel à ce sujet, j'ai décidé d'enquêter davantage. Serait-il possible de gérer ce lecteur dans WinUAE, ce qui signifierait que vous pourriez utiliser des disquettes Amiga natives dans l'émulateur ?

Je n'en avais absolument aucune idée, mais je ne voyais en quoi ce ne serait pas le cas. J'ai supposé que, quelque part dans tout ce code, il devait simuler un lecteur de disquette... en quelque sorte... J'ai supposé que mon approche devrait changer quelque chose, mais comme vous l'avez vu, je n'ai pas peur de changer le code !

Première étape. Pourrais-je au moins compiler le code source de WinUAE ? Toni Wilen, qui le maintient si excellemment, inclut des instructions sur de telles choses, et quelques minutes après avoir téléchargé tous les fichiers, il s'est compilé et a fonctionné. Il ne me restait plus qu'à comprendre le code, et sans surprise, il y en a beaucoup, et dans tous les genres, probablement en raison de la nature du projet.

Il n'y a pas de raison que quelqu'un ait cherché à faire cela avant, et on pourrait dire que ce n'est pas nécessaire. Mais j'aime les défis. Toni Wilen avait fait remarquer qu'il aurait pensé qu'il serait difficile de synchroniser les chronomètres de l'Amiga émulé avec ceux d'une vraie disquette en train de tourner. J'ai supposé qu'il voulait dire physiquement faire correspondre la position de rotation de la MFM de la disquette à celle que l'émulateur pensait être, mais j'avais un plan.

Tout d'abord, afin de soutenir au mieux ce projet, j'ai décidé que je devais avoir un meilleur accès au lecteur de disquette. Bien que cela puisse fonctionner avec la configuration existante, il serait bien de pouvoir allumer et éteindre le moteur séparément des autres fonctions, et de détecter la présence d'une disquette dans le lecteur. Toutes ces fonctions sont disponibles sur la disquette, mais je n'en ai pas eu besoin et elles n'étaient pas correctement connectées. Il y a une modification détaillée pour cela dans la section "hardware". Elle est optionnelle mais hautement recommandée !

Mon plan était que si je pouvais, d'une manière ou d'une autre, transmettre les données au PC pour que WinUAE puisse les lire, alors tout devrait bien se passer. Je ne vais pas entrer dans les détails ici, mais bien que cela semble assez simple, la réalité est loin d'être simple.

Une partie du problème réside dans le fait que les données doivent être disponibles pour WinUAE lorsqu'il en a besoin, et non pas lorsque je les ai à disposition. Si je n'ai pas les données et que je fournis des données vierges, vous obtenez essentiellement des erreurs de lecture. Si toutefois je force WinUAE à attendre, le programme saccadera et se figera (même le son et le pointeur de la souris), ce qui est hautement indésirable et désagréable. Ce n'est la faute de personne, c'est juste le piège de ce que j'essaie de faire.

Maintenant, je peux mettre en cache une lecture de piste complète en mémoire, mais je dois être sûr qu'il s'agit d'une rotation complète et exacte sans chevauchement ni données manquantes, littéralement au bit près, ce qui n'est pas une mince affaire. Mais que dois-je demander à WinUAE de faire pendant qu'il le met en cache ?

Donc, premier défi. Lire une piste complète. Ah, cette impulsion /INDEX que le lecteur génère, ça doit aider. C'est le cas, mais pas au niveau de précision que j'avais espéré. C'est peut-être mon implémentation, mais j'ai trouvé que son impulsion vacillait un peu. Pas beaucoup, mais juste assez pour que ce ne soit pas assez fiable en l'état.

J'ai donc décidé de lire les données jusqu'à l'impulsion, de continuer à lire jusqu'à l'impulsion suivante, puis d'essayer de faire correspondre le chevauchement. Cela a fonctionné étonnamment bien, même si ce n'était pas aussi trivial que je le disais. Il a fallu plusieurs semaines pour que cela fonctionne de façon fiable, en s'assurant de ne pas manquer un seul bit.

L'inconvénient est que la disquette tourne d'un tour environ toutes les 200 millisecondes. Mais je dois d'abord attendre le marqueur d'index, qui peut se trouver jusqu'à 200 millisecondes plus loin si nous sommes vraiment malchanceux. Cela signifie que, dans le pire des cas, la lecture d'une piste entière peut prendre jusqu'à 400 millisecondes. Nous ne pouvons pas arrêter WinUAE pendant tout ce temps.

Au lieu d'attendre l'intégralité de la piste, je me suis demandé s'il n'était pas possible de fournir ce que nous avions jusqu'à présent, puis de bloquer le processus si nécessaire. Ce n'est pas bon. Il était encore trop instable pour être acceptable.

J'ai alors eu une idée. Nous parlons ici de matériel physique, qui ne fonctionne pas comme son homologue émulé. Par exemple, lorsque vous allumez le moteur pour faire tourner la disquette, vous ne pouvez pas lire les données immédiatement, vous devez généralement attendre jusqu'à 750 millisecondes. Je le sais, WinUAE le sait et le Kickstart le sait aussi.

Qu'en est-il du passage d'une piste à l'autre ? Y a-t-il un temps mort pendant lequel les données de la disquette ne sont pas disponibles ou ne sont pas considérées comme stables ? Il s'avère qu'il y en a un. C'est généralement jusqu'à environ 18 millisecondes. J'ai encore besoin de trouver jusqu'à 382 millisecondes de plus.

...ou je l'ai fait. J'ai eu une idée. Et si, jusqu'à ce que les données soient prêtes, je continuais à envoyer un "0" ou un "1" à WinUAE. Il n'y aurait pas de motif à verrouiller, et pas de données, mais compte tenu de la façon dont WinUAE fonctionne, il penserait toujours qu'une disquette est présente et tourne (il simule l'impulsion /INDEX). Cela permettrait-il d'allonger le temps mort ?

Il s'avère que oui. J'ai augmenté ce temps jusqu'à un maximum de 600 millisecondes avant que nous puissions potentiellement bloquer WinUAE ; et tout était fluide. Les données ont été lues, la souris et la musique ont fonctionné sans problème et une disquette s'est amorcée.

Les données étaient diffusées par l'Arduino en arrière-plan en utilisant un fil d'exécution séparé. J'avais mis en place un système de file d'attente dans lequel les commandes demandant de changer le numéro de piste, etc. étaient mises en file d'attente, mais nous simulions une réponse valide en sachant que la seule chose importante était de lire et d'écrire sur la disquette.

Toute commande émise arrêterait immédiatement ce flux, mais si elle était laissée, elle lirait continuellement les rotations exactes de la disquette, que WinUAE activerait et désactiverait à sa demande. Ma théorie est que cela peut aider à gérer la protection "bit faible" utilisée dans certains jeux, mais aussi corriger une erreur de lecture potentielle qui aurait pu se produire.

J'avais donc une disquette qui pouvait être lue avec succès et, bien que celle-ci n'était pas aussi rapide qu'un vrai Amiga, WinUAE ne semblait pas s'en soucier. Même X-Copy ne semblait pas s'en soucier.

Maintenant, le défi de l'écriture s'est présenté. J'avais à présent le nouveau code d'écriture de précomposition prêt à être utilisé. Avec un peu de chance, je pourrai aussi le fiabiliser.

Pour faire court, c'était un véritable monstre à faire fonctionner. La raison principale était la façon dont WinUAE gérait les données qu'il essayait d'écrire. Les données pouvaient se perdre et je ne savais pas où était le début de celles-ci. Donc, à la place, j'ai décidé de capturer les données telles qu'elles sont reçues de l'écriture DMA de l'Amiga. C'est ce que WinUAE faisait de toute façon, mais je pouvais les recevoir directement, et ensuite, quand finalement la requête DMA était terminée, je pouvais simplement mettre en file d'attente une requête pour l'écrire sur la disquette.

Cela fonctionnait beaucoup mieux. Le meilleur test était d'essayer de formater une disquette à partir du Workbench, car il fallait aussi vérifier les résultats. Mes astuces de synchronisation pouvaient-elles fonctionner ici aussi ? Il semblerait que oui. J'étais maintenant capable de formater une disquette à partir du Workbench, de vérifier le formatage et de lire un fichier MOD sans aucune interruption de l'audio. Mission réussie.

Je voulais cependant une meilleure gestion, car j'avais quelques disquettes protégées contre la copie qui ne démarraient toujours pas. Je soupçonnais que cela avait quelque chose à voir avec des astuces consistant à faire varier légèrement les synchronisations des cellules de bits pour que certaines zones de la disquette soient lues plus rapidement que d'autres. C'est une technique assez courante. J'ai ajouté du code au flux de lecture pour savoir si un bit était lu normalement, rapidement ou lentement, que j'ai ensuite simulé dans WinUAE. Malheureusement, ce n'était pas suffisant, donc je suppose que la carte Arduino ne va pas aller plus loin. Je suppose que ces jeux étaient des copies pirates.

J'avais conçu cette interface pour également gérer d'autres matériels. Après tout, ajouter la gestion pour ce matériel avait été assez complexe, non seulement au niveau des considérations physiques, mais aussi pour le fait de comprendre ce qu'il se passait dans l'émulateur !

Le code a donc été écrit avec un ensemble de fonctions communes (classe abstraite) qui devaient être implémentées afin de gérer différents matériels, ce qui rendrait les choses beaucoup plus simples à l'avenir, et je voulais toujours atteindre mon objectif de démarrer correctement un jeu protégé contre la copie. J'en avais deux à essayer, Captain Planet et Lemmings, tous deux fournis avec le lot Cartoon Classics que j'ai eu avec l'Amiga 500+. Ces deux jeux ne fonctionnaient pas quand on les copiait, et tous deux ne démarraient que jusqu'à un certain point et s'arrêtaient sur la copie au même endroit où ils s'arrêtaient quand on les utilisait dans WinUAE avec mon lecteur.

WinUAE et les disquettes protégées contre la copie

Je n'aime pas abandonner. Je suis heureux que les disquettes AmigaDOS standards démarrent, je me suis tourné vers la protection contre la copie. Je savais déjà beaucoup de choses sur ce qu'il se passait, des pistes courtes/longues, des zones faibles/non formatées, etc. Je pensais que si je pouvais lire les données avec suffisamment de précision, je ne voyais pas pourquoi l'émulateur ne pourrait pas lancer ces jeux.

Le problème était les données. J'avais une bande passante très limitée sur le port série de l'Arduino. Nous fonctionnons à 2 Mbauds, ce qui (à 8 bits, 1 bit d'arrêt et 1 bit de départ) signifie que nous devrions pouvoir transmettre un octet tous les 5 microsecondes. Cependant, à partir de la disquette, la séquence la plus courte, "01", pourrait arriver à des intervalles de 4 microsecondes. Parfois même plus rapidement, en fonction de la vitesse du lecteur utilisé par rapport à celui qui écrit.

J'ai donc d'abord décidé de regarder ma disquette de Lemmings. Je savais qu'elle ne démarrait pas si on la copiait avec X-Copy, donc je savais qu'il y avait quelque chose de louche.

J'ai ajouté une commande supplémentaire au firmware de l'Arduino qui me donnerait (pour une seule rotation complète) une décomposition des temps pour les cellules de bits trouvées sur un cylindre. Il y avait évidemment des points chauds autour des intervalles de 4, 6 et 8 microsecondes, comme prévu. Mais il y avait des pistes qui présentaient beaucoup plus de variations.

Je savais qu'il y avait des astuces pour enregistrer les données à des débits légèrement différents, la théorie étant que si vous lisez la piste, la chronométrez et la comparez à une autre piste, vous pouvez mesurer la différence et détecter l'original par rapport à la copie (car la copie corrigerait cette légère incohérence de chronométrage). Nous parlons ici de très petites différences, si elles étaient importantes, le lecteur ne pourrait pas lire correctement les données.

Dans la version précédente, j'avais essayé d'ajouter une option "fast" (rapide) et "slow" (lent), mais cela ne fonctionnait pas. J'avais besoin de quelque chose de plus sophistiqué. Auparavant, j'avais essayé d'indiquer cette vitesse par cellule binaire, mais fallait-il vraiment être aussi précis ?

Je me suis demandé ce qu'il se passerait si je faisais la moyenne des temps sur plusieurs motifs, et que je l'envoyais au PC, et que je l'utilisais pour dire à WinUAE de lire les données légèrement plus vite ou plus lentement. Je savais qu'il pouvait le faire, j'avais trouvé quelques enregistrements SCP de ces disquettes et ils ont démarré, donc je savais que c'était possible.

Le format final que j'ai obtenu consistait à renvoyer un octet à l'ordinateur avec les informations suivantes :

Bits d'un octet
7 6 5 4 3 2 1 0
Index Cellule Cellule Vitesse
  • Index : ce bit est activé lorsque l'impulsion /INDEX est détectée.
  • Cellule : ceci représente ce que nous avons trouvé. C'est un 01=1, 001=2, 0001=3 ou autre chose=0.
  • Vitesse : vitesse moyenne pour les deux cellules.
La vitesse a été simplement calculée en additionnant la valeur du chronomètre - le début de la synchronisation de la cellule. Par exemple, si le temps était à 4,2 microsecondes, on enlève le 4,0. Bien entendu, ces valeurs sont exprimées en tics et non en microsecondes, mais vous comprenez l'idée. Cela donnerait (pour chaque cellule) un nombre entre 0 et 31, 15/16 étant le centre ou la "vitesse est 100% correcte".

Une fois additionné, ce nombre se situerait entre 0 et 63, donc nous le décalons de trois à droite (divisé par 8), ce qui nous donne un nombre entre 0 et 7, ce qui convient parfaitement. Mais est-ce que cela serait d'une résolution assez élevée pour résoudre le problème.

Une fois présenté correctement à WinUAE, Lemmings a démarré ! Donc, quelle que soit la méthode utilisée pour la synchronisation des cellules de bits, nous l'avons simplement ignorée. Maintenant, cela pourrait ne pas fonctionner pour tous les jeux qui utilisent cette technique, et si une plus haute résolution est nécessaire plus tard, nous devrions faire la moyenne des temps sur quatre cellules à la place.

Lemmings
Temps de la piste 0, face supérieure, disquette de Lemmings
Ici, certains temps de cellule binaire sont suspects, mais bien dans les limites de la tolérance !


Alors, et pour Captain Planet ? Pas de chance. Quelle que soit la méthode utilisée, ce n'était pas ça. Alors que faisait-il ? Encore une fois, j'ai essayé d'analyser les temps sur la disquette pour voir ce qu'il se passait. La plupart des pistes étaient assez typiques, mais il y en avait deux, en particulier la piste 0 de la face inférieure, qui était vraiment étrange.

Le meilleur moyen est de vous montrer à quoi ressemble une copie du contenu SCP de cette piste dans l'analyseur de piste de HxCFloppyEmulator. Je ne l'ai découvert que récemment :

SCP1

Ainsi, les sections vertes représentent onze secteurs de données. Il les détecte même comme AmigaDOS. Tout a l'air normal, les données se trouvent dans les 4, 6 et 8 microsecondes comme vous vous y attendez. La zone blanche est le "vide" à la fin qui remplit généralement l'espace pour compléter une rotation complète. La plupart du temps, cet espace est ignoré, et de toute façon, comme je dois fournir une révolution précise (ou rotation complète) des données à WinUAE, il est quoi qu'il en soit inséré. Les copieurs ignorent généralement cela, mais cela semble étrange, comme s'il y avait des données là aussi.

En regardant de plus près, vous pouvez voir ceci :

SCP2
Disquette visuelle pour Captain Planet, piste 0, face inférieure - intervalle des pistes

C'est un peu étrange. Donc, en ignorant les zones vertes qui sont des secteurs de données, la moitié droite de l'image est juste la séquence 0xAA (01010101) que vous voyez souvent, c'est normal, mais qu'est-ce qu'il se passe sur le côté gauche ?

Cela doit avoir quelque chose à voir avec cela. Mon code continuait déjà à fournir des révolutions de données à WinUAE, ce qui lui permettait de contourner le schéma de protection "bit faible" puisque les données mal lues étaient présentées différemment à chaque fois. Mais que se passait-il ici ?

J'ai décidé de prendre une seule rotation complète de données à partir du fichier SCP, et de l'insérer dans WinUAE, juste pour cette piste, et de lui faire lire le reste de la disquette comme avant. Et le jeu a démarré ! Il est donc clair qu'il n'avait pas besoin de plusieurs révolutions de données. Alors que se passait-il ? Si vous regardez l'enregistrement, cette dispersion de points est différente, donc il y a quelque chose qu'il se passe.

Il n'y a clairement aucun moyen d'envoyer cette folle quantité en 2 Mbauds. Je n'ai tout simplement pas la bande passante. Mais j'ai quand même quelques astuces.

Ce cas de "quelque chose d'autre" (plus de trois "0" à la suite) que j'ai pu recevoir, et que je traitais comme une erreur, commence peut-être à signifier quelque chose de plus utile.

J'ai recommencé à me documenter sur les disquettes, et ce qu'elles font s'il n'y a pas de flux de données pendant un certain temps. Si vous remarquez les synchronisations, certaines d'entre elles sont très élevées, entre 11 et 12 microsecondes. C'est hors de portée des données MFM.

Les lecteurs sont donc équipés d'un circuit qui ajuste automatiquement l'amplification de la tête lors de la lecture pour compenser les disquettes enregistrées avec des champs magnétiques d'intensité différente. Après tout, les disquettes plus anciennes fonctionnent encore, et leurs données magnétiques auront commencé à se dégrader.

Apparemment, après environ trois zéros d'affilée, il continue à essayer de se régler, et finalement cette amplification devient si élevée qu'un bit peut être détecté alors qu'il n'existe pas. C'est comme ça que le truc du "bit faible" est censé fonctionner. Quand il détecte un vrai bit, il est massivement surchargé et commence à revenir à ses niveaux précédents, et tout le processus recommence.

En regardant les données que mon code lisait, ce cas spécial était déclenché assez souvent, mais comment le traitais-je ? De manière incorrecte évidemment. Mais je ne pouvais pas signaler les séquences de 00001 ou plus, je n'avais tout simplement pas les bits nécessaires. Mais si je n'avais pas besoin de le faire ? Et si j'avais seulement besoin de savoir que nous avions dépassé la possibilité de la séquence 0001, mais sans détecter un "1", c'est-à-dire en confirmant une séquence "0000" ?

J'ai donc fait quelques modifications pour que, lorsque cette séquence était déclenchée, elle génère quatre bits "0", mais le jeu ne démarrait toujours pas. J'ai donc examiné la condition qui a déclenché cela. Nous attendions dans une boucle, jusqu'à ce que nous voyions un bit, ou que le chronomètre aille trop loin. Le dépassement étant ce qui a pu déclencher, ou un bit bien au-delà des normes. Cela signifie que 00001 et 00000 seraient détectés comme la même chose. Mais ils ne le sont pas.

J'ai rendu cela plus strict. J'ai modifié la boucle pour qu'elle se termine si le chronomètre atteint la limite de 8 microsecondes ou atteint un bit. A moins que le bit ne soit exactement sur cette limite, nous saurions maintenant avec précision que nous avons trouvé un 0000, et tous les futurs bits "1" seraient détectés sur la boucle suivante. Par exemple, nous pourrions détecter 0000 et ensuite 01, ce qui signifie que nous avons détecté avec succès 000001.

Est-ce que ça a marché ? Non. J'ai dormi là-dessus et j'y suis revenu dans la matinée.

Une idée m'est venue. Entre les vérifications des bits, j'effectue d'autres tâches, comme envoyer des données au PC ou vérifier si la broche /INDEX a émis un signal. Et si une impulsion de bit arrivait pendant ce temps, et que l'impulsion était suffisamment courte pour qu'au moment de la vérification, elle ait disparu et que nous l'ayons manquée.

Existe-t-il une meilleure façon de surveiller les données ? En apprenant comment je détecte maintenant l'impulsion /INDEX, j'ai commencé à regarder la fiche technique. La méthode que j'ai utilisée n'a pas fonctionné car les données n'arrivaient pas sur une broche qui pouvait détecter les fronts montants ou descendants. L'ATMega328 vous permet cependant de configurer une interruption pour le changement d'état de chaque broche.

Je ne veux pas d'interruption, mais il faut signaler qu'une interruption doit être générée. Bien sûr, si je fais ceci :

PCMSK2 = bit(PCINT20);
PCICR = bit(PCIE2);

...cela devrait normalement déclencher une interruption lorsque l'état de la broche (4) change. Mais vous ne pouvez détecter que les changements, pas les bords. Ce n'est pas un problème majeur. Si nous savons que nous ne sommes pas dans une impulsion, alors le changement serait l'impulsion, et tant que nous attendons jusqu'à ce que l'impulsion est fini avant de réinitialiser alors tout devrait être bon. Notre boucle principale de "détection d'impulsions" ressemble maintenant à ceci :

while ((!(PCIFR & bit(PCIF2)))&&(!(TIFR2 & bit(OCF2B)))) {}

En gros, j'attends dans la boucle que la broche change ou que le chronomètre atteigne le seuil supérieur de 8 microsecondes. Puis à la fin, j'ajoute ce qui suit :

while (!(PIN_READ_DATA_PORT & PIN_READ_DATA_MASK)) {};
PCIFR |= bit(PCIF2);

Bingo ! Le jeu a démarré ! Vous ne pouvez pas croire à quel point j'étais heureux ! Alors que ce qui précède semble n'être que quelques jours de travail, j'y ai travaillé soir après soir, en rentrant du boulot, pendant environ un mois pour arriver à ce résultat.

Cette protection contre la copie utilisait deux méthodes, d'abord, des secteurs valides que tout pouvait lire correctement, puis des données cachées dans l'espace entre les secteurs, mais cachées entre les zones non formatées. Ces données ne seraient normalement pas copiées.

Maintenant, je ne dis pas que cela va contourner toutes les protections anti-copie, mais ce sont les seules que j'ai pu essayer, et elles fonctionnent toutes les deux. Si quelqu'un en a d'autres qui ne fonctionnent pas avec ça, nous devrions voir si je pourrais obtenir une copie et l'essayer.

Mais pour l'instant, j'ai terminé. J'ai eu beaucoup de plaisir à essayer d'atteindre cet objectif impossible, et je suis heureux de ne pas avoir abandonné. Ces petits appareils Atmel peuvent faire tellement plus que ce à quoi vous vous attendez si vous prenez le temps d'étudier la fiche technique. Ils me rappellent un peu l'Amiga, à leur manière. Ils ont beaucoup d'options spéciales comme des chronomètres, des compteurs de surveillance, etc. disponibles au niveau matériel qu'il suffit d'activer, laissant le processeur faire d'autres choses. Une grande partie du code que je vois sur Internet me rend triste. Je vois que le PWM est fait manuellement alors que le matériel peut le faire. Je vois des gens qui veulent compter les impulsions de manière externe, et se retrouvent dans des boucles, et parfois des interruptions, alors que rien de tout cela n'est nécessaire. Si vous vous intéressez à la programmation embarquée, je vous conseille vivement de lire la fiche technique et le guide de programmation ! Il y a tellement plus pour ces petits appareils.

Dites NO à CH340

J'ai décidé de faire quelques vérifications sur les différences entre les convertisseurs USB/série FTDI et CH340. En bref, les deux fonctionnent parfaitement pour de petites quantités de données (comme quelques octets), mais pour des flux/grandes quantités de données, le CH340 ne peut pas faire face et les données commencent à être corrompues. Donc, n'utilisez pas de convertisseur CH340 pour ce projet.

En même temps, j'ai testé le convertisseur USB/série sur l'Arduino UNO. Même problème. Donc, pour ce projet, vous devez utiliser le convertisseur FTDI.

Kickstart 1.3

N'ayant jamais possédé d'Amiga avec quelque chose d'antérieur au Kickstart 2.04, je n'ai jamais vraiment eu l'occasion de le tester. C'était jusqu'à ce que j'essaie d'écrire quelques ADF sur de véritables disquettes pour pouvoir les essayer. Les disquettes étaient de vieilles démos, très bonnes, mais elles n'étaient pas compatibles avec le Kickstart 2+. J'ai trouvé une de mes vieilles disquettes qui contenait une copie de Relokick et j'ai redémarré en Kickstart 1.3.

Bizarrement, les disquettes que j'avais créées ne se chargeaient pas du tout. C'était comme si le système d'exploitation ne détectait rien. Après être retourné au code et avoir comparé les données d'une disquette qui fonctionnait et d'une disquette que j'avais écrite, j'ai découvert quelque chose de très intéressant.

A l'époque où je travaillais sur le lecteur, j'avais lu qu'il y avait un bogue dans le Kickstart 1.3 et que certaines disquettes étaient codées avec 2AAAAAAA44894489 (commençant par un 2, pas un A). Après un examen plus approfondi, j'ai vu que ce n'est pas un bogue. Le "2" a dû être placé à cet endroit, sinon la séquence MFM se serait retrouvée avec deux "1" d'affilée, et cela n'est pas autorisé.

Je sais que le trackdisk.device du Kickstart 2 et plus a été réécrit, donc je suppose qu'ils ont ignoré la partie 2AAAAAAA et ont juste cherché le mot de synchronisation (4489). Après avoir corrigé cela et réécrit la disquette, tout a fonctionné parfaitement.

WinUAE - mais plus rapide ?

J'ai été très satisfait de l'accès aux disquettes de WinUAE, et à l'époque, mon code n'était pas disponible publiquement. J'avais cependant distribué des copies à quelques personnes pour des tests supplémentaires.

Un de ces testeurs est membre de la scène démo et avait indiqué que certaines de ces démos faisaient des choses folles avec l'accès aux disquettes. Une démo spécifique dont il m'a parlé s'appelle State Of The Art de Spaceballs.


State Of The Art

J'ai donc décidé de télécharger l'ADF de cette démo, de l'écrire sur une disquette et de l'essayer sur mon Amiga. C'est vraiment une démo intéressante, d'un point de vue technique. Dès que la disquette est insérée, la démo démarre, presque instantanément, et pendant qu'elle tourne, elle charge les séquences suivantes en arrière-plan. Toute la démo est chronométrée de façon à ce que les animations et les effets durent suffisamment longtemps pour que les données soient lues sur la disquette.

Pour le plaisir, je l'ai essayé sur WinUAE. Ce n'était pas très bon. La disquette ne pouvait tout simplement pas suivre. Je n'ai pas été surpris, regardons les choses en face, il y a beaucoup de choses à faire pour que cela fonctionne.

Ainsi, je me suis demandé si je pouvais accélérer l'accès à la disquette sans affecter les performances de WinUAE. J'avais besoin d'une base de référence à mesurer.

Ma ligne de base est d'utiliser la disquette de Captain Planet. J'ai chronométré le temps écoulé entre l'apparition de la fenêtre de WinUAE et le début de la musique du jeu, soit 21 secondes seulement :


Captain Planet sur WinUAE, du début à la musique, 21,23 secondes

Il y a plusieurs facteurs qui affectent cette lenteur. Les principaux sont que lorsque l'émulateur passe du côté piste/disquette, je commence à lire les données instantanément. Mais elles ne seront pas disponibles pour l'émulateur avant environ 200 millisecondes dans le meilleur des cas. L'émulateur rejoue ensuite ces données, ce qui prend 200 millisecondes supplémentaires. Le problème est que le code doit extraire une rotation complète de la disquette pour que WinUAE voie une piste complète qui tourne parfaitement en boucle.

J'y suis parvenu en notant où l'impulsion /INDEX se trouvait sur la disquette, puis en capturant toutes les données entre ce moment et celui où elle se produit à nouveau. L'impulsion /INDEX n'est pas parfaitement alignée, mais elle est suffisamment proche pour que nous puissions faire une comparaison approximative afin d'aligner parfaitement les données.

Donc, avant même de commencer à lire les données à envoyer à l'émulateur, nous devons attendre la première impulsion /INDEX. Dans le pire des cas, c'est 200 millisecondes. Donc, pour une seule piste, cela peut prendre jusqu'à 600 millisecondes avant qu'elle ne soit traitée par AmigaOS, dans le meilleur des cas, environ 400 millisecondes.

Je me suis demandé s'il y avait un moyen d'éviter cela et de capturer une rotation complète sans les impulsions /INDEX. J'ai donc entrepris de réécrire le code, car c'était un peu le bazar, et maintenant que j'ai compris comment tout cela fonctionne, je pourrais probablement l'écrire un peu mieux.

Je me suis dit que je connaissais le temps nécessaire pour chaque cellule binaire, et que je pouvais donc inventer un marqueur /INDEX virtuel en chronométrant environ 200 millisecondes de données et en corrigeant ensuite le résultat.

Eh bien, cela n'a pas très bien fonctionné. Il s'avère, et j'aurais dû m'en rendre compte, que tous les lecteurs ne tournent pas exactement à 300 tours par minute, ce qui ne correspond pas exactement à 200 millisecondes.

J'ai donc décidé d'utiliser les impulsions /INDEX pour déterminer la durée d'une rotation, puis d'utiliser cette information pour mieux aligner une piste. Cela a fonctionné, et après quelques ajustements, j'ai obtenu un chargement légèrement plus rapide et amélioré. Nous sommes passés de 21,23 secondes à 19,5 secondes, soit un gain de près de 2 secondes !


Captain Planet sur WinUAE, du début à la musique, 19,5 secondes

Pas mal. Après avoir regardé quelques disquettes démarrer, j'ai eu quelques autres idées. Tout d'abord, chaque fois que l'émulateur demande quelque chose au lecteur, le code interrompt la lecture de ce qu'il est en train de lire. Les pistes incomplètes sont rejetées. Donc, si au moment de la demande d'interruption, la piste était complète à 90% ou plus et que le code n'avait pas d'enregistrement de cela auparavant, alors l'interruption était ignorée et la lecture de la piste pouvait s'achever. Techniquement, cela pourrait ralentir le processus d'environ 20 millisecondes, mais il s'agit d'un cas limite tellement petit qu'il accélérera la lecture de la piste par la suite.

Après avoir mesuré cela, je n'ai pas vraiment vu de différence. Alors que pouvais-je faire d'autre ? J'ai remarqué que les jeux que j'avais semblaient être bien optimisés. Typiquement, ces disquettes voyaient une piste, la lisaient, puis passaient à l'autre tête de lecture (supérieure ou inférieure) et la lisaient.

J'ai décidé qu'après avoir extrait une rotation complète et l'avoir présentée à l'émulateur, pendant que le système d'exploitation la lisait, je changerais de côté et la lirait aussi. C'est un temps mort, autant l'utiliser à quelque chose. Je pense que c'est ce qu'on appelle le "cache read-ahead" (cache de lecture anticipée), ou du moins c'est comme ça que je l'appelle. Pour les disquettes qui ne sont pas optimisées, cela ne fait aucune différence, mais essayons notre test de performances.

J'ai démarré la disquette, et oh, 16,14 secondes ! C'est presque 5 secondes plus rapide que la première tentative, et 3,5 secondes plus rapide que la tentative précédente :


Captain Planet sur WinUAE, du début à la musique, 16,14 secondes

Donc avec ces changements, pour ce jeu, j'ai accéléré l'accès d'environ 24%. C'est génial ! Ne vous méprenez pas, nous ne lisons pas la disquette plus rapidement, nous la lisons juste plus efficacement. Maintenant, qu'en est-il de la démo originale que j'avais essayer ? Eh bien, elle fonctionne beaucoup mieux, elle ne peut toujours pas garder le rythme mais c'est beaucoup mieux. Pour référence, sur une vraie machine, Captain Planet prend un peu plus de 12 secondes, donc il y a encore beaucoup d'améliorations qui pourraient être faites... si je pouvais juste trouver comment...

Une dernière chose, j'ai décidé d'appeler toute cette technologie (pour le plaisir) "RIT" (comme JIT - Just in Time) qui signifie "Read in Time(ish) (lire en temps réel).

La fantastique vidéo par RetroManCave - The Cave (juin 2021)

Je suis sûr que la plupart d'entre vous ont découvert mon projet grâce à la fantastique vidéo que Neil Thomas de RMC The Cave a réalisée en juin 2021.

En tant que mécène de longue date de sa chaîne YouTube, je me suis dit que ce serait bien qu'il soit celui qui annonce officiellement la nouvelle concernant WinUAE et j'étais en contact avec Neil Thomas pendant environ un mois avant de lui envoyer quoi que ce soit. Le premier envoi fut la carte Nano qu'il a brièvement montré dans une de ses vidéos, et comme il a reconstruit sa cave dans un nouvel endroit, il n'a pas pu prendre le temps de l'examiner jusqu'à ce que je lui envoie un lecteur slimline.

Travailler avec Neil Thomas a été fantastique. Nous nous sommes rapidement appelés sur Discord pour parler du projet, puis nous n'avons plus eu qu'à attendre, même si je n'avais aucune idée de ce qu'il y aurait dans la vidéo avant qu'il ne la diffuse.

Comme vous l'avez vu, la vidéo est géniale et Neil Thomas a fait un très bon travail en racontant l'histoire de la bonne manière. Je pense qu'aucun de nous ne s'attendait à ce que la vidéo soit visionnée autant de fois, et je remercie tout le monde pour le soutien. Je me fraie lentement un chemin dans la liste d'attente, alors soyez patients.

Un nouveau nom ?

Après plusieurs années à l'appeler par un nom long et dingue avec "Arduino" et "disquette", un nom plus recherché est apparu sur Discord... DrawBridge.

"Disk Reader And Writer BRIDGE" (entre le lecteur et l'ordinateur).

Fonction "Pas de clic" dans WinUAE (juin 2021)

J'ai fait quelques expérimentations et bien que tout le monde soit enthousiaste à l'idée d'entendre le lecteur cliqueter, il était temps de faire fonctionner la fonction "no-click" (pas de clic). C'était en fait très simple car elle est déclenchée lorsque l'émulateur essaie de passer à la piste -1. Ce n'est pas autorisé. Donc tout ce que j'avais à faire était de capturer cet événement (qui était ignoré) et de demander au lecteur de faire la même chose. Et ça a marché ! C'était facile...

Écriture améliorée à partir de WinUAE (juin 2021)

J'ai donc expérimenté l'écriture sur disquette dans WinUAE. Certaines écritures étaient intermittentes. En fait, l'écriture était correcte, mais diverses applications signalaient des erreurs.

Le problème est qu'il faut environ 200 millisecondes pour collecter les données de l'émulateur, puis 200 millisecondes pour les écrire sur la disquette, puis encore 200 millisecondes pour les relire, et enfin 200 millisecondes pour que l'émulateur les relise. Le problème est qu'une fois que l'émulateur a soumis ses 200 millisecondes de données, il les relit instantanément pour les vérifier. Mais je ne pouvais pas encore fournir les données et donc je ne faisais que renvoyer mon "1" habituel (pas de données). Il s'avère que la plupart des opérations se sont arrêtées avant que les données ne soient disponibles. Ce n'est pas vraiment surprenant...

Alors comment contourner ce problème ? Il s'est avéré que la protection contre la copie ne se préoccupe probablement pas de l'écriture sur la disquette, et bien que cette solution ne fonctionnerait probablement pas dans toutes les situations, elle pourrait être utile ici.

Typiquement, lors de l'écriture sur la disquette, l'Amiga met en place un transfert DMA. Ceci est évidemment simulé dans WinUAE. Vous avez peut-être remarqué les réglages de la vitesse de la disquette, et bien, en fait, ils perturbent les vitesses de transfert DMA.

J'ai décidé de prendre les données du tampon mémoire DMA au moment où il démarre et que je les écrirais sur la disquette immédiatement. Ainsi, je me suis débarrassé du premier et du second délai de 200 millisecondes ! Mais je devais encore les relire. Je me suis dit que si je relisais les données, et seulement quand j'avais lu une rotation complète de données, je pourrais alors signaler à l'Amiga que l'opération DMA est terminée.

Étonnamment, cela a fonctionné ! J'ai essayé la copie de la disquette avec X-Copy. Cela a fonctionné. J'ai même essayé de faire la copie d'une vraie disquette vers une autre... Incroyable, ça a aussi marché ! C'était assez facile...

Alors, que va-t-il se passer ensuite ?

Je veux ajouter la gestion des disquettes Amiga haute densité ainsi que la possibilité de réécrire un fichier SCP sur la disquette. Le serveur Discord est très actif et je reçois constamment des retours.

L'ensemble du projet est libre et Open Source sous la licence GNU General Public Licence V3. Si nous voulons avoir un quelconque espoir de préserver l'Amiga, nous ne devrions pas nous arnaquer les uns les autres pour ce privilège. En outre, je veux apporter quelque chose en retour à la meilleure plate-forme sur laquelle je n'ai jamais travaillé. J'espère aussi que les gens développeront ce projet, le feront évoluer et le partageront.

Je me suis rendu compte que cette interface est également valable pour d'autres formats. Et bien que je ne les gère pas directement avec mes applications Windows (vous pouvez extraire une disquette double densité via l'option SCP), le lecteur devrait être capable de lire n'importe quel format de disquette MFM. Il faut juste écrire le code pour décoder les données de la piste.

Je garderai un oeil sur ce projet, et je ne sais pas si c'est à cause de ce projet ou si c'est juste une coïncidence, mais il y a quelques autres projets qui font la même chose, pas spécifiquement pour l'Amiga, et en utilisant du matériel différent, mais dans tous les cas c'est bon à voir. Je vais continuer à étudier et à améliorer la gestion de WinUAE quand le temps le permettra. Si quelqu'un a une idée de ce qui ne se charge pas, vous êtes les bienvenus !


[Retour en haut] / [Retour aux articles]


Soutenez le travail de l'auteur