Triceraprog
La programmation depuis le Crétacé

VG5000µ, les hooks ()

Pour cet article, nous allons laisser de côté la partie BASIC-80 pour regarder du côté d'un fonctionnement spécifique au VG5000µ. Pas que le principe soit original, il est présent dans de nombreuses machines, mais que les capacités sont diverses suivant les différentes machines.

Les « hooks » (la traduction de « crochet » me semble un peu hasardeuse, une « accroche » me semble un peu meilleur) est un moyen qu'offre le système pour intervenir lors de certaines opérations en augmentant le fonctionnement de la ROM. En y mettant son grain de sel en quelque sorte.

Plus prosaïquement, les « hooks » sont des appels à des adresses précises, en RAM, à des routines qui peuvent être modifiées. Il peut s'agir aussi sur d'autres machines de récupérer dans une variable système une adresse d'indirection. Sur le VG5000µ, toutes les adresses de « hooks » sont fixes.

Les « hooks » sont parfois aussi appelés vecteurs d'indirection. Ou bien tout simplement vecteurs.

L'initialisation des « hooks »

L'initialisation des « hooks » arrive très tôt dans l'initialisation de la machine, juste après la détection de la mémoire disponible.

             ld       a,$c9
             ld       hl,inthk
             ld       b,$1e

hk_ini_lop:  ld       (hl),a
             inc      hl
             djnz     hk_ini_lop

             ld       a,$c3
             ld       (inst_lpen),a
             ld       (inst_disk),a
             ld       (inst_modem),a
             ld       hl,no_device
             ld       ($47f2),hl
             ld       ($47f5),hl
             ld       ($47f8),hl
             ld       hl,resetlang
             ld       ($47ef),hl
             ld       (nmihk),a

A prend la valeur C9, qui est le code pour l'instruction RET sur Z80. HL est initialisé à inthk, qui est le premier hook d'une plage consécutive. Et B prend $1e, la taille de cette plage.

La première étape et de remplir cette plage avec des RET. Cette plage contient les 10 premiers hooks, que sont les suivants.

  • $47D0, inthk : interruption masquable
  • $47D3, calhk : vecteur CALL
  • $47D6, sonhk : vecteur de générateur de son
  • $47D9, plyhk : début de commande PLAY
  • $47DC, rsthk : vecteur pour l'instruction RST utilisateur
  • $47DF, prthk : début de commande PRINT
  • $47E2, outhk : début d'impression de caractère
  • $47E5, crdhk : début de retour chariot
  • $47E8, inlhk : début de lecture d'une line
  • $47EB, inphk : début de commande INPUT

Chaque hook fait 3 octets de long, nous verrons pourquoi plus loin. Et pour le moment tous remplis de RET. Un CALL à ces adresses ne fait donc rien d'autre que de revenir à l'appelant immédiatement.

A prend ensuite la valeur C3, qui, suivi d'une adresse sur deux octets, est un JP absolu à cette adresse. Les hooks lpnhk, dskhk et modhk sont remplis avec ce jp no_device, qui est un branchement vers l'erreur indiquant que le périphérique n'est pas géré.

Plus étonnant est la valeur que prend le hook nmihk, qui est appelé en cas d'interruption non masquable. Cette interruption est appelée lors de l'appui sur la touche Delta du VG5000µ. La routine resetlang met le système en anglais au niveau des messages et du clavier puis ressort de l'interruption. Et c'est tout.

À vrai dire, cela ne dure qu'un instant. Juste après, le VG5000µ initialise sa partie graphique puis remplace le hook par une nouvelle valeur :

             ld       hl,test_reset
             ld       ($47ef),hl

Cette nouvelle routine test_reset vérifie si la touche Ctrl est appuyée. Si ce n'est pas le cas, la routine sort immédiatement. Sinon, un reset à chaud a lieu.

Mise en place d'une routine

« Accrocher » une routine est assez facile, et demande juste quelques précautions. Afin de prendre un premier exemple, je vais accrocher une routine sur l'interruption qui provoque l'affichage sur le VG5000µ.

Rapidement, dans le VG5000µ, le processeur graphique est la cause d'une interruption INT à chaque rafraîchissement. C'est la seule raison, de base, qui provoque cette interruption.

Lors de l'interruption, le PC est branché en $0038 et la première instruction y est call inthk. On a donc une possibilité d'agir lors de l'interruption, avant que le rafraîchissement potentiel de l'écran n'ait lieu (il n'a pas lieu à chaque fois).

Le code nécessaire pour une routine de « hook » est en deux parties. La première se charge de modifier le branchement du « hook » vers notre routine. La seconde est la routine elle-même.

