À la découverte du FDC – Épisode 2

par roudoudou.

Si vous…

  • avez raté le précédent épisode, vous pouvez le lire ici,
  • avez trouvé qu’il manquait une description du registre d’état ET0 pour l’état d’interruption, vous pouvez le relire car je l’ai mis à jour avec quelques bafouilles supplémentaires,
  • avez trouvé que la routine d’envoi de paramètre était trop grosse, vous avez raison, je l’ai modifiée aussi pour supprimer la sauvegarde du registre AF en attente d’être envoyé,
  • découvrez la série aujourd’hui, bonjour !

Une calibration parfaite

Dans le précédent épisode, je vous mettais en garde contre les pièges de la calibration et la récupération de l’état d’interruption du FDC. Nous avons essentiellement deux choses à respecter :

  • s’assurer de récupérer le bon nombre de résultats,
  • s’assurer que la calibration s’est bien déroulée.

Une routine générique de récupération des résultats

Quitte à écrire une routine qui récupère ce qu’il y a à récupérer, autant faire une routine qui prenne en charge tous les cas possibles de récupération des résultats. Concernant les fonctions FDC, le maximum de résultats renvoyés est sept quand tout se passe bien, parfois sept quand ça se passe mal, parfois moins. Nous sommes supposés tout vérifier.

GetResult
push de,hl
ld d,7       ; max de résultats à récupérer
ld hl,result ; tableau des résultats
.wait_ready in a,(c) : jp p,.wait_ready
and 64 : jr z,.GetDone      ; est-ce un résultat?
inc c : ini : inc b : dec c ; oui, on le stocke
dec d
jr nz,.wait_ready
GetDone
ld a,7          ; nombre de résultats récupérés
sub d           ; est égal à 7 moins restant à faire
ld (nbresult),a ; on enregistre ce nombre
pop hl,de
ret

nbresult defb 0
result   defs 7

Notre routine de calibration peut être écrite :

; A=lecteur a calibrer
CalibrateDrive
push de
ld d,a ; sauvegarde du lecteur dans D
caliretry
ld a,7:call sendFDCcommand ; calibration
ld a,d:call sendFDCparam   ; lecteur
calires
ld a,8:call sendFDCcommand ; sense int state, pas d'autre paramètre!
call GetResult
ld a,(nbresult) : cp 2 : jr nz,calires ; 2 résultats ou rien
ld a,(result) : and #F8                ; on garde uniquement les bits d'état de ET0
                                       ; sinon ça ne fonctionnera 
cp 32 : jr nz,caliretry                ; si problème on recommence
pop de
ret

Dans le cas où on essaierait de calibrer le lecteur depuis les pistes 78 et plus, l’instruction se termine après avoir envoyé 77 pas de déplacement de tête. Le registre ET0 indiquera %01110000 ce qui se traduit par :

  • instruction en erreur (KO)
  • instruction terminée
  • calibration échouée ou erreur électronique

On relance alors la séquence de calibration. Il existe un petit piège à éviter lors d’une calibration. Si l’instruction se termine en erreur (cas des pistes 78+), le deuxième résultat indique quand même une piste zéro. Il ne faut donc pas en tenir compte !

Et si on changeait de piste ?

Puisque l’on sait correctement calibrer un lecteur, on va pouvoir changer de piste en toute sécurité. La fonction 15 du FDC prend en paramètres le lecteur suivi du numéro de piste et déclencher un état d’interruption en retour, elle ressemble fortement à la routine de calibration, à ceci près que si nous avons un problème, on ne relancera pas de suite un déplacement piste mais une calibration, avant de réessayer à se positionner sur une piste.

