Triceraprog
La programmation depuis le Crétacé

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


  • VG5000µ, les chaînes de caractères ()

    Dans l'article précédent, on avait vu la création d'une variable dans la zone principale de la mémoire. Cette variable a par défaut un contenu nul, et ne s'occupe pas de savoir si ce contenu est un nombre ou une chaîne de caractères. Les quatre octets de contenus qui suivent les deux octets du nom sont donc tous les quatre à $00.

    Pour qu'une valeur soit associée à une variable, il faut une instruction d'assignation, directement via LET (éventuellement de manière implicite), plus indirectement avec une instruction FOR, ou encore plus indirectement par un couple READ/DATA.

    Dans tous les cas, la valeur à assigner à la variable est le résultat de l'évaluation d'une expression, c'est-à-dire le résultat d'un calcul numérique ou d'une opération à partir de chaînes.

    Afin de comprendre comment sont créées et stockées les chaînes de caractères, c'est donc du côté de l'évaluation d'expression qu'il faut commencer.

    Évaluation d'expression

    L'évaluation d'une expression commence en $2861 et nous n'allons pas nous y attarder. Nous suivons la piste immédiatement vers la routine de lecture d'une valeur depuis le buffer d'entrée. Cette routine se situe en $28d8 et commence comme suit :

    parse_value: xor      a,a
                 ld       (valtyp),a
    
                 rst      chget
    
                 jp       z,missing_op
                 jp       c,str_to_num
    
                 cp       a,'&'
                 jp       z,str_hex_dec
    
                 call     a_to_z_2
                 jr       nc,str_to_var
    
                 cp       a,'+'
                 jr       z,parse_value
    
                 cp       a,'.'
                 jp       z,str_to_num
    
                 cp       a,'-'
                 jr       z,str_to_min
    
                 cp       a,'"'
                 jp       z,str_to_str
    
                 cp       a,$b7 ; 'NOT'
                 jp       z,str_to_not
    
                 cp       a,$b4 ; 'FN'
                 jp       z,str_to_fn
    
                 sub      a,$c3 ; 'SGN'
                 jr       nc,str_to_func
    

    Voici toute une série de tests pour déterminer ce que contient l'opérande pointée actuellement par HL.

    On remarque au tout début que la valeur par défaut de l'expression en cours est mis à 0 (c'est-à-dire : valeur numérique).

    Puis le premier caractère est lu et la suite de tests ressemble à ceci :

    • Est-ce qu'on est à la fin de la ligne ? Alors il manque quelque chose...
    • Est-ce que c'est un chiffre ? Alors on commence à convertir l'entrée en nombre
    • Est-ce que ça commence par & ? Alors on commence à décoder un nombre hexa
    • Est-ce que c'est une lettre ? Alors on va lire une variable
    • Est-ce que c'est un '+' ? On l'ignore et on boucle un caractère plus loin
    • Est-ce que c'est un '.' ? Alors on commence à convertir l'entrée en nombre
    • Est-ce que c'est un '-' ? Alors on démarre une sous-expression qui sera inversée
    • Est-ce que c'est un '"' ? Alors on décode une chaîne !
    • Etc... (les trois derniers cas sont pour NOT, une fonction utilisateur, ou une fonction prédéfinie, puis on continue avec le traitement des parenthèses)

    D'après cette liste, on part donc vers str_to_str.

    Les chaînes à la chaîne

    Arrivée dans str_to_str, on a HLqui pointe vers une chaîne qui commence avec des guillemets. La première étape va être de chercher la fin de la chaîne et de compter le nombre de caractères.

    str_to_str:  ld       b,'"'
    
                 ld       d,b
    direct_str:  push     hl
    
                 ld       c,$ff
    loop_str:    inc      hl
                 ld       a,(hl)
                 inc      c
    
                 or       a,a
                 jr       z,create_str
                 cp       a,d
                 jr       z,create_str
                 cp       a,b
                 jr       nz,loop_str
    
    create_str:  cp       a,'"'
                 call     z,skipch
    
                 ex       (sp),hl
                 inc      hl
                 ex       de,hl
    
                 ld       a,c
    

    Cette routine commence par placer le caractère guillemets dans les registres B et D. Les caractères présents dans B et D sont des terminateurs potentiels. Cette routine est en effet appelée par un autre chemin directement en direct_str avec d'autres terminateurs possibles.

    Note : ces autres terminateurs possibles sont : et ',' dans le cas où la chaîne est lue par une instruction READ depuis une séquence de DATA.

    La suite du préambule de la routine se fait en poussant sur la pile le pointeur sur la ligne en exécution et en initialisant C avec -1. C est le compteur de caractères. Au passage, on peut en déduire que les chaînes de caractères auront donc comme longueur maximale 255.

    Puis débute la boucle loop_str, qui commence par avancer le pointeur HL sur le caractère suivant, récupère la valeur de ce caractère dans A et incrémente le nombre de caractères.

    Le premier test vérifie si A est nul. Si c'est le cas, on a atteint la fin de la chaîne et il est temps de la créer. De même que si le caractère est égal à l'un des deux terminateurs. Dans le cas contraire, la boucle est bouclée et le caractère suivant traité.

    Note : mais et s'il y a plus de 255 caractères avant de trouver un terminateur ? Ça ne se passe pas très bien... Il n'y a pas de tests et vous pouvez vérifier (c'est un peu long) qu'il peut se passer des choses étranges.

    Avant de créer la chaîne, il faut mettre les choses en place. Si le dernier caractère sont des guillemets, une routine va les consommer et ignorer tout ce qui est inintéressant, pour recaler HL sur la prochaine valeur ou instruction.

    Ce pointeur est échangé avec le haut de la pile, qui contenait le début de la chaîne. Ce début de chaîne est avancé de 1 pour ignorer les premier guillemets (le chemin READ s'arrange pour mettre HL au bon endroit en sachant qu'il sera incrémenté ici).

    Puis le pointeur de début de chaîne est transféré dans DE et le nombre de caractères lus dans A.

    Création de la chaîne temporaire

    À présent que l'on sait où est la chaîne (pointée par DE) et combien de caractères elle contient, l'étape suivant consiste à l'extraire dans un endroit où l'évaluation de l'expression ou l'assignation pourra la trouver.

                 call     crt_tmp_str
    cpy_to_pool: ld       de,dsctmp
    
                 ld       hl,(temppt)
                 ld       (faclo),hl
    
                 ld       a,$01
                 ld       (valtyp),a
    
                 call     cpy_detohl_4
                 rst      de_compare
    
                 ld       (temppt),hl
                 pop      hl
                 ld       a,(hl)
                 ret      nz
    
                 ld       de,$001e
                 jp       error_out
    

    Tout commence par un appel à crt_str_dsc qui créé un descripteur temporaire de chaîne à l'adresse dsctmp ($499b). Dans ce buffer qui sert aux opérations sur les chaînes, la routine placera en premier octet la taille de la chaîne, puis rien de spécial, puis la valeur de 'DE' sur les deux derniers octets.

    Les descripteurs de chaînes font donc 4 octets, dont le deuxième est inutilisé.

    Puis, DE prend la valeur de dsctmp, le buffer temporaire qui vient d'être initialisé, et HL la valeur contenue dans la variable système temppt. Ce pointeur est initialisé par le BASIC vers le buffer tempst, qui est un buffer de 120 octets réservé.

    Ce pointeur est placé dans l'accumulateur flottant, qui maintient en fait toute valeur courante d'une expression, qu'elle soit numérique (lorsque (valtyp) vaut 0) ou chaîne (lorsque (valtyp) vaut 1).

    Et d'ailleurs, (valtyp) passe à 1 pour indiquer la nature du contenu de l'accumulateur flottant.

    L'appel suivant est une routine qui copie 4 octets pointés par DE vers ce qui est pointé par HL. Autrement dit, le descripteur de chaîne qui vient d'être créé est copié vers le buffer temporaire pointé par HL.

    Comme dsctmp est placé astucieusement après le buffer tempst, si jamais, après copie, HL est égal DE, alors c'est qu'on a atteint la fin de l'espace de travail, une erreur est latente, traitée un peu plus loin. Comme 120 (la taille du buffer) est divisible par 4 (la taille des descripteurs) on est assuré de tomber juste, et que HL ne dépasse jamais DE.

    En attendant de traiter l'erreur, il s'agit de mettre les choses en ordre. La variable système (temppt) est mise à jour avec la nouvelle valeur de HL, puis on récupère le pointeur sur la ligne depuis la pile, et le caractère pointé par HL est placé dans A, tout est prêt pour continuer le décodage.

    Enfin, on sort de la routine si la dernière comparaison n'était pas nulle (il reste de la place dans le buffer temporaire) ou bien on saute vers une erreur indiquant à l'utilisateur que l'opération sur les chaînes de caractères était trop complexe.

    Note : il existe 30 emplacements de descripteurs de chaîne dans le buffer temporaire avant que la routine ne laisse tomber avec un message d'erreur. En sachant qu'une expression comme PRINT "ABC" + "CDE" + "DEF" en consomme 2, ça laisse de la marge...

    Association de la variable

    Pour l'association de la variable avec sa valeur, voyons le cas de l'instruction LET (qui est de toute façon appelée par FOR et READ).

    Passons rapidement sur le début de l'instruction LET qui récupère l'adresse de la variable à gauche du signe égal selon la méthode décrite dans l'article précédent, appel l'évaluation de ce qui est à droite du signe égal, et vérifie que les types sont cohérents des deux côtés (soit numérique, soit chaîne de caractères).

    Une fois tout ceci en place, l'association de la chaîne elle-même a lieu :

    let_string:  push     hl
                 ld       hl,(faclo)
                 push     hl
                 inc      hl
                 inc      hl
                 ld       e,(hl)
                 inc      hl
                 ld       d,(hl)
    
                 ld       hl,(txttab)
                 rst      de_compare
                 jr       nc,crtstrentry
    
                 ld       hl,(strend)
                 rst      de_compare
                 pop      de
                 jr       nc,pop_string
    
                 ld       hl,dsctmp
                 rst      de_compare
                 jr       nc,pop_string
    
                 defb     $3e
    crtstrentry: pop      de
                 call     bc_from_tmp
                 ex       de,hl
                 call     save_str
    pop_string:  call     bc_from_tmp
                 pop      hl
                 call     cpy_detohl_4
                 pop      hl
                 ret
    

    En début de routine, HL pointe vers la variable à gauche du signe =, on sauve cette adresse sur la pile pour plus tard.

    Puis on récupère dans HL la valeur de la dernière expression évaluée, qui est dans l'accumulateur flottant. On pousse aussi cette valeur sur la pile et on va chercher deux octets plus loin le pointeur vers la chaîne de caractère elle-même, qui est placée dans DE.

    À présent, il s'agit de savoir où sont situés ces octets de chaînes. Le premier cas est une comparaison avec (txttab). Si les caractères sont avant, c'est qu'ils sont dans les variables systèmes, et donc dans un endroit volatile, il va donc falloir les copier ailleurs et c'est ce que va faire le saut en crtstrentry.

    Note : si vous vous amusez avec les pointeurs de zones mémoire du BASIC pour déplacer le contenu du code, gardez en tête que pour le BASIC, une chaîne située avant le code est volatile.

    Le second test vérifie si la chaîne se situe avant (strend). Si c'est le cas, c'est que la chaîne se trouve dans le programme.

    Note : en toute rigueur, la comparaison aurait du être faite avec (vartab), car il n'y a pas de contenu de chaînes entre (vartab) et (strend). En regardant d'autres dérivés du BASIC-80, je pense qu'il s'agit d'une adaptation un peu hâtive, car d'autres BASIC-80 semblent placer leurs chaînes différemment. Même si le pointeur de comparaison n'est pas exactement le bon, le test fonctionne néanmoins, et ce n'est pas plus lent.

    Si la chaîne se trouve dans le programme, on va pouvoir conserver ce pointeur sans dupliquer les octets ailleurs. En effet, un programme n'est pas volatile et le moindre changement dans le listing efface toutes les variables. On est donc assuré que les chaînes de caractères présentes dans le programme lorsque celui-ci tourne restent en place.

    Note : cela donne quelques contraintes si vous vous amusez à modifier le listing en cours de route depuis le programme qui tourne...

    Le troisième test, enfin, vérifie si le contenu de la chaîne ne serait pas par hasard dans un autre buffer temporaire, celui où l'on met le descripteur temporaire (et qui se situe juste avant dsctmp)

    Note : ce troisième test est étrange. Ce buffer est situé dans les variables système et donc est déjà avant le code BASIC. Je ne vois donc pas comment on peut arriver ici. Je pense que c'est un reliquat d’adaptation du BASIC-80 où le buffer temporaire se situe après le code BASIC.

    Le defb $3e est un instruction morte permettant d'éviter l'exécution du POP DE qui suit. En effet, ce POP DE pour récupérer le pointeur sur le descripteur de chaîne a déjà été fait lorsqu'on arrive par là.

    crtstrentry replace le pointeur HL sur les informations de la chaîne temporaire la plus récente (la plus en haut du buffer temporaire), puis cette adresse et échangée avec celle tenue dans DE qui est aussi le pointeur vers ce même descripteur.

    Note : ici, je ne sais pas dans quel cas HL et DE peuvent être différent. L'idée est d'enlever le descripteur de chaîne du buffer temporaire en ajustant le pointeur sur ce buffer (temppt), le buffer temporaire étant manipulé comme une pile, la routine bc_from_tmp est en quelque sorte le pop de cette pile, dont la valeur part dans BC, mais avec une sécurité. Si le pointeur HL n'est pas celui qui était attendu DE alors le pop n'a pas lieu, c'est juste une récupération de la valeur en haut de la pile.

    Avec les informations récupérées, un appel à save_str est effectué, et nous verrons ça juste après.

    Dans tous les cas, la description de chaîne la plus récente du buffer temporaire est à nouveau récupérée, l'adresse de la variable popée de la pile dans HL et le descripteur temporaire copié vers la valeur de cette variable.

    Après une remise en ordre de la pile, on rend la main, la variable est maintenant associée à la valeur de la chaîne.

    Et la création ?

    Dans le cas où la chaîne de caractères doit être sauvée quelque part, alors un appel à save_str est fait.

    save_str se situe en $3646 et est comme suit :

    save_str:    ld       a,(hl)
                 inc      hl
                 inc      hl
                 push     hl
    
                 call     alloc_str_mem
    
                 pop      hl
    
                 ld       c,(hl)
                 inc      hl
                 ld       b,(hl)
    
                 call     crt_str_dsc
    
                 push     hl
                 ld       l,a
                 call     copy_str
                 pop      de
    
                 ret
    

    En entrée, HL pointe vers un descripteur de variable de type chaîne. Le premier des 4 octets contient donc le nombre de caractères, qui est récupéré dans A. Puis HL est positionné sur le premier octet de l'adresse du contenu et cette adresse est poussée sur la pile.

    L'appel à alloc_str_mem vérifie ensuite s'il reste assez de place dans la mémoire dédiée aux chaînes pour ajouter A octets.

    Si la routine de vérification ressort, c'est qu'il y a de la place (une erreur aurait été immédiatement émise sinon) et une chaîne de A caractères a été allouée, (fretop) ajusté, et DE pointe vers cette nouvelle allocation.

    On récupère alors HL pour obtenir dans BC l'adresse actuelle du contenu de la chaîne.

    Un appel à crt_str_dsc crée une nouveau descripteur dans le buffer temporaire avec DE comme pointeur de contenu.

    Puis copy_str est appelé après avoir sauvé le pointeur vers le nouveau descripteur dans la pile et mis la taille de la chaîne dans L.

    Je ne copie pas le code de copy_str ici. Il est extrêmement simple et copie L caractères de la zone pointée par BC vers la zone pointée par DE. Autrement dit, de la chaîne source vers l'emplacement nouvellement alloué.

    Au retour, DE prend la valeur du nouveau descripteur de chaîne, qui est actuellement dans le buffer temporaire et sera récupéré par la fin de la routine de l'instruction LET.

    Ouf!

    Ramasse miettes

    En fait... ce n'est pas tout à fait complet. Lors de la tentative d'allocation de chaîne alloc_str_mem, s'il n'y a plus de place dans la mémoire dédiée, un ramasse miettes est lancé (garbage collection). Cette routine va compacter la mémoire des chaînes de caractères en comblant les trous des données qui ne sont plus valides, d'anciennes valeurs de chaînes qui ne sont plus pointées par aucune variable.

    C'est un gros morceau qui doit parcourir les variables mais aussi les tableaux, je laisse ça de côté (pour le moment ?).

    À la fin de cette routine, l'allocation est tentée à nouveau. Si lors de cette nouvelle tentative, il n'y a toujours pas assez de mémoire, alors l'erreur est vraiment lancée.

    Un peu de BASIC

    C'est un peu la tradition de ces articles, voyons maintenant un programme en BASIC qui affiche la valeur des variables. Puisque toutes les variables sont effacées au démarrage d'un programme, il est nécessaire d'en initialiser dans le programme.

    10 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
    20 PRINT"1":A$="ABC"
    30 GOSUB 1000
    40 PRINT"2":B$="DEF"
    50 GOSUB 1000
    60 PRINT"3":A$=""
    70 GOSUB 1000
    100 END
    1000 VT=FNPK(&"49D8")
    1010 AT=FNPK(&"49DA")
    1020 FOR PT=VT TO AT-1 STEP 6
    1030 T1=PEEK(PT)
    1040 T2=PEEK(PT+1)
    1050 IF (T2 AND 128)=0 THEN 1100
    1060 PRINT CHR$(T1 AND 127);
    1070 PRINT CHR$(t2 AND 127);
    1080 PRINT "$="+CHR$(34);
    1090 GOSUB 2000:PRINT CHR$(34)
    1100 NEXT PT
    1200 RETURN
    
    2000 V=FNPK(PT+4)
    2000 C=PEEK(PT+2)
    2010 IF C=0 THEN RETURN
    2020 V=FNPK(PT+4)
    2030 FOR I=1 to C
    2040 PRINT CHR$(PEEK(V+I-1));
    2050 NEXT I
    2060 RETURN
    

    Va afficher

    1
    A$="ABC"
    2
    A$="ABC"
    B$="DEF"
    3
    B$="DEF"
    

    La partie du programme entre 10 et 100 s'occupe de manipuler des variables et d'appeler l'affichage de leur contenu.

    La partie entre 1000 et 1200 regarde la liste des variables comme dans l'article précédent, mais ne sélectionne que celles de types chaînes de caractères.

    La partie à partir de 2000 va chercher le nombre de caractères et le pointeur vers les données pour afficher le tout, caractère par caractère.


Page 1 / 13 (suivant) »