Commençons par la première partie. À noter que pour être propre, il faudrait effectuer un chaînage en faisant appeler à notre propre routine une routine éventuellement déjà installée. Je ne m'en occuperai pas ici.

    defc inthk = $47D0  ; Adresse du hook

    org $7A00           ; Spécification de l'adresse mémoire d'implémentation

    push AF             ; Sauvegarde des registres sur la pile
    push HL

    ld A,$C3            ; Mise en place de la routine sur le HOOK
    ld (inthk),A        ; le 'JP'
    ld HL,int_routine
    ld (inthk+1),HL     ; et l'adresse


    pop HL              ; Restauration des registres depuis la pile
    pop AF

    ret                 ; Retour au programme appelant

int_routine:
    ret

Le programme est directement commenté. Si depuis le BASIC, ce programme est injecté et appelé, alors... il ne se passera pas grand chose de visible, mais en réalité, le RET de int_routine sera appelé à chaque interruption.

Une routine plus intéressante

Pour rendre les choses plus intéressantes, voici une routine à installer qui affiche à l'écran une petite barre qui tourne.

    defc screen = $4000

int_routine:
    push AF             ; Sauvegarde de AF

    ld  A,(IX+$00)
    dec A
    jp nz, no_display   ; Respect du timer de rafraîchissement

    push HL             ; Sauvegarde de HL

    ld A,(count)        ; Compteur du caractère à afficher
    inc A
    cp A, $4            ; S'il est à la dernière position, on boucle
    jp nz, display
    ld A, $0

display:
    ld (count),A        ; Mise à jour du compteur

    ld HL,cursor        ; Récupération du caractère à afficher
    add A,L
    ld L,A
    ld A,(HL)

    ld HL,screen + 32   ; Affichage
    ld (HL), A

    ld (ix+$01),$01     ; Force le ré-affichage

    pop HL              ; Restauration des registres depuis la pile
no_display:
    pop AF

    ret

count:
    defb 0
cursor:
    defb $2F, $60, $5C, $7C     ; Les 4 caractères qui forment l'animation

Il faut veiller dans cette routine à bien préserver les registres utilisés, nous sommes ici en pleine interruption, nous n'avons aucune connaissance du contexte.

La lecture de la variable système à travers le registre IX permet de savoir si le système va considérer un rafraîchissement de l'affichage. La commande DISPLAY du BASIC influe directement sur une valeur de compteur qui, lorsqu'il arrive à zéro, provoque éventuellement un affichage avant de revenir à sa valeur spécifiée.

L'affichage n'est cependant pas systématique. Le bit 0 de la variable système qui suit le compteur doit être à 1 pour que l'affichage est vraiment lieu. Et l'on trouve parsemé dans la ROM des ld (ix+$01),$01 qui signifient qu'un rafraîchissement de l'écran est demandé. Ce que je fais à la fin de la routine.

La partie après PUSH HL est un bête compteur cyclique de 0 à 3, qui est ensuite utilisé pour pointer dans un tableau de 4 caractères provoquant l'animation.

L'adresse écran est calculé en dur et on y place directement le caractère. Puis le contexte est restauré.

Une dernière note pour comprendre l'affichage du VG5000µ dans sa ROM BASIC. La ROM maintient en $4000 une image logique du contenu de l'écran, et un ensemble de variables systèmes, pointées en tout temps par IX. Lorsqu'un rafraîchissement à lieu, tout ce contenu est envoyé vers le processeur graphique pour une grande (et lente !) mise à jour.

Il se peut donc que des modifications aient lieu en mémoire graphique côté processeur qui ne soient pas répercutées tout de suite vers le processeur graphique.

Mais tout ceci est une autre histoire qui n'est pas le sujet ici.

Le test

Voici un programme BASIC qui va monter la routine en mémoire. Suivi d'un RUN pour l'exécuter puis du CALL pour lancer le hook.

10 CLEAR 50,&"79FF"
20 S=&"7A00"
30 READ A$
40 IF A$="FIN" THEN END
50 A$="&"+CHR$(34)+A$+CHR$(34):A=VAL(A$)
60 POKE S,A
70 S=S+1
80 GOTO 30
300 DATA F5,C5,D5,E5,3E,C3,32,D0,47,21,14,7A,22,D1,47,E1,D1,C1,F1,C9
310 DATA F5,DD,7E,0,3D,C2,3A,7A,E5,3A,3C,7A,3C,FE,4,C2,28,7A,3E,0
320 DATA 32,3C,7A,21,3D,7A,85,6F,7E,21,20,40,77,DD,CB,1,C6,E1,F1,C9
330 DATA 0,2F,60,5C,7C
1000 DATA FIN
RUN
CALL &"7A00"

Pour éviter d'avoir à tout rentrer au clavier, voici le fichier .k7. À charger avec CLOAD, suivi d'un RUN et du CALL &"7A00".

La suite

Nous avons vu un exemple de mise en place de routine sur un « hook » du VG5000µ. Dans les articles suivant, j'irai examiner les autres « hook » et dans quels contextes ils sont appelés.