À la découverte du FDC – Épisode 1

par roudoudou.

Née au tout début des années 70, la disquette a été conçue chez IBM par David Noble sous la supervision d’Alan Shugart. Retenez bien ce dernier nom car c’est celui qui nous intéresse. Shugart quitte IBM pour Memorex qu’il quitte à nouveau quelques années plus tard pour fonder sa propre société: Shugart Associates. Le marché est porteur, les fabricants sont nombreux et tout le monde se tire un peu la bourre côté innovation mais au milieu des années 80, si il y a toujours pléthore de constructeurs, il ne reste guère que deux normes: La norme IDC34 du nom de son connecteur pour les PC et la norme Shugart pour le reste.

La norme Shugart, utilisée par le FDC du CPC, permet de contrôler jusqu’à 4 lecteurs de disquettes là où la norme IDC34 ne permet d’en piloter que 2 (ce qui explique pourquoi la lettre d’un disque dur sur PC commence toujours par C !). Par contre, là où un PC à la norme IDC34 peut éteindre et allumer chaque moteur de lecteur indépendamment, notre CPC va allumer ou éteindre tous les moteurs en même temps. La signification du Ready est différente, ce qui occasionne quelques menus problèmes quand on veut utiliser un lecteur “moderne” sur un CPC. Oui, je sais, nous sommes en 2021 et cela fait 10 ans que les disquettes ne sont officiellement plus fabriquées.

Le READY du lecteur 3″ CPC indique que le lecteur a lu deux fois le trou d’index après un démarrage moteur (c’est la donnée constructeur que j’ai trouvée dans la doc d’un lecteur SA850/851, 3 fois l’index si le lecteur est double face). Il suffirait d’interroger l’état des lecteurs et de commencer à travailler dès que le lecteur est en READY mais il n’est pas garanti que la disquette ait atteint sa vitesse nominale. Dommage, on éviterait d’attendre cette seconde interminable lorsqu’on tape un CAT avant de commencer à lire mais on perdrait aussi un peu en compatibilité si on a greffé un lecteur d’une autre norme sur le CPC. Fort heureusement, les lecteurs récents démarrent quasi instantanément et les Gotek supportent le SHUGART (c’est le comportement par défaut du firmware libre FlashFloppy). Pire, les Goteks ne tiennent absolument pas compte de la commande moteur, ils se considèrent toujours allumés, ce qui m’a causé des tonnes de surprises et d’oublis !

Les FDC aussi ont leurs petites différences. Ainsi, j’avais lu que le Western Digital qui équipe les Atari se faisait berner par de faux entêtes secteurs à l’intérieur d’autres secteurs, simplement en écrivant des entêtes avec des données ! Un peu dangereux pour un contrôleur de confondre des données et des entêtes non ?

En fouinant sur CPCWiki, j’ai appris que le CPC a eu 3 références différentes de FDC :

  • Nec D765AC & Nec D765AC-2
  • UMC UM8272A
  • Zilog Z0765A08PSC

En fonctionnement normal, les FDC sont identiques à s’y méprendre, tout au plus quelques pouièmes de timing sans incidence sur des routines classiques et probablement pas dû au FDC lui même (des modifications de carte mère, le passage CPC vers Plus ?). Ils réagissent vraiment comme de simples clones les uns les autres, au point que leur documentation semble recopiée, parfois jusqu’à la virgule près ! Seul le nom du fabricant changeant la page de garde.

Comme certains le savent, le FDC fonctionne de manière asynchrone avec le CPC. Son comportement est peu prédictible car le FDC n’est pas un composant comme les autres. Il contient en fait une ALU, une ROM et une RAM. Cette particularité permet d’avoir une puce d’encombrement réduit, d’économiser les coûts et aussi de faciliter les adaptations entre modèles. Le programme du FDC fait donc sa petite vie et quand on lui donne des ordres, il met du temps à répondre. Il met aussi du temps avant de commencer à faire ce qu’on lui demande ! Longtemps !

