Triceraprog
La programmation depuis le Crétacé

  • Récréation 3D, Tortue Jeulin ()

    Depuis quelques temps, je planche à mes heures perdues sur ma prochaine vidéo. Et le sujet est le langage de programmation Logo. Après avoir survolé le BASIC, cela me semblait une suite logique.

    En me plongeant dans l'univers Logo, j'ai cherché à mieux connaître le robot qui y est associé : la tortue Jeulin. Et j'ai été très étonné de voir aussi peu de ressources dessus. Et pourtant, il semble qu'elle ne soit pas si rare chez les collectionneurs.

    Grâce à l'aide de photographie envoyées sur le forum system-cfg par Fool-DupleX (merci à lui), j'ai tenté une modélisation. Ça m'a pris... un certain temps.

    Le résultat n'est pas correct au milimètre, mais donne une relativement bonne idée.

    Tortue Jeulin T2 Tortue Jeulin T2


  • VG5000µ, les hooks d'entrées/sorties ()

    Pour ces quatre nouveaux hooks, je ne suis pas très inspirés. Il s'agit de hook destiné au traitement des entrées sorties. Trois d'entre eux sont appelés lors d'une impression de caractères, le quatrième pour de l'acquisition.

    Routines en sorties

    Voici les trois premières :

    • $47DF, prthk : début de commande PRINT.
      • Est appelé en tant que première instruction de l'exécution de l'instruction PRINT.
      • À ce moment là, HL pointe vers les arguments de PRINT et le flag Z est à 1 s'il n'y a rien dans ces arguments.
      • Si vous rendez la main à la routine, elle déroulement l'affichage.
    • $47E2, outhk : début d'impression de caractère -> pour rerouter vers de nouvelles sorties
      • Est appelé pour chaque caractère envoyé sur un périphérique de sortie (en $3bd0)
      • La variable système (prtflg) désigne le périphérique.
      • Le caractère à afficher est dans le registre A.
      • Attention, ce caractère est à comprendre par rapport aux modes d'affichage : est-ce qu'on est en caractères normaux ? semi-graphiques ? redéfinis par l'utilisateur ?
    • $47E5, crdhk : début de retour chariot -> pour rerouter vers de nouvelles sorties
      • Est appelé par chaque demande de retour à la ligne lors de l'émission des caractères sur le périphérique (en $3c57).
      • N'est pas appelé lors d'un retour chariot dans l'éditeur par contre.
      • La variable système (prtflg) désigne le périphérique.

    La variable système (prtflg) désigne le périphérique en sortie selon les valeurs suivantes :

    • 0 - L'écran
    • 1 - L'imprimante
    • 255 - La cassette (en écriture)

    Les appels aux hooks se situent avant le tri vers les trois routines. La sélection ne se fait pas systématiquement dans le même ordre, ce qui signifie que les valeurs 2 à 254 n'envoient pas au même endroit pour les différentes fonctions.

    Le retour chariot enverra sur l'imprimante et la sortie de caractère sur la cassette. PRINT ne considère que deux cas, l'imprimante et l'écran, et s'occupera de la position du chariot ou du curseur suivant le cas. Pour l'affichage en lui-même, c'est la routine d'envoi de caractère et de retour chariot qui sont utilisés.

    Ainsi, si vous voulez supporter un nouveau périphérique en sortie, il faudra probablement effectuer un premier traitement au niveau du PRINT suivant si vous voulez être traité comme un écran ou une imprimante ; puis rendre la main, ou bien tout faire seul et jeter la première adresse de retour de la pile.

    Du côté de la sortie de caractère et du retour chariot, il vous faudra prendre la main et jeter la première adresse de retour de la pile quoi qu'il arrive.

    N'ayant pas de périphérique à tester (on pourrait imaginer une sortie série), je n'ai pas essayé.

    Précautions

    Pendant l'exécution d'une routine de sortie, n'appelez-pas les routines de la ROM qui elles-mêmes font de l'affichage. Vous vous appelleriez vous-même...

    De toute façon, les routines d'affichages ne sont pas ré-entrantes et ne supportent pas d'être exécutées dans deux contextes simultanément. Cela explique pourquoi, si vous avez essayé comment moi d'utiliser des routines d'affichage pendant l'interruption d'affichage, des choses étranges se produisent.

    En effet, si l'interruption à lieu pendant que la ROM est en train d'exécuter une de ces routines (un PRINT tout simplement), il y a de bonnes chances que cela se passe mal. Même si vous sauvez tous les registres. Le contexte est beaucoup plus gros que ça, avec des buffers de constructions de chaînes.

    Mieux vaut avoir vos routines d'affichages spécialisées.

    Routine en entrée

    Pour l'entrée, il n'y a qu'un hook, celui de l'instruction INPUT :

    • $47EB, inphk : début de commande INPUT
      • Appelé en second lors de l'exécution de l'instruction BASIC INPUT, la première instruction étant la vérification que INPUT n'a pas été appelée en direct, hors programme.
      • Comme pour PRINT, HL pointe sur les arguments.
      • La variable système (getflg) désigne le périphérique en entrée. 0 pour le clavier, 255 pour la cassette. Il n'y a pas de périphérique écran en entrée...

    Il est donc possible d'aller router l'INPUT vers un nouveau périphérique externe.

    Le manque d'idée...

    Oui... décidément, comme je n'ai pas de nouveau périphérique à router, je manque d'idée. Le programme que j'écris se contente donc de compter les appels aux différents hooks puis d'offrir une routine pour afficher les compteurs.

    Installation

        defc out_number = $0726
        defc out_str = $36aa
    
        defc xcursor = $4805
    
        defc prthk = $47df
        defc outhk = $47e2
        defc crdhk = $47e5
        defc inphk = $47eb
        defc inthk = $47D0
    
        org $7A00           ; Spécification de l'adresse mémoire d'implentation
    
        push AF             ; Sauvegarde des registres sur la pile
        push HL
    
        ld   hl, call_text  ; Affichage du CALL pour la routine d'Affichage
        call out_str
    
        ld   hl, call_routine
        call out_number
    
        ld A,$C3            ; Mise en place des JP
        ld (prthk), A
        ld (outhk), A
        ld (crdhk), A
        ld (inphk), A
    
        ld HL,prt_routine   ; Mise en place des adresses de saut
        ld (prthk+1),HL
    
        ld HL,out_routine
        ld (outhk+1),HL
    
        ld HL,cr_routine
        ld (crdhk+1),HL
    
        ld HL,inp_routine
        ld (inphk+1),HL
    
        pop HL              ; Restauration des registres depuis la pile
        pop AF
    
        ret                 ; Retour au programme appelant
    
    call_text:
        defm "CALL ", $00
    

    Dans cette première partie, on effectue comme d'habitude l'installation des routines. Comme il y a quatre routines à mettre en place, on me d'un coup les quatre JP , puis dans la foulée les quatre adresses.

    Mais auparavant, on affiche à l'écran un message indiquant l'instruction CALL à lancer pour provoquer l'affichage des compteurs.

    Comme indiqué ci-dessus, appeler les routines d'affichages textes et nombres pendant une interruption ne fonctionne pas sans de nombreuses précautions. J'ai donc préféré offrir une routine qui affiche les compteurs sur un appel explicite. Et plutôt que de placer cet appel à une adresse fixe, elle est à la suite des autres routines. Son adresse étant variable en fonction des modifications des autres routines, je m'aide en affichant l'adresse à appeler.

    Comptage

    Le comptage se résume à quatre fois la même routine, à l'adresse de la variable de comptage près.

    prt_count:
        defw    $0000
    out_count:
        defw    $0000
    cr_count:
        defw    $0000
    inp_count:
        defw    $0000
    
    prt_routine:
        push hl
    
        ld   hl, (prt_count)
        inc  hl
        ld   (prt_count), hl
    
        pop  hl
        ret
    
    out_routine:
        push hl
    
        ld   hl, (out_count)
        inc  hl
        ld   (out_count), hl
    
        pop  hl
        ret
    
    cr_routine:
        push hl
    
        ld   hl, (cr_count)
        inc  hl
        ld   (cr_count), hl
    
        pop  hl
        ret
    
    inp_routine:
        push hl
    
        ld   hl, (inp_count)
        inc  hl
        ld   (inp_count), hl
    
        pop  hl
        ret
    

    Au tout début, je réserve quatre emplacements de 16 bits pour les compteurs. Puis viennent les quatre routines qui chacune va augmenter le compteur associer lors d'un appel.

    Prenons prt_routine. Tout d'abord, HL est sauvegardé. Comme d'habitude au début d'une instruction, ce registre pointe vers la ligne en train d'être exécutée, il est essentiel de préserver sa valeur.

    HL est ensuite chargé avec la valeur de compteur, cette valeur est incrémentée puis replacée dans le compteur.

    On restaure HL et on revient. Vraiment simple.

    L'affichage des compteurs

    La dernière partie est une routine qui sera appelée explicitement par un CALL pour afficher la valeur des compteurs.

    call_routine:
        push hl
        push bc
        push de
        push af
    
        ld   hl, (xcursor)
        push hl
    
        ld   hl, $0020
        ld   (xcursor), hl
        ld   hl, (prt_count)
        call out_number
    
        ld   hl, $0120
        ld   (xcursor), hl
        ld   hl, (out_count)
        call out_number
    
        ld   hl, $0220
        ld   (xcursor), hl
        ld   hl, (cr_count)
        call out_number
    
        ld   hl, $0320
        ld   (xcursor), hl
        ld   hl, (inp_count)
        call out_number
    
        pop  hl
        ld   (xcursor), hl
    
        pop  af
        pop  de
        pop  bc
        pop  hl
    
        ret
    

    Vu qu'on appelle out_number, mieux vaut sauver pour commencer tous les registres (sauf IX et IY, mais ces registres sont particuliers pour la ROM). On les restaure en fin de routine.

    Puis on récupère les coordonnées du curseur dans HL. Attention ici, l'adresse xcursor contient une valeur sur 8 bits. Elle est suivi par ycursor qui contient aussi une valeur sur 8 bits. Grâce au LD HL, (xcursor), c'est donc les coordonnées X et Y qui sont chargées dans HL en une seule instruction.

    Ces coordonnées seront elles-aussi restaurées en fin de routine. Pour remettre le curseur là où il se situait avant le CALL.

    Vient ensuite une répétition de quatre fois la même séquence, aux valeurs près.

    Tout d'abord, HL prend les coordonnées d'affichages du nombre que l'on va écrire. Dans la valeur sur 16 bits, Y arrive en premier, suivi de X. Ainsi $0020 signifie ligne 0, colonne 32.

    Cette position est placée dans la variable système (xcursor). HL prend ensuite la valeur du compteur à afficher, et enfin CALL out_number se charger d'afficher le nombre entier contenu dans HL à la position courante.

    BASIC

    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,E5,21,37,7A,CD,AA,36,21,6D,7A,CD,26,7,3E,C3,32,DF,47,32
    310 DATA E2,47,32,E5,47,32,EB,47,21,45,7A,22,E0,47,21,4F,7A,22,E3,47
    320 DATA 21,59,7A,22,E6,47,21,63,7A,22,EC,47,E1,F1,C9,43,41,4C,4C,20
    330 DATA 0,0,0,0,0,0,0,0,0,E5,2A,3D,7A,23,22,3D,7A,E1,C9,E5
    340 DATA 2A,3F,7A,23,22,3F,7A,E1,C9,E5,2A,41,7A,23,22,41,7A,E1,C9,E5
    350 DATA 2A,43,7A,23,22,43,7A,E1,C9,E5,C5,D5,F5,2A,5,48,E5,21,20,0
    360 DATA 22,5,48,2A,3D,7A,CD,26,7,21,20,1,22,5,48,2A,3F,7A,CD,26
    370 DATA 7,21,20,2,22,5,48,2A,41,7A,CD,26,7,21,20,3,22,5,48,2A
    380 DATA 43,7A,CD,26,7,E1,22,5,48,F1,D1,C1,E1,C9
    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".

    Une fois le programme chargé et les routines installées via le CALL, vous pouvez regarder les effets sur les compteurs avec un programme similaire à celui qui suit (pensez à faire un NEW pour partir à vide) :

    10 CALL xxxxx
    20 PRINT "HELLO"
    30 INPUT A$
    40 INPUT A$
    50 CALL xxxxx
    

    En remplaçant bien entendu les xxxxx par la valeur indiquée lors du CALL &"7A00"

    Le résultat

    Et voici le résultat d'une session comme indiquée ci-dessus.

    Un CALL avec paramètres


  • VG5000µ, les hooks d'appel ()

    Dans la série des hooks sur VG5000µ, voyons en cette fois la paire probablement la plus simple. Ça sera donc rapide.

    Le hook CALL

    Du nom de calhk et d'adresse $47D3, ce hook est utilisé en interne par la commande BASIC CALL.

    Le code de cette instruction est extrêmement simple :

    inst_call:   call     eval_num_ex
                 call     deint_impl
                 ld       a,$c3
                 ld       (calhk),a
                 ld       (calhk+1),de
                 jp       calhk
    

    En premier, l'argument passé à CALL est évalué, puis est ensuite transformé en entier sur 16 bits.

    Cette adresse, précédée par l'opcode pour JP à une adresse absolue 16 bits est placée dans le hook calhk. La routine saute enfin vers cette adresse, qui agit comme un tremplin vers l'adresse indiquée à l'instruction CALL.

    Comme c'est le RET de la routine appelée qui fera office de RET pour l'ensemble de l'instruction CALL, la préservation de l'environnement est à la charge de la routine appelée. Essentiellement, il vous faut préserver HL, qui pointe sur la fin de l'instruction. Si vous ne le faite pas, il y a de bonnes chances que vous obteniez un Erreur de Syntaxe en retour d'instruction.

    Pour un exemple d'utilisation, voyez les articles précédents, qui montent une routine assembleur en mémoire puis l’appellent.

    Récupérer des paramètres

    Que HL pointe juste après le CALL se révèle pratique pour récupérer des arguments potentiels. Dans l'article sur les hooks de périphériques, j'étais allé chercher un argument de type chaîne de caractères. Cette fois, je vais aller chercher trois arguments de type nombre. Le premier entier sur 8 bits, le second sur 16 bits, et le troisième un nombre quelconque (dans les limites du VG5000µ).

    Les deux premiers arguments seront obligatoires, le troisième optionnel.

    Grâce à ça, il est possible d'ajouter des commandes au BASIC, sans leur donner un nom cependant.

        defc out_number = $0726
        defc out_fp = $0731
        defc deint_impl = $2552
        defc eval_num_ex = $284d
        defc type_eq_num = $2850
        defc read_expr = $2861
        defc getbyt_impl = $2aa5
        defc out_str = $36aa
    
        org $7A00           ; Spécification de l'adresse mémoire d’implantation
    
    cmd_routine:
        rst  $08            ; Vérification obligatoire du caractère qui suit
        defb ','
    
        call getbyt_impl    ; Récupération d'un entier 8 bits dans A
        ld   C,A            ; Sauvegarde du premier paramètre dans C
        ld   B,$00          ; BC contient le premier paramètre
    
        rst  $08            ; Vérification obligatoire du caractère qui suit
        defb ','            ; La virgule de séparation
    
        push BC             ; Sauvegarde du premier paramètre dans la pile
    
        call eval_num_ex
        call deint_impl     ; Lecture d'un entier signé 16 bits dans DE
    
        push DE             ; Sauvegarde du second paramètre dans la pile
    
        ld   A,'('
        rst  $18            ; Affichage de la parenthèse ouvrante
    
        ex   (SP), HL       ; Récupération du deuxième paramètre, sauvegarde du pointeur
        call out_number     ; Affiche le contenu de HL
    
        ld   A,','
        rst  $18            ; Affichage de la virgule
    
        pop  HL
        ex   (SP), HL       ; Récupération du premier paramètre, sauvegarde du pointeur
        call out_number     ; Affiche le contenu de HL
    
        pop  HL             ; Récupération du pointeur d'exécution
    
        rst  $10            ; Lecture du caractère suivant
        jr   Z,no_third     ; Fin de la ligne, il n'y a pas de troisième paramètre
    
        dec  HL             ; Sinon, on revient en arrière pour vérifier le caractère suivant
        rst  $08
        defb ','            ; Qui doit être une virgule
    
        ld   A,','
        rst  $18            ; Affichage de la virgule
    
        call read_expr      ; Lecture du troisième paramètre comme une expression
    
        push HL             ; Sauvegarde du pointeur d’exécution
    
        call type_eq_num    ; Vérification du type de paramètre (numérique)
        call out_fp         ; Construction de la chaîne de caractères correspondant au nombre
        call out_str        ; Affichage de la chaîne
    
        pop  HL             ; Restauration du pointeur d’exécution
    
    no_third:
        ld   A,')'
        rst  $18            ; Affichage de la parenthèse fermante
    
        ret
    

    C'est assez long, il y a peut-être plus optimisé, le principal ici est que la suite d'instructions soit lisible et que les morceaux différents allant chercher les arguments puissent être pris indépendamment pour réutilisation.

    Le code est commenté, mais nécessite peut-être quelques autres éclaircissement :

    • rst $08 vérifie que le caractère codé juste après le RST est pointé par HL. Si ce n'est pas le cas, une erreur de syntaxe est levée. Le DEFB qui suit le RST est bien entendu sauté, le retour de la routine se fait juste après. La ROM se sert beaucoup de cette séquence pour vérifier la syntaxe de commandes et d'expressions.
    • rst $18 affiche le caractère présent dans A.
    • read_expr évalue une expression de n'importe quel type et met le résultat dans l'accumulateur flottant.
    • eval_num_ex lire une expression (via read_expr) et vérifie dans la foulée si elle est numérique.
    • getbyt_impl appelle eval_num_ex, la converti en entier, et vérifie que le résultat tient sur 8 bits. Le résultat est dans A.
    • deint_impl effectue une troncature entière sur 16 bits du nombre présent dans l'accumulateur flottant. Le résultat est dans DE.
    • out_number affiche le nombre (entier positif) présent dans HL.
    • out_fp écrit dans un buffer temporaire une chaîne représentant le nombre présent dans l'accumulateur flottant. Le pointeur vers la chaîne est renvoyé dans HL et la chaîne se termine par 0.
    • out_str affiche la chaîne pointée par HL se terminant par 0.

    On notera au passage que lors de l'affichage, les deux premiers paramètres sont inversés... c'était juste plus simple à écrire.

    Le hook RST

    Le second hook d'appel est rsthk, à l'adresse $47DC. Son fonctionnement n'est pas atteignable depuis le BASIC.

    La fonction RST du Z80 est une sorte de CALL sur 1 octet. Il permet de brancher à une série de 8 adresses prédéfinies : $0000, $0008, $0010, $0018, $0020, $0028, $0030, $0038. Suivant la syntaxe de l'assembleur, soit l'adresse, soit le numéro du restart (entre 0 et 7) est accepté.

    Toutes ces adresses sont allouées à des fonctions souvent appelées, cela permet de gagner de la place. Comme il n'y a pas beaucoup d'instructions possibles entre deux adresses de restart, la routine est souvent placée ailleurs, et certains emplacements sont remplis avec des données.

    Par exemple, entre RST $00, qui fait un démarrage complet de la machine et RST $08, qui fait un test de syntaxe, se situe la chaîne ".1.1" pour la ROM 1.1. C'est cette chaîne qui est affichée au démarrage de la machine.

    Les différents RST

    Puisqu'on y est, voici les différents RST sur VG5000µ, brièvement.

    • $00 : redémarrage complet de la machine
    • $08 : vérification de la présence d'un caractère pointé par HL, ou lancement d'une erreur de syntaxe (pour vérifier une virgule entre deux arguments par exemple)
    • $10 : acquisition d'un caractère depuis la chaîne pointée par HL (avec quelques flags concernant sa nature, et en sautant les espaces)
    • $18 : envoie d'un caractère sur le périphérique sélectionné (écran, imprimante, modem)
    • $20 : comparaison de HL et DE, qui a lieu très souvent.
    • $28 : renvoie -!, 0 ou 1 en fonction du signe de l'accumulateur flottant
    • $30 : libre
    • $38 : vecteur d'interruption (utilisé par l'affichage)

    Un RST libre

    Le restart $30 est donc libre, et son code est le suivant :

    usrrst:      jp       rsthk
    

    Net et précis. Il suffit donc d'ajouter en rsthk un JP suivi d'une adresse absolue, et vous pouvez alors utiliser RST $30 dans vos routines pour un appel très fréquent.

    Cependant, il y a une limite : il n'existe qu'un seul hook libre pour tout le monde. À garder en tête si vous mélanger des routines qui veulent chacune utiliser cette instruction.

    À noter aussi que si RST $30 ne prend qu'un octet dans votre routine, le saut utilise deux indirections pour un total de 31 T States contre 17 pour un CALL.

    Le résultat

    Voici le résultat de l'appel de la routine via CALL avec décodage de paramètre.

    Un CALL avec paramètres


  • VG5000µ, les hooks de périphériques ()

    L'article précédent présentait un accrochage sur un hook d'interruption. Dans cet article, je vais regarder du côté des hooks de commandes de périphériques.

    Ces hooks sont initialisés différements des 10 premiers, selon le code qui suit :

                 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
    

    L'instruction placée au début de chaque vecteur de redirection est un JP et l'adresse par défaut est celle d'une routine indiquant que le périphérique n'est pas géré.

    Il s'agit d'une extension de commandes mise à disposition par le VG5000µ, permettant de traiter les commandes LPEN, MODEM et DISK. Il est d'ailleurs amusant de voir que dans le manuel d'utilisation, ces trois commandes sont mentionnées dans le rappel des instructions reconnues, mais qu'elles ne sont pas décrites.

    Dans la table des instructions, le décodage de ces tokens envoie directement sur chacun des trois vecteurs, sans traitement particulier. C'est donc du code de décodage de paramètres qu'il faut écrire si l'on veut traiter ces instructions.

    Mise en place de la routine

    Le code est similaire à celui de la mise en place de la routine sur l'interruption. Seule l'adresse du hook change.

        defc dskhk = $47f4  ; 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 (dskhk),A        ; Il y a normalement déjà un 'JP' ici, mais on s'en assure
        ld HL,cmd_routine
        ld (dskhk+1),HL     ; l'adresse de la routine
    
    
        pop HL              ; Restauration des registres depuis la pile
        pop AF
    
        ret                 ; Retour au programme appelant
    
    cmd_routine:
        ret
    

    Passons donc rapidement sur la partie la plus intéressante : le traitement de l'instruction.

    Traitement de l'instruction

    Lorsque l'interpréteur appelle la routine associée à une instruction, plusieurs choses sont mises en place. Les principales sont :

    • HL pointe vers la chaîne en train d'être interprétée, sur le prochain caractère à lire,
    • Le flag "Z" est à 1 si l'on est à la fin de la chaîne,
    • L'adresse de retour sur la pile est positionnée pour traiter l'instruction suivante.

    Le contrat en sortie est :

    • HL doit pointer sur le caractère après le dernier consommé par l'instruction,
    • S'il s'agit d'une fonction, son résultat doit être dans l'accumulateur flottant (numérique, ou pointeur de chaîne).

    Pour s'amuser avec le paramètre, je vais écrire le décodage d'un paramètre de type chaîne, qui sera ensuite affiché à l'écran, suivi par un message.

        defc type_eq_str = $2851
        defc read_expr = $2861
        defc out_str = $36aa
        defc out_str1 = $36ad
    
    cmd_routine:
        push HL             ; Sauvegarde du pointeur d'exécution
    
        call read_expr      ; Lecture de l'expression suivant la commande
        ex (sp), hl         ; Remplacement de la valeur du pointeur d'exécution
    
        call type_eq_str    ; Vérification du type du paramètre
        call out_str1       ; Affichage de la chaîne
    
        ld hl, answer
        call out_str        ; Affichage du message de la commande
    
        pop HL              ; Récupération du pointeur d'exécution
    
        ret
    
    answer:
        defm " <-- PARAM", $00
    

    La structure est assez simple, car les routines nécessaires sont toutes disponibles dans la ROM.

    Tout d'abord, on sauve le pointeur vers la chaîne interprétée. HL est actuellement sur le début du paramètre de DISK (en tout cas, ce qui suit).

    L'appel à read_expr se charge de lire une expression valide (ou échouer avec une erreur). À la sortie de la routine, HL est positionné à la fin de ce qui a été consommé par l'expression.

    Ce pointeur est celui qu'il faudra conserver pour l’interpréteur. Du coup, on échange la valeur précédente de HL actuellement sur la pile avec la nouvelle valeur. Hop, le tour est joué.

    L'appel à type_eq_str vérifie si le type de l'expression est bien une chaîne, et affiche un message d'erreur sinon (et dans ce cas l'interprétation est arrêtée immédiatement).

    Si c'est bien une chaîne, l'appel à out_str1 affiche le résultat de l'expression de type chaîne de caractères qui vient d'être évaluée.

    L'appel à out_str qui suit affiche la chaîne terminée par un 0 définie dans le programme.

    Et voilà, voir le résultat à la fin de l'article.

    BASIC

    Voici un programme BASIC pour charger et lancer la routine.

    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,E5,3E,C3,32,F4,47,21,10,7A,22,F5,47,E1,F1,C9,E5,CD,61,28
    310 DATA E3,CD,51,28,CD,AD,36,21,23,7A,CD,AA,36,E1,C9,20,3C,2D,2D,20
    320 DATA 50,41,52,41,4D,0
    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".

    Le résultat

    La commande DISK


  • 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 ligne
    • $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.


Page 1 / 13 (suivant) »