; D=lecteur
; E=piste
SeekTrack
ld a,15:call sendFDCcommand ; déplacement piste
ld a,d :call sendFDCparam   ; lecteur
ld a,e :call sendFDCparam   ; piste
WaitSeek
ld a,8 :call sendFDCcommand
call GetResult
ld a,(nbresult) : cp 2 : jr nz,WaitSeek ; 2 résultats ou rien, comme la calibration
ld a,(result) : and #F8                 ; on ne conserve que les bits d'état de ET0
cp 32 : jr nz,WaitSeek                  ; est-ce que l'instruction est terminée?
ld a,(result+1) : cp e : ret z          ; on est sur la piste on s'en va
ld a,d : call CalibrateDrive            ; sinon on recalibre
jr SeekTrack                            ; et on recommence

Je ne vous avais pas dit ?

Pour le moment, les fonctions que nous avons utilisées ne requièrent pas d’activer le moteur du lecteur. Et régler le lecteur ou déplacer une tête peut même se faire sans disquette insérée. Voici donc venu le moment tant attendu du démarrage moteur ! Je ne vous cache pas que cette partie n’a rien de passionnante et c’est une des rares choses à avoir été correctement documentée par le passé.

La routine que je vous donne est compatible avec l’Amsdos (et les ROM Parados/Unidos qui s’interfacent en douceur avec l’Amsdos). Comme d’habitude, le problème est souvent le premier appel. Si votre programme est chargé par l’Amsdos, le moteur reste allumé quelques instants après le lancement de votre programme. Vous pouvez soit couper les interruptions et le système une fois pour toutes, soit laisser le système terminer sa tambouille et continuer à profiter de tous les vecteurs systèmes pour la suite.

Note : j’ai vérifié les adresses #BE5F et #BE69 sur des firmwares de 664, 6128 et 6128+.