On accède au FDC via deux ports. Un port qui ne sert qu’à consulter son statut et un port I/O auquel on ne doit accéder qu’après avoir reçu l’autorisation d’y venir, via le statut qui passe à l’état disponible. Ce même statut précisera ce qu’attend le FDC : recevoir une donnée, ou en envoyer une. Ainsi que son type dans les phases de lecture/écriture. et si il vous prenait l’envie de passer outre ce que le FDC attend de vous, il ne tiendrait tout simplement pas compte de ce que vous lui envoyez. Pire, certains accès non autorisés pourraient le mettre dans un état encore plus bloquant. Il faut donc dans le cadre de routines robustes s’assurer que le dialogue est parfait.

Description du registre d’état principal du FDC qui nous informe à tout moment sur ce qu’il est possible de faire :

bit 7 | disponible au dialogue
bit 6 | 0 : prêt à recevoir / 1 : donnée à récupérer
bit 5 | 0 : port donnée sur commande ou résultat / 1 : port donnée en mode exécution
bit 4 | instruction en cours
bit 3 | seek drive D
bit 2 | seek drive C
bit 1 | seek drive B
bit 0 | seek drive A

Seuls les 3 bits supérieurs nous intéressent, les autres bits sont éventuellement le témoin d’opérations internes, comme lorsque lors d’un déplacement de tête, le FDC scanne la présence des autres lecteurs, on voit qu’il passe alternativement les lecteurs en revue. Mais ils ne sont pas à prendre au pied de la lettre. Le bit instruction en cours peut tarder à s’activer ou s’effacer prématurément. Les bits supposés indiquer l’occupation d’un lecteur sont inutilisables, on utilisera les interruptions du mode pooling.

Exemple propre d’envoi d’une commande au FDC. On utilisera une première routine sécurisée avec un timeout de disponibilité et un test de sens car lors du premier appel, on ne peut être sûr de l’état du FDC :

sendFDCcommand
push hl,bc,af
ld bc,#FB7E   ; statut port
ld hl,0       ; init timeout counter
.waitready
inc hl
ld a,h : or l : call z,unblockFDC     ; au bout d'une seconde on débloque
in a,(c) : jp p,.waitready            ; tant que bit 7 nul, on attend
bit 6,a : call nz,unblockFDC          ; si bit 6 alors le FDC veut renvoyer quelque chose
inc c                                 ; sinon tout est ok pour envoyer au FDC
pop af
out (c),a
pop bc,hl
ret

sendFDCparam
push bc
ld bc,#FB7E   ; statut port
.waitready in 0,(c) : jp p,.waitready ; tant que le FDC est occupé, on attend
inc c         ; I/O port
out (c),a
pop bc
ret

Il n’y a pas besoin comme on peut le voir dans les ROM Amsdos et les routines de ceux qui s’en sont inspiré d’ajouter des délais à la main (boucles d’attente, suite d’instructions lentes type EX (SP),HL). Le FDC est robuste et ne plante jamais tout seul. Il peut se retrouver vraiment en boucle infinie avec des lecteurs bricolés ou un problème matériel mais nous nous contenterons de gérer le cas de reset sauvage et d’état du FDC indéterminé, ce qui est déjà pas mal.

De mes petites expériences, il est arrivé quelques fois que le FDC soit dans un état léthargique. C’est à dire que bien qu’il n’ait rien à faire, il refusait de se rendre disponible au dialogue. Dans ce genre de cas, on peut quand même essayer de lui envoyer une commande dont il tiendra compte. Pour éviter tout malentendu, on lui demandera une chose simple, son numéro de version. Puis on verra si il y a des choses à lire sur le port I/O.

UnblockFDC
in a,(c)
call p,.forceIO ; tant qu'il n'a vraiment rien à nous dire, on tape dans le port I/O
.ready
inc hl
ld a,h : or l : jr z,UnblockFDC
in a,(c) : jp p,.ready
bit 6,a     ; 0:FDC prêt à recevoir 1:FDC prêt à envoyer
ret z       ; FDC dispo à écouter et en attente, on s'en va
inc c
in a,(c)    ; dépiler une donnée du port I/O
dec c
jr .ready

.forceIO inc c : out (c),0 : dec c : ret ; demande de version de la puce