MotorON
ld a,(#BE5F) ; AMSDOS considère son moteur allumé?
or a : jr z,.next
ld a,1 : ld (#BE69),a ; ticker au minimum et on attend
jr MotorON
.next
ld a,(motor_state) : or a : ret nz ; déjà allumé
push bc
ld bc,#FA7E : ld a,1 : out (c),a   ; on démarre
ld (motor_state),a
ld bc,0                            ; on attend environ 1s
.wait push bc : pop bc : djnz .wait : dec c : jr nz,.wait
pop bc
ret

MotorOFF
push bc
ld bc,#FA7E : xor a : out (c),a
ld (motor_state),a
pop bc
ret

motor_state defb 0

La première chose à faire après avoir allumé le moteur

Avant toute opération sur une disquette, il serait de bon ton de s’assurer :

  • qu’elle est présente
  • que le lecteur est OK
  • éventuellement savoir si elle est protégée ou non

Toutes ces informations sont présentes dans le registre d’état ET3 et il n’y a qu’une seule façon de récupérer le contenu de ce registre : utiliser la fonction dédiée du FDC en lui donnait le numéro du lecteur dont on veut récupérer les informations d’état.

ld a,4:call sendFDCcommand ; récupération d'ET3
ld a,d:call sendFDCparam   ; lecteur
call GetResult
ret

; description du registre ET3
bit 7   | lecteur mort ou pas branché
bit 6   | disquette protégée (ne pas tenir compte de la valeur si le drive n'est pas READY)
bit 5   | lecteur prêt et disquette insérée
bit 4   | tête sur la piste 0
bit 3   | 1=>lecteur simple tête / 0=>lecteur double tête
bit 2   | tête sélectionnée avec la dernière commande
bit 0,1 | lecteur sélectionné

Les petites traces de log montrent que cette fonction prend son temps pour répondre :

=> #04 statuts lus [#10,#10,#10,#90] ; sense drive, parfois précédé de quelques #00
=> #00 statuts lus [#10,#10,#10,#10,#50,#D0] ; drive
<= #30 statuts lus [#80] ; disquette non protégée double face et tête sur piste 0

De l’importance des statuts secondaires

Depuis l’épisode 1, je reviens souvent sur les délais que prennent chaque fonction FDC a répondre, ainsi que les statuts secondaires renvoyés. On pourrait se dire qu’il n’est pas utile d’en tenir compte vu qu’il suffit d’attendre d’avoir le bit 7 du statut principal indiquant une disponibilité.

Sauf que le dialogue avec le FDC se complique dès lors qu’on va avoir une phase d’exécution. Jusqu’à présent, les fonctions présentées peuvent être exécutées de bout en bout avec le système en arrière plan. On peut prendre le temps qu’on veut à envoyer les paramètres, le temps qu’on veut à lire les statuts et le temps qu’on veut pour récupérer les résultats. Mais les fonctions de lecture, écriture et formatage ont besoin d’échanges particulièrement rapides avec le Z80 et l’affaire se complique.

La plupart des logiciels écrits ne passent pas par de jolies fonctions bien encapsulées (encore moins propres) pour gérer l’envoi de données et la raison est toute simple. Le FDC a besoin très rapidement de recevoir ou s’assurer que les données sont reçues. On prendra d’abord soin d’avoir coupé les interruptions (certains ont essayé sans, ils ont eu des problèmes, même si des fois, ça passe, on évitera ces cochonneries).

Enfin, il faut savoir qu’en phase exécution, le FDC nous laisse 26 nops (valeur confirmée par la lecture des spécifications du FDC de chez UMC et une routine de test empirique) pour répondre, temps très court duquel il faudra soustraire la durée totale de la boucle d’attente, le pire cas ne manquant jamais d’arriver. Sur une boucle classique le temps disponible est celui-là :

waitFDC in 0,(c)
        jp p,waitFDC
        defs 17,0 ; 1 nop de plus c'est la catastrophe
        inc c
        out (c),a
        dec c

Normalement, dans ces 17 nops restants, nous sommes supposés tester que le FDC attend bien une donnée, donc une comparaison et un saut conditionnel. Mais Targhan c’est l’meilleur il a 18 nops de dispo au lieu d’une poussive douzaine. Je ne saurais que trop vous conseiller la (re)lecture d’Another World n°48. Si vous ne faites pas partie du peuple élu, nous allons décortiquer ce que pourrait être une routine de lecture à la sauce Targhan.

La lecture dans tous ses états

La commande de lecture d’un secteur est la commande 6. Cette commande, comme toutes les commandes de lecture ou d’écriture se pare d’attributs secondaires dans les bits supérieurs. Ainsi la commande 6 seule fera une lecture en simple densité (inutilisé sauf protections sur CPC).

bit 7 | 1:utilisation de la lecture multi-tête
bit 6 | 1:travail en double densité (toujours 1 sur CPC)
bit 5 | 1:ignorer les secteurs marqués comme effacés

Une lecture classique se fera donc avec la commande #46.

=> #46 statuts lus [#10,#10,#10,#10,#10,#90] ; commande
=> #00 statuts lus (#10,#10,#90] ; lecteur
=> #00 statuts lus [#90] ; piste (pour vérif)
=> #00 statuts lus [#90] ; tête
=> #C1 statuts lus [#90] ; premier ID à lire
=> #02 statuts lus [#90] ; taille du secteur à lire (pour vérif)
=> #C1 statuts lus [#90] ; dernier ID à lire
=> #4F statuts lus [#90] ; nombre d'octets après le CRC pendant lesquels le PLL sera inhibé
                         ; on y reviendra plus tard ;)
                         ; le dernier paramètre ne sert qu'avec les secteurs de taille 0
=> #FF statuts lus [#10,#10,#10,#10,#10... pendant 1800 à 3800 nops
    ...#10,#10,#30,#70,#70,#70... jusqu'à lire les premières données
    ...#70,#70,#70,#F0 => Donnée!

Première observation, le FDC met très longtemps à préparer sa commande de lecture, largement le temps de jouer un peu de musique ! C’est la technique utilisée par le chargement musical de la 5KB demo 3. On lance une commande de lecture, on sait qu’on a minimum 1800 nops avant que le FDC ne commence à lire la disquette, on peut jouer une musique. Ensuite, on lira ce qu’on pourra jusqu’à la prochaine interruption et si l’interruption ne vient pas interrompre la lecture, on aura lu quelque chose.

Deuxième observation, le statut ne passe pas directement de #10 (instruction en cours de préparation) à #70 (j’ai une donnée à t’envoyer, tiens toi prêt). On peut souvent récupérer un statut intermédiaire #30 qui indique une donnée de phase d’exécution. On a donc l’indication donnée avant le sens.

L’idée de Targhan fût donc d’écrire une routine par statut localisée à des adresses dont le poids faible correspond au statut en cours du FDC. En désassemblant la routine d’Imperial Majhong, on peut voir quelque chose comme ceci :

; DE=destination
org #A000
FDCReadData
ld hl,#A010
ld bc,#FB7E
jp (hl)

org #A010 ; statut d'attente, on ne fait rien
in l,(c)
jp (hl)
org #A030 ; idem
in l,(c)
jp (hl)
org #A050 ; statut #50 en fin d'instruction
in l,(c)
jp (hl)
org #A070 ; idem
in l,(c)
jp (hl)
org #A0D0 ; instruction terminée, résultat à renvoyer
jp GetResult
org #A0F0 ; une donnée!
inc c
in a,(c) : ld (de),a : inc de
dec c
in l,(c)
jp (hl)

L’intérêt de dégager du temps pour Targhan étant d’avoir plus de NOP à disposition pour jouer des samples pendant la lecture des données. Sa routine en #A0F0 est évidemment plus complexe, elle termine par un RET avec la pile qui pointe sur une liste de tâches à exécuter. Comme la récupération des donnée se fait à 32 nops, on peut avoir cette précision pour la replay : 30KHz (bon, ptêtre pas ^_^), 15KHz, 10KHz, 7800Hz, …

On devine qu’avec un émulateur dont le FDC serait toujours disponible, ou un périphérique tel que l’UsifacII qui répond un peu trop vite, le sample sera joué trop rapidement. Pire, il pourrait renvoyer des statuts farfelus (j’ai déjà vu ACE faire ça, Philippe, si tu nous lis :p) et tout simplement planter le programme.

Un usage détourné de la commande lecture en erreur

On est d’accord, ces articles n’ont pas vocation à être une documentation exhaustive du FDC, je voulais juste partager mes modestes “découvertes” avec vous. Pour savoir ce que renvoie une commande lecture réussie ou ratée, lisez les documentations techniques qui sont assez explicites à ce sujet et expérimentez de votre côté avec les quelques routines que je vous ai fournies, peut-être que vous aurez des corrections à me faire parvenir ?

Je vous parle un peu d’un des cas d’erreur de la commande de lecture. Les documentations constructeurs indiquent que si la commande de lecture ne trouve pas le secteur demandé, le FDC attend de tomber deux fois sur le trou d’index (le début de la piste) avant de renvoyer l’erreur. Logique évidente, puisqu’on ne sait pas à quelle position de la piste la commande de lecture démarre, il faut faire un vrai tour complet pour s’assurer que le secteur n’existe pas.

C’est la méthode choisie par Discology pour déterminer quel est le premier secteur de la piste. Un petit tour dans le débogueur pour y voir qu’il cherche à lire un secteur d’ID #00 et de taille 1.

Vous devinez la suite, j’ai formaté une piste avec un secteur d’ID #00 et de taille 1 ! Et Discology a dû refaire un tour en cherchant cette fois le secteur #FF de taille 1. La valeur de l’ID est conservé définitivement dans la mémoire du programme, ce qui évite les petits malins qui voudraient ralentir les copies sous Disco. À chaque tentative ratée, le programme décrémente la valeur de l’ID à trouver. Avec un maximum d’une grosse vingtaine de secteurs par piste, il est impossible que Discology trouve les 256 ID de secteurs possibles.

Enfin, quand Discology a trouvé le début de la piste, il peut commencer à scanner tous les secteurs grâce à la fonction Get First ID. Mais je crois que le détail de cette autre histoire sera raconté dans l’épisode 3 !