Avant toute chose, il faut configurer le FDC pour son lecteur. C’est ce que fait la ROM Amsdos du CPC au démarrage de la machine via la commande 3. Pour une raison que j’ignore, le réglage n’est pas exactement le même entre les CPC et les Plus avec un Head Unload Time (retrait de la tête de lecture) à 240ms au lieu de 16ms. Dans l’inventaire des bonnes pratiques, on devrait mettre cette valeur au minimum soit 16ms pour notre FDC. Si l’envie vous prend de patcher vos ROM…

Concernant le Step Rate qui commande le déplacement de la tête sur les pistes, il est réglé à 6ms soit deux fois plus que ce qu’est supposé supporter le lecteur (documentation donne 3ms), on peut là aussi jouer avec les valeurs depuis le Basic et profiter de déplacements de tête plus rapides en changeant la valeur #A1 de la rom CPC en #C1. On sera à 4ms avec une marge sur les spécifications du lecteur. Toute valeur inférieure exposant à des effets secondaires désagréables (rater l’envoi d’impulsion et s’emmêler dans les pistes).

FastTrack
ld a,#03 : call sendFDCcommand ; commande de réglage du lecteur
ld a,#C1 : call sendFDCparam   ; step rate à 4ms au lieu de 6ms de l'AMSDOS / head unload à 16ms
ld a,#03 : call sendFDCparam   ; head load time 6ms
ret

Pour analyser les échanges entre le Z80 et le CPC, j’ai utilisé des routines modifiées qui stockent toutes les valeurs du statut (port #FB7E) et aussi du port I/O pendant qu’elle attend que le FDC lui dise qu’il est disponible, le code est rudimentaire :

poolStatus_reloop
inc hl
poolStatus
in a,(c) : ld (hl),a
jp p,poolStatus_reloop
inc hl
ret

La boucle en continu prend 11 nops par valeur. J’ai aussi testé la suite de INI : INC B mais son usage n’est pas aussi pratique et la précision, bien que meilleure (6 nops), d’une utilité faible.

Quelle ne fut pas ma surprise, ou plutôt, quelle fut-elle ! Quand je découvrais une multitude d’états intermédiaires entre la prise en compte d’un ordre et le statut “définitif” qui me donnait l’autorisation de continuer à dialoguer ! Nous allons visualiser quelques unes des commandes, à commencer par celle que nous venons d’utiliser.

Voici donc le déroulement d’un appel à la commande 3 avec traces de statut :

=> #03 statuts lus [#10,#90]
=> #C1 statuts lus [#90]
=> #03 statuts lus [#80]

Il arrive quelques fois qu’on s’adresse au FDC pendant qu’il vaquait à d’autres occupations. Il peut mettre plus longtemps à répondre et se mettre en route

=> #03 statuts lus [#00,#00,#10,#90] ; +22 nops à #00 avant de réagir
=> #C1 statuts lus [#90]
=> #03 statuts lus [#80]

Maintenant que notre lecteur est réglé, il ne fait pas grand chose pour le moment. Il faudrait déjà pouvoir positionner la tête de lecture où bon nous semble. Pour ce faire il y a deux fonctions très proches que sont la calibration et la recherche de piste.

La recherche de piste fonctionne (en interne) en relatif rapport à la position courante tandis que la calibration va lancer des impulsions de déplacement jusqu’à trouver la piste zéro grâce à un capteur matériel sur le lecteur.

Chose amusante, la calibration effectue au maximum 77 déplacements de piste et cette valeur est un héritage lointain des premières disquettes 8″. Si vous voulez faire des routines qui gèrent les lecteurs 3.5″ il faudra donc en lancer deux d’affilée pour s’assurer qu’une calibration depuis les pistes 78 et plus fonctionne !

Une légende tenace raconte que l’armée américaine utilise encore ces disquettes pour valider des tirs nucléaires, soyez sûrs que dès que nous aurons de plus amples informations, vous en serez les premiers informés.

La calibration

La calibration d’un lecteur se fait avec la fonction 7 qui prend comme paramètre unique le numéro de lecteur (0 à 3).

=> #07 statuts lus [#10,#10,#10,#90]
=> #00 statuts lus [#10,#10,#90]

Comme pour la fonction 3 de réglage des lecteurs, il n’y a pas de résultat à récupérer. Le dernier statut #90 signifie qu’il est occupé mais qu’il attend un ordre et surtout, qu’il n’y a PAS de seek en cours (bit 0), alors qu’on sait que oui ! Par contre, il est important de savoir qu’il ne sera pas possible de formater, lire ou écrire tant qu’on n’aura pas récupéré le résultat de la calibration via la commande 8

Interrogation de l’état d’interruption

Comme une calibration ou un déplacement de tête peut prendre un temps indéfini, il faut interroger l’état d’interruption jusqu’à ce que celui-ci nous retourne qu’il a terminé (bit 4 de ET0). Cette fonction doit renvoyer deux paramètres d’après la documentation officielle du FDC. C’est aussi ce que l’on trouve claqué en dur dans de nombreuses routines. Le premier paramètre renvoyé est le registre d’état ET0 et le supposé second, la piste:

; description du registre ET0
; bit 7 et 6
; 00 => OK
; 01 => Calibration/Seek KO ou opération de lecture/écriture terminée (en fait OK!)
; 10 => instruction illégale (j'ai pas fini de bouger la tête!)
; 11 => lecteur perdu (inquiétez-vous)
; bit 5 => instruction terminée
; bit 4 => calibration ratée ou erreur matérielle
; bit 3 => lecteur ou tête indisponible
; bit 2 => tête sélectionnée
; bit 1 et 0 => lecteur actif

Voici ce qu’on peut récupérer comme traces si on demande un état d’interruption juste après un déplacement de la tête et une disquette fraichement insérée.

=> #08 statuts lus [#11,#11,#11,#11,#11,#51,#D1] => statut #D1 signifie que l'on a un résultat à lire
<= #C1 statuts lus [#D1] => ah, un autre résultat à lire
<= #00 statuts lus [#81] => plus de résultat à envoyer mais l'instruction est toujours en cours !

Le premier résultat renvoyé est donc ET0. Ici sa signification officielle est une fin anormale car le signal READY du lecteur aurait changé d’état. Si on demande assez rapidement (avant que la calibration se termine) deux autres interrogations de l’état d’interruption, on verra le premier résultat passer de #C1 à #C2 puis #C3. En réalité, le FDC profite qu’il n’a pas terminé sa calibration pour tester les autres lecteurs. Le deuxième paramètre censé indiquer la piste courante n’est pas mis à jour dynamiquement, il contient la dernière valeur connue avant calibration ou déplacement de tête.

Une fois les 3 autres lecteurs testés, il ne renverra plus ces statuts jusqu’au prochain changement de disquette et passe en mode erreur :

=> #08 statuts lus [#11,#11,#11,#11,#11,#51,#D1] => résultat à lire, seek en cours
<= #80 statuts lus [#81] => invalid command issue et statut dispo, on arrête de lire les résultats !!!

Fun fact => toutes les 2.5ms (temps approximatif), le retour se fait avec des statuts plus longs que la normale. Pas d’incidence particulière, si ce n’est qu’on frise les 150 nops avant d’avoir une réponse !

=> #08 statuts lus [#01,#01,#01,#01,#01,#01,#11,#11,#11,#11,#11,#51,#D1] => résultat à lire, seek en cours
<= #80 statuts lus [#81] => invalid command issue et statut dispo, on arrête de lire les résultats !!!

La fonction ne renvoie pas toujours le même nombre de résultats selon qu’elle se considère en erreur ou non. Cet état de fait est important car la quasi totalité des sources de routines FDC diffusés ne teste pas correctement les statuts et demande à récupérer dans tous les cas des données qui ne sont pas disponibles. Avant de digresser sur ce point, je vous montre ce qui doit terminer la calibration.

=> #08 statuts lus [#11,#11,#11,#11,#11,#51,#D1]
<= #20 statuts lus [#D0] ; bit 5 pour instruction terminée et le bit 0 de seek revient à zéro
<= #05 statuts lus [#80] ; pour la piste 5

Nous venons de voir que la fonction 8 renvoie 1 ou 2 paramètres selon le contexte mais comment font les routines cradoques pour fonctionner correctement et détecter la fin d’un SEEK ou d’un CALIBRATE ?

Ce qu’il faut savoir, c’est qu’une lecture sur le port I/O quand le FDC attend une valeur va déclencher l’écriture de la dernière valeur du port ! Ainsi, après avoir récupéré la valeur #80 du invalid command, la tentative de lecture du port va envoyer #80 au FDC, soit la fonction 0 avec des paramètres lecteur non pris en compte.

La fonction 0 renvoie toujours #80 sur CPC mais si on suit le déroulement, c’est au moment où on est supposé envoyer à nouveau la fonction 8. Je commence à vous perdre, déroulons la séquence et regardons ce qui se passe réellement.

sense_interrupt_cracra
sendFDC 8
; récupérer deux résultats de façon inconditionnelle
getFDC ET0
getFDC track
si ET0<>32 on recommence

=> #08 [#11,#11,#11,#11,#11,#51,#D1]
<= #80 [#81] ; le sense INT n'a qu'un seul paramètre à rendre, on ne devrait pas continuer!
; comme on va quand même lire sur le port, la lecture ENVOIE la valeur précédente du port I/O
; et si on envoie #80, c'est la fonction #00 qui est demandée (numéro de version du FDC)
; la lecture des statuts confirme que le FDC va vouloir nous renvoyer un résultat (#D1)
<=/=> #80 [#11,#11,#11,#11,#11,#11,#11,#11,#11,#11,#51,#D1]

; Le FDC veut nous renvoyer un résultat mais nous recommençons notre cycle
; L'envoi de la valeur #08 n'est PAS pris en compte par le FDC et la valeur interne du port I/O n'est PAS modifiée
; Enfin, après avoir tenté d'envoyer le résultat, le FDC va de nouveau attendre un ordre (#81)
=> #08 [#11,#81] ; le port I/O va rester à la valeur qu'on aurait dû lire

; mais notre routine veut lire deux résultats! La lecture du port I/O va donc envoyer à nouveau la valeur #80
; un petit brun sans lunettes pourrait objecter que c'est la fonction #08 qui repart mais comme les traces vont
; le confirmer, il n'y a bien qu'un résultat de renvoyé ;)
<=/=> #80 [#11,#11,#11,#11,#11,#11,#11,#11,#11,#11,#51,#D1] ; rebelote fonction #00
; on veut lire un deuxième résultat, ouf, on va se resynchroniser avec le FDC
<= #80 [#91] ; petite différence il se considère toujours avec une instruction en cours (alors qu'il nous a rendu la main. On veillera à ne pas utiliser le bit 4 pour savoir si le FDC est occupé ;)

; cet enchainement chaotique se répètera jusqu'à obtention d'un résultat de fin de calibration/changement piste
; enfin, quand il aura terminé de déplacer la tête, nous pourrons lire le résultat final

=> #08 [#01,#01,#01,#01,#01,#11,#11,#11,#11,#11,#D1] ; j'en rajoute pour le plaisir
<= #20 [#50,#D0]
<= #00 [#90] ; le statut peut revenir à #80 quelques instants plus tard, sans incidence

Quand la lecture du port I/O en entrée fait une écriture

Bon, je vous ai un peu jeté au visage que lire le port I/O quand le FDC attend un ordre est en réalité considéré comme l’écriture de la dernière valeur. Je passe à une démonstration un peu plus éloquente, en Basic s’il vous plait. Évitez de taper ces lignes dans un programme Basic, le FDC a besoin de prendre son temps et je vais faire au plus simple.

out &fb7f,15 : rem seek track
out &fb7f,0  : rem drive A
out &fb7f,8  ; rem track 8
rem vous devriez entendre la tête de lecture se déplacer
print inp(&fb7f) : rem 8, précédente valeur écrite => on envoie donc la fonction 8 (sense int)
print inp(&fb7f) : rem 32 => opération terminée
print inp(&fb7f) : rem 8  => piste 8
print inp(&fb7e) : rem #80 => j'ai fini je suis disponible :)

; on peut multiplier les exemples amusants, en demandant d'aller à la piste 7, on ferait une calibration, retour à la piste zéro, mais on pourrait aussi aller en piste 4 pour récupérer l'état des lecteurs, etc.

J’espère que vous n’avez pas trouvé cette entrée en matière trop indigeste car il reste encore pas mal de fonctionnalités du FDC à explorer. La suite au prochain numéro.

One thought on “À la découverte du FDC – Épisode 1

Comments are closed.