Site logo

Triceraprog
La programmation depuis le Crétacé

  • VG5000µ, les variables en mémoire ()

    Pour terminer cette série sur la gestion de la mémoire par le BASIC sur VG5000µ, j'aborde la manière dont sont stockées les variables en mémoire.

    Les variables systèmes

    On l'a vu dans l'article sur la cartographie de la mémoire, il y a six variables systèmes intéressantes sur ce sujet :

    • (vartab) est la première adresse d'une variable. C'est ici que sont stockés les noms des variables numériques (avec leur valeur) ou chaînes (avec un pointeur vers leur contenu),
    • (arytab) est la première adresse de stockage du contenu des tableaux dimensionnés par DIM (ou bien les tableaux crées par défaut par le BASIC avec un DIM implicite), avec leur nom, leur taille, et leur contenu (ou pointeurs),
    • (strend) est la première adresse de la zone libre de stockage, tout ce qu'il y a à partir de cette adresse et « au-dessus » jusqu'à la pile (pointée par le registre SP) est la mémoire BASIC « libre » (ce qui n'est pas tout à fait vrai puisque la mémoire pour les chaînes de caractères est séparée).
    • (stktop) est le haut de la pile, l'adresse un octet au-dessus est la première adresse de la zone réservée pour les chaînes de caractères.
    • (fretop) est le pointeur de la zone libre pour les chaînes de caractères. L'adresse juste au-dessus contient le début des données de chaînes de caractères,
    • (memsiz) est l'adresse la plus haute adressable par le BASIC, et aussi le haut de l'espace réservée aux chaînes de caractères (inclus).

    Une première constatation est qu'il y a deux mémoires réservées au BASIC, disjointes. La première est celle qui contiendra tous les noms des variables et les contenus numériques. La place restante de cette zone est donnée par la fonction FRE(0) (la valeur du paramètre importe peu, seul son type importe).

    La seconde est celle qui contiendra tout le contenu des chaînes de caractères, dans des variables ou dans des tableaux. L'espace restant dans cette zone est donné par la fonction FRE(" ") (la encore, seul le type du paramètre compte). L'espace est fixe, de 50 octets au démarrage de la machine, et déterminé par le premier paramètre de la commande CLEAR. Faire un CLEAR 0 est tout à fait possible, mais alors vous ne pourrez plus stocker de chaîne de caractères.

    Création d'une variable

    Aparté sur la commande LET

    Dans le BASIC tel qu'il a été créé à l'université de Dartmouth, chaque ligne doit contenir une commande et une seule. La définition et l'assignation d'une variable se font avec la commande LET. Cette obligation de commande a un avantage sur l'analyse du programme BASIC par un compilateur ou un interpréteur : s'il n'y a pas de commande, c'est une erreur de syntaxe, et il n'y a pas d'exception.

    Le BASIC de Microsoft a relaxé cette obligation en rendant la commande LET optionnelle, et cette exception a été conservée par de nombreux BASIC par la suite. Mais pas partout, sur un ZX81 par exemple, chaque nouvelle ligne demande d'entrer une instruction via la touche du clavier correspondante, et le LET est obligatoire.

    Lorsque l'instruction LET est optionnelle, le décodage du BASIC lors de la tokénisation est plus complexe : il faut vérifier que ce qui est trouvé sur la ligne ne correspond à aucune instruction et dans ce cas-ci, faire comme si une instruction LET était présente. C'est du code en plus pris dans la ROM.

    Avant tout, que cherche-t-on ?

    Lorsque BASIC rencontre une variable, il lui faut toujours en premier lieu vérifier son existence. En effet, la première assignation de valeur à une variable vaut création avec une valeur par défaut.

    Et pour savoir si cette variable existe, il faut en connaître le nom. C'est la première partie de la routine qui se trouve en $38da.

    getvar:      xor      a,a
    
                 ld       (dimflg),a
                 ld       c,(hl)
    get_id:      call     a_to_z
                 jp       c,stx_err_prt
    
                 xor      a,a
                 ld       b,a
                 ld       (valtyp),a
    
                 rst      chget
                 jr       c,idnum_trail
                 call     a_to_z_2
                 jr       c,id_end
    
    idnum_trail: ld       b,a
    idtrail_skp: rst      chget
                 jr       c,idtrail_skp
    
                 call     a_to_z_2
                 jr       nc,idtrail_skp
    
    id_end:      sub      a,$24
                 jr       nz,num_vrble
    
                 inc      a
                 ld       (valtyp),a
                 rrca
                 add      a,b
                 ld       b,a
                 rst      chget
    num_vrble:   ld       a,(subflg)
    

    En premier lieu, (dimflg) est mis à zéro. Cette variable sert lorsque l'on manipule un tableau. Comme on n'a pas encore cette information, la routine part sur du non tableau.

    Note : il se peut que A soit différent de 0, lorsque l'on arrive par l'instruction DIM, qui saute par-dessus le XOR A,A, mais laissons ça de côté, le fonctionnement de DIM est de l'acrobatie en assembleur Z80...

    HL pointe, comme souvent lors de décodage d'une ligne, sur l'emplacement de la ligne en cours d'exécution. Si on est arrivé ici, c'est que l'on s'attend à trouver un nom de variable à cet endroit. Le premier caractère est donc lu dans C, puis il est vérifié que ce caractère est une lettre. En effet, chaque identifiant doit commencer par une lettre.

    Si ce n'est pas le cas, un saut vers le traitement d'une erreur de syntaxe est fait immédiatement.

    Donc le cas contraire, tout comme la routine s'initialise dans un mode non tableau, elle part du principe que la variable est numérique. On place donc 0 dans (valtyp), dans lequel est tenu à jour en tout temps le type de l'expression en cours. 0 dans (valtyp) signifie numérique.

    Par RST CHGET, un potentiel second caractère pour le nom de la variable est lu. Cette routine chget renvoie le caractère lu dans A et l'accompagne de quelques informations. Si le flag Carry est à 1, cela signifie que le caractère est un chiffre. La routine en profite pour traiter ce cas en sautant plus loin, un chiffre en deuxième caractère est valide.

    Si ce n'était pas un chiffre, on vérifie que c'est une lettre. Note au passage : la première fonction A_TO_Z lit un caractère depuis ce que pointe HL, A_TO_Z_2 fait la même vérification mais depuis le caractère déjà présent dans A.

    Si ce second caractère n'est pas une lettre, alors on doit avoir le nom de l'identifiant, on passe à la suite en id_end.

    Le LD B,A sauve le second caractère de la variable s'il existe. B avait été initialisé à 0 peut avant.

    Entre idtrail_skp et id_end (exclu), une boucle saute tous les caractères qui sont soit des chiffres, soit des lettres. En effet, il est tout à fait valide d'avoir des noms de variables plus long que deux caractères. Même si les caractères surnuméraires sont ignorés.

    Arrivé en id_end, on vérifie le dernier caractère lu. Est-ce un $, si non, on saute plus loin en num_vrble, la variable est bien numérique. Dans le cas contraire, 1 est mis dans (valtyp) pour désigner un type chaîne de caractères.

    Puis le second caractère de la variable, qui avait été sauvé dans B est augmenté de $80 (A égal à 1 après RCCA vaut $80). C'est comme cela que les variables de types chaînes de caractères sont identifiées : le second octet de leur nom a le bit 7 à 1.

    Lorsque l'on arrive dans num_vrble, on a dans C le premier caractère de la variable, dans B le second caractère de la variable, porteur de l'information de type chaîne, et (valtyp) qui contient aussi le type de la variable.

    Aparté sur les noms de variables longs

    Dans le BASIC original, la simplicité voulu par le langage avait amené les auteurs à ne permette des noms de variables qu'à une seule lettre, éventuellement suivie par un chiffre. Plus tard, les variables de type chaînes ont été ajoutées, et le nombre de caractères étendus.

    Dans le monde de la micro-informatique 8 bits, il n'y a pas beaucoup de place en RAM, et cette limite est soit conservée, soit relaxée par l'intermédiaire du système permissif des noms longs, dont seuls les premiers caractères sont significatifs.

    Sur VG5000µ, ce sont les deux premiers caractères qui comptent. D'ailleurs, tous les noms de variable en interne ont deux caractères, le second caractère étant éventuellement égal à $00.

    Je trouve cette idée de caractères significatifs désastreuse. Si elle part de l'idée que l'on peut utiliser des noms de variables plus expressifs, elle n'en n'offre pas les moyens, car absolument aucun test n'est fait sur ces caractères supplémentaires. D'expérience, il est facile, dans un programme un peu long, de ne plus faire attention au fait que deux noms longs possèdent les deux mêmes premiers caractères.

    Et si l'on y fait attention, il faut alors trouver un autre nom pour éviter la collision, et ce nom perd souvent en signification claire, et par la même perd l'intérêt des noms longs.

    Pour qui est-ce ?

    Il existe quelques contraintes sur les variables, et c'est dans l'aiguillage suivant qu'elles sont traitées.

    num_vrble:   ld       a,(subflg)
                 dec      a
                 jp       z,aryvar
    
                 jp       p,simplevar
    
    
                 ld       a,(hl)
                 sub      a,' ('
                 jp       z,subscript
    
    simplevar:   xor      a,a
    

    La variable système (subflg) sert à plusieurs choses. Ici, elle donne une indication sur une contrainte au niveau de la variable attendue. Si (subflg) est à 1, c'est que l'on s'attend à un tableau, le branchement est alors vers la recherche d'une variable tableau.

    Si (subflg) est supérieur à 1, alors c'est que les tableaux sont interdits, on saute donc vers simplevar. Les deux cas d'interdictions sont la variable d'index d'un FOR et la variable paramètre d'une fonction DEFFN.

    S'il n'y a pas de contrainte sur cette variable, mais que l'on trouve une parenthèse ouvrante à la suite de la variable, alors c'est qu'il y a un index, et l'on va vers cette routine.

    Et dans le cas contraire, il s'agit d'une variable simple. Ouf!

    Dans le cas simple

    Comme cet article va être assez long comme ça, on traitera les tableaux une autre fois. À présent que l'on sait que l'on a affaire à une variable simple (pas un tableau), que l'on a son nom et son type, il est grand temps de vérifier si elle existe !

    La première partie de cette recherche consiste à initialiser le domaine de recherche et... de vérifier si par hasard on ne serait pas en train d'évaluer une fonction DEFFN.

    simplevar:   xor      a,a
                 ld       (subflg),a
                 push     hl
    
                 ld       d,b
                 ld       e,c
    
                 ld       hl,(prmnam)
                 rst      de_compare
                 ld       de,prmval
                 jp       z,pop_hl_ret
    
                 ld       hl,(arytab)
                 ex       de,hl
                 ld       hl,(vartab)
    

    Tout d'abord, (subflg) est remis à 0. Le contexte a bien été traité et ne doit plus l'être. Puis le PUSH HL sert à sauver le pointeur vers la ligne tokenisée en court.

    Le nom de la variable, présente dans BC est placée dans DE pour utiliser la comparaison entre DE et HL.

    HL prend la valeur de la variable système (prmnam) (parameter name), qui contient, s'il y a lieu, le nom de la variable qui sert de paramètre à une fonction DEFFN en train d'être évaluée.

    Si la variable que l'on cherche est celle du paramètre d'une fonction que l'on est en train d'évaluer, il faut la traiter spécialement, car le nom de cette variable ne doit pas affecter une variable du même nom hors de la fonction.

    Le traitement spécial consiste à, si DE et HL sont égaux, sortir immédiatement de la routine de recherche, en faisant pointer DE sur la variable système prmval. Ce buffer de 4 octets contient la valeur actuelle du paramètre de la fonction, et DE est le pointeur que renvoie la routine de recherche de variable pour identifier la valeur cherchée.

    Dans le cas d'une recherche générique, DE est initialisé avec (arytab) et HL avec (vartab), les deux bornes de la mémoire contenant les variables simples.

    Cette fois on cherche !

    Ça y est, après tous ces préparatifs, on arrive au point de la recherche de la variable dans la mémoire ! Et pour cela, la routine va dérouler une boucle qui va cherche parmi tous les variables existantes une dont le nom correspond.

    next_var:    rst      de_compare
                 jp       z,no_var_yet
    
                 ld       a,c
                 sub      a,(hl)
    
                 inc      hl
                 jp       nz,var_diff
    
                 ld       a,b
                 sub      a,(hl)
    var_diff:    inc      hl
                 jp       z,var_found
    
                 inc      hl
                 inc      hl
                 inc      hl
                 inc      hl
                 jp       next_var
    

    En début de boucle, une comparaison entre HL et DE est faite. Si les deux sont égaux, c'est qu'on a fini la recherche. En effet, tout au long de la boucle, HL, parti de (vartab), va être incrémenté. DE représente la limite haute, qui ne bouge pas.

    Si la recherche est terminée sans avoir trouvé la variable, alors on saute en no_var_yet, il va falloir la créer. En effet, tout accès à une variable en BASIC induit sa création si elle n'existe pas encore.

    En se souvenant que BC contient le nom de la variable inversé, le premier caractère, dans C est soustrait de celui pointé par HL. S'ils sont différents, c'est qu'on n'a pas trouvé la variable pour le moment, on saute plus loin.

    Si le premier caractère correspond, on fait le même test avec le second caractère. Si ces deux tests passent, alors on a trouvé le nom, on saute en var_found.

    Sinon, on saute les quatre octets suivants, qui contiennent la valeur de la variable, et on boucle.

    Note : comme le nom de la variable en interne est modifié en fonction de son type, cette recherche montre bien que les variables A et A$ sont deux variables différentes. De même que les fonctions, que l'on n'a fait qu'effleurer. Dans la cas d'une fonction définition par DEF FN A(...), c'est le premier des deux octets qui porte un bit 7 à 1, et qui défini donc une troisième espace de nom.

    Le cas où les deux octets aurait un bit 7 à 1 pourrait définir une fonction sur des chaînes de caractères. Mais ce cas n'est pas permis par le BASIC sur VG5000µ.

    Création de la variable

    De la section précédente, on peut sortir soit en ayant trouvé la variable, soit en ne l'ayant pas trouvé. Si la variable n'est pas trouvé, il faut la créer et l'initialiser puis enchaîner sur la section où la variable est trouvée... si elle a pu être créée bien entendu.

    Comme c'est assez long, voyons ça en plusieurs partie. Tout d'abord, la création elle-même, avec un petit plot twist.

    no_var_yet:  pop      hl
                 ex       (sp),hl
    
                 push     de
                 ld       de,from_eval
                 rst      de_compare
                 pop      de
                 jp       z,ret_null
    
                 ex       (sp),hl
                 push     hl
    
                 push     bc
                 ld       bc,$0006
                 ld       hl,(strend)
                 push     hl
                 add      hl,bc
    
                 pop      bc
                 push     hl
                 call     mem_move_ckk
                 pop      hl
    
                 ld       (strend),hl
                 ld       h,b
                 ld       l,c
                 ld       (arytab),hl
    

    Tout d'abord, on récupère le pointeur sur la ligne évaluée qui était depuis la pile et on l'échange avec la valeur actuellement en haut de la pile. C'est un pas de danse assez classique en Z80 qui permet d'aller récupérer la valeur en deuxième position sur la pile.

    Puis on sauve la valeur de DE (qui contient (arytab)) avant d'y mettre une valeur spécifique : l'adresse d'une instruction de retour après un CALL particulier. Cette adresse est le chemin que prend l'appel à la récupération d'une variable lors de l'évaluation d'une expression.

    Si on vient de là, alors c'est un cas spécifique, et on saute à la section ret_null qui est un raccourci qui renvoie directement la valeur nulle à l'appelant. Et tout ceci sans créer de variable ! Après tout, pourquoi créer une variable qui a sa valeur par défaut ? Nous verrons ret_null plus loin.

    Dans le cas où l'on ne vient par d'une évaluation, alors l'adresse de retour est remise à sa place sur la pile et on y repousse le pointeur sur la ligne exécutée.

    Il s'agit maintenant de vérifier s'il y a de la place en mémoire pour créer la variable. Une variable a besoin de 6 octets en mémoire, et c'est donc avec 6 qu'est initialisé BC (après avoir été sauvé sur la pile, car BC contenait une information importante : le nom de la variable).

    Ici, il faut suivre... HL est initialisé avec (strend), c'est-à-dire la première adresse libre en mémoire principale et on lui ajoute 6 via BC. Au passage, sur la pile est poussé (strend), qui est récupéré dans BC, puis l'adresse (strend) + 6 est poussée sur la pile.

    On résume, par ordre croissant, on a :

    • Dans DE se trouve (arytab)
    • Dans BC se trouve (strend)
    • Dans HL se trouve (strend) + 6

    Sur la pile on a (strend) + 6 en première position de POP.

    Tout est prêt pour appeler mem_move_chk qui va déplacer la zone comprise entre DE et BC, c'est-à-dire toute la mémoire des tableaux, vers une zone dont l'adresse de fin sera HL. Autrement dit, les tableaux sont poussés de 6 octets pour faire de la place pour la nouvelle variable.

    Cette routine de déplacement commence aussi par une vérification que la place nécessaire est disponible. Dans le cas contraire, une erreur est levée et le processus est arrêté.

    Après le déplacement de la mémoire pour faire de la place, les variables (strend) et (arytab) sont ajustées à leur nouvelles valeur.

    clear_mem:   dec      hl
                 ld       (hl),$00
                 rst      de_compare
                 jr       nz,clear_mem
    
                 pop      de
                 ld       (hl),e
                 inc      hl
                 ld       (hl),d
                 inc      hl
    

    À présent qu'un emplacement est libre pour la variable, on parcourt son emplacement pour y placer des $00 avec cette boucle.

    Puis on récupère le nom de la variable dans DE et on enregistre ce nom dans les deux premiers octets des 6 octets tout neufs.

    La variable est à présent créée. Il ne reste plus qu'à retourner un pointeur avec l'emplacement de sa valeur à l'appelant.

    var_found:   ex       de,hl
                 pop      hl
                 ret
    

    DE prend la valeur de HL qui pointe juste après le nom de la variable, donc sur les 4 octets de sa valeur.

    Puis HL récupère sa valeur de pointeur d'exécution et la routine se termine.

    Retour de variable non définie

    On l'a vu juste avant, si, lors de l'évaluation d'une expression, une variable n'est pas trouvée, le BASIC ne va pas créer cette variable et se contentera de renvoyer la valeur nulle pour le type demandé.

    Nous pouvons le confirmer avec cette petite expérience :

    10 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
    20 PRINT FNPK(&"49D8"), FNPK(&"49DC")
    30 PRINT A
    40 PRINT A$
    50 PRINT NPK(&"49D8"), NPK(&"49DC")
    

    Donne :

    Aucune variable créée

    Le nombre à droite est (vartab) et ne bouge pas, puisque le listing ne bouge pas. La valeur de droite est la valeur de (strend) et reste constante entre le premier et le second affichage. Aucune variable n'a été créée par les accès à A et A$.

    La différence de 12 octets correspond à la variable créé par la fonction elle-même et à la variable paramètre de cette fonction. En effet, et c'est assez étrange, alors que la recherche de variable dans une fonction est court-circuitée, comme expliqué plus haut, le BASIC créé tout de même une variable vide, qui ne sera pas utilisée.

    Du coup, si vous voulez éviter de perdre 6 octets, prenez comme non de paramètre de vos fonction une variable utilisée ailleurs. Elle ne sera pas modifiée.

    Changeons un peu l'expérience et cette fois, donnons les valeurs nulles (0 pour un nombre, "" pour une chaîne) spécifiquement aux deux variables (le LET est optionnel, mais je le laisse pour être clair sur la signification de la création de la variable).

    Aucune variable créée

    La valeur de (vartab) est différente par rapport au premier test car le programme est un peu plus long. Néanmoins, sa valeur ne change pas avant et après la création des variables, ce qui donne une référence. Il y a toujours 12 octets pour les variables créées par la fonction.

    Par contre, (strend) augmente de 12 octets avant et après les assignations. Les variables ont bien été créées.

    Et cette valeur nulle ?

    ret_null:    ld       (fac),a
                 ld       hl,null_str
                 ld       (faclo),hl
                 pop      hl
                 ret
    

    Lors du branchement vers ret_null, A avait été mis à 0, c'est donc cette valeur que l'on met dans l'accumulateur flottant, qui contient la valeur de l'expression évaluée. Lorsqu'on lit une chaîne, ce sont dans les deux premiers octets de ce même accumulateur que se trouve un pointeur vers la chaîne évaluée. On y place un pointeur vers une chaîne nulle (null_str contient un $00).

    POP HL récupère le pointeur sur la ligne en exécution. Pas besoin de mettre en place DE, car cette branche de la routine n'est appelée qu'en cas d'évaluation d'expression, et comme l'adresse de retour a été enlevée de la pile pour comparaison et n'y a pas été remis, le retour ne se fait pas à l'appelant direct (qui irait chercher la valeur de la variable retournée) mais à son appelant précédent (l'évaluateur).

    Est-ce que cette optimisation acrobatique était bien nécessaire ? Je n'ai pas la réponse.

    Reset de la mémoire

    Une dernière chose avant de clore cet article. Le BASIC du VG5000µ efface toutes les variables lorsqu'un programme est lancé avec RUN. On est assuré que la mémoire est « effacée » (le contenu est toujours là, seul les pointeurs sont réinitialisés). Les valeurs spécifiées par un CLEAR sont par contre conservées.

    De même, les variables sont effacées au moindre changement dans le listing.

    Un peu de BASIC

    Et pour terminer, un petit listing BASIC qui va afficher les variables présentes en mémoire... et donc du programme en cours d'exécution.

    10 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
    20 VT=FNPK(&"49D8")
    30 AT=FNPK(&"49DA")
    40 PRINT (AT-VT)/6;" VARIABLE(S)"
    50 FOR PT=VT TO AT-1 STEP 6
    60 PRINT CHR$(PEEK(PT) AND 127);
    70 PRINT CHR$(PEEK(PT+1) AND 127)
    80 NEXT PT
    

    Affiche

      4 VARIABLE(S)
     PK
     P
     VT
     AT
    

    Et PT ? Comme la variable est créés après la récupération de AT ((arytab)), cette variable n'est pas vue par le programme, ce qui est tout à fait conforme à ce qui était attendu.


  • VG5000µ, le BASIC cherche ses lignes ()

    Peut-être l'avez vous remarqué, certains programmes en BASIC sont construits avec leur programme principal avec des numéros de ligne « hauts » et ont leurs traitements souvent appelés dans les lignes « basses ».

    Ainsi, dans l'article précédent sur le listing, le programme commence par un GOTO 10000 et le décodage de ligne est en 1300. À vrai dire, avant que je ne stocke les adresses des tokens dans un tableau, le décodage systématique de leurs noms étaient dans des lignes encore plus basses.

    La raison en est toute simple : lorsque le BASIC sur VG5000µ cherche un numéro de ligne, il le cherche systématiquement depuis le début du programme.

    C'est où que ça cherche ?

    La routine de recherche de ligne est en $2347 et est appelée par GOTO, RESTORE, RENUM (beaucoup !), LIST, AUTO (à chaque nouvelle ligne), NEW, mais aussi lorsqu'il s'agit d'insérer une nouvelle ligne dans le listing au bon endroit, voire de remplacer une ancienne.

    Pour appeler cette routine, il suffit d'assigner à DE le numéro de ligne recherché.

    line_search: ld       bc,$0000
    
                 ld       hl,(txttab)
    line_search_lp:
                 ld       (prelin),bc
    
                 ld       b,h
                 ld       c,l
                 ld       a,(hl)
                 inc      hl
                 or       a,(hl)
                 dec      hl
    
                 ret      z
    
                 inc      hl
                 inc      hl
                 ld       a,(hl)
                 inc      hl
                 ld       h,(hl)
                 ld       l,a
    
                 rst      de_compare
    
                 ld       h,b
                 ld       l,c
    
                 ld       a,(hl)
                 inc      hl
                 ld       h,(hl)
                 ld       l,a
    
                 ccf
                 ret      z
    
                 ccf
                 ret      nc
    
                 jr       line_search_lp
    

    Tout commence avec l'initialisation de BC avec $0000 et de HL avec le pointeur de début de listing. Puis débute la boucle de recherche.

    En début de boucle, la ligne contenue dans BC est stocké dans une variable système prelin. Cette variable système contiendra en tout temps le dernier numéro de ligne qui a été trouvé dans le listing.

    Jusqu'au RET Z, il s'agit de lire le contenu du pointeur de ligne suivante et vérifier s'il est égal à $0000. Si c'est le cas, ce RET Z provoque la fin de la routine. Le Carry Flag est à 0 grâce au OR qui précède, ce qui indiquera que la ligne n'a pas été trouvée.

    Pour rappel, une ligne de BASIC stockée en mémoire commence par un pointeur de chaînage vers la ligne suivante, ou $0000 s'il s'agit de la dernière ligne.

    On peut être surpris par ce DEC HL avant le retour de la routine, compensé par le premier INC HL juste après le RET Z. Comme le BASIC sauve un octet en fusionnant les 4 octets nuls terminant le chaînage des lignes avec le $00 d'une fin de ligne, le pointeur, qui est un cran après ce $00 de fin de ligne, réintègre le besoin des 4 octets nuls de fin de chaînage.

    Mais si on ne sort pas de la routine, alors cet octet n'est pas fusionné, il faut remettre HL à sa place initiale.

    Après le RET Z, le numéro de la ligne BASIC, qui est dans la ligne BASIC stockée formé par les deux octets suivants, est lu dans HL. Puis comparé à DE via RST DE_COMPARE.

    En se souvenant que le numéro de ligne recherché est dans DE, après cette comparaison, il y a trois cas :

    • HL et DE sont égaux : on a donc trouvé la ligne. Le flag Z est mis à 1, le Carry Flag est à 0
    • HL est supérieur à DE : on a trouvé un numéro de ligne plus grand que celui recherché. La ligne recherchée n'existe donc pas. Z est à 0, Carry est à 0 aussi.
    • HL est inférieur à DE : il faut continuer à chercher. Z est à 0, Carry est à 1.

    Après la comparaison, HL est remis à l'adresse sauvée en début de boucle dans BC, pour revenir sur l'adresse de chaînage. Cette adresse est celle de la ligne que l'on vient de valider comme étant la suivante (voire la bonne !), l'adresse est donc lue à nouveau dans HL.

    Les lignes suivantes traitent les différents cas de comparaison de ligne.

    Tout d'abord le couple CCF / RET Z fait sortir la routine si la ligne a été trouvée. CCF qui inverse la valeur du Carry Flag, le met donc à 1 dans le cas où Z était à 1. En sortie de routine, le Carry Flag à 1 indique que la ligne a été trouvée. HL pointe sur le début de la zone de cette ligne.

    Puis le couple CCF / RET NC remet le Carry Flag a son état d'après la comparaison entre HL et DE et le test. Dans le cas où la ligne n'est pas là, on sort de la routine, mais donc cette fois avec le Carry Flag à 0, indiquant que la ligne n'a pas été trouvée. HL pointe alors vers l'adresse de la ligne suivante, et BC sur la ligne précédente. C'est intéressant dans le cas où l'on veut chaîner une nouvelle ligne entre les deux.

    Si aucun de ces cas de sortie n'a eu lieu, le JR final repart pour un nouveau tour de boucle.

    Et NEXT et RETURN ?

    NEXT et RETURN sont deux instructions qui provoquent une rupture de séquence. Autrement dit, suite à leur exécution, l'exécution continue ailleurs. En début de boucle si la boucle n'est pas terminée avec NEXT, ou après le GOSUB dans le cas du RETURN.

    Le fonctionnement de ces instructions est différent. FOR et GOSUB vont placer le pointeur sur les instructions en train d'être interprétées dans un endroit sûr. GOSUB le place sur la pile, FOR aussi... si des FOR sont imbriqués, sinon, dans une variable système spécifique (endfor).

    Lorsqu'il faut reprendre l'exécution, le pointeur vers l'instruction en cours en remis à la valeur sauvegardée (avec son numéro de ligne correspondant, une autre variable système à garder cohérente, (curlin)) puis le décodage continue.

    Par conséquent, les boucles FOR/NEXT et un RETURN ne sont pas affectés par la recherche de numéro de ligne.

    Ça optimise ?

    Choisir pour des destinations de saut de numéros de lignes les plus proches du début du programme est donc une optimisation. Mais soyons franc, c'est une toute petite optimisation. La recherche par parcours de liste chaînée est rapide en assembleur en comparaison de l’évaluation d'une expression un peu complexe avec des variables à manipuler par exemple.

    J'ai pu lire que sur certains BASICs d'autres machines, une optimisation avait été faite par le simple fait de rechercher la ligne de destination par rapport à la ligne actuelle, privilégiant ainsi la localité des sauts. Est-ce que cela serait efficace sur la ROM VG5000µ ? Peut-être... à suivre.


  • VG5000µ, un listing en BASIC ()

    Un programme en BASIC qui ferait un listing de lui-même. Reprogrammer l'instruction LIST. C'est un peu inutile, mais cela est un prétexte pour expliquer comment le programme est stocké en mémoire dans la machine.

    Le VG5000µ utilise une version du BASIC-80 de Microsoft. Ce BASIC se retrouve sur d'autres machines dans des versions variées, mais dont on retrouve les grands principes.

    Structure générale d'un programme en mémoire

    Comme on l'a vu précédemment, le BASIC respecte un pointeur vers le début du programme qui se nomme (txttab), situé en $488E. Par défaut avec la ROM interne du VG5000µ, si aucune extension ne l'a modifié, (txttab) vaut $49FC au démarrage.

    Chaque ligne est composée par les éléments suivants :

    • Une adresse mémoire (sur 2 octets) vers l'emplacement de la ligne suivante, ou $0000 pour marquer la fin de la liste de lignes,
    • Un numéro de ligne (sur 2 octets) qui correspond au numéro de ligne BASIC,
    • Un ensemble d'octets représentant le contenu de la ligne, qui se termine pas un $00

    Juste après le zéro se trouve la nouvelle ligne, et la ROM maintient les lignes dans l'ordre de leur numéro de ligne BASIC.

    L'ensemble des lignes stockées forme donc une « liste chaînée » selon le schéma suivant.

    Liste chaînée des lignes du BASIC-80 sur VG5000µ

    Note : comme la dernière ligne est suivi par le pointeur vers l'adresse $0000, le BASIC en profite pour gagner un octet et ne met pas spécifiquement un $00 à la fin de cette ligne, puisqu'il y en a déjà 4.

    Pour notre programme de décodage de ce qui se trouve dans la mémoire, il va donc falloir d'abord aller récupérer l'adresse de (txttab). On pourrait la mettre en dur, mais faisons ça proprement.

    Le BASIC sur VG5000µ n'a comme instruction pour lire la mémoire que PEEK, qui ne lit qu'un octet. Qu'à cela ne tienne, voici une petite fonction qui, donnée une variable P pointant sur une zone mémoire, en retourne la valeur entière sur 16 bits qui y est stockée.

    DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
    

    De là, on peut donc partir de (txttab) puis de parcourir tout le chaînage jusqu'à trouver un pointeur nul ($0000).

    Ce qui peut donner ça :

    10020 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P) : REM Définition de la fonction pour lire un entier sur 2 octets
    10030 PT=FNPK(&"488E")                 : REM Récupération de (txttab)
    10040 NX=FNPK(PT)                      : REM Lecture du pointeur **next** (ligne suivante)
    10050 IF NX=0 THEN 10200               : REM Si ce pointeur est nul, on sort plus loin
    10060 PT=PT+2:LI=FNPK(PT)              : REM Sinon, on avant le pointeur de 2 et on lit le numéro de ligne
    10070 PRINT LI;" ";                    : REM On affiche le numéro de ligne
    10080 PT=PT+2:GOSUB 1300               : REM On avance le pointeur de 2 et on décode la ligne (voir plus loin)
    10090 PT=NX                            : REM Passage du pointeur vers la ligne suivante
    10100 GOTO 10040                       : REM Et on boucle...
    

    Voici le corps principal du programme écrit. Avec ça, même en ne mettant qu'un RETURN en ligne 1300, vous pouvez voir les numéros de ligne du votre programme s'afficher.

    Décodage du contenu des lignes

    Les lignes sont formées d'une suite d'octets terminée par un $00, située entre le numéro de la ligne BASIC et l'adresse de début de la ligne suivante.

    Le contenu d'une ligne peut contenir des caractères, mais aussi des tokens. Les tokens (jetons en français) représentent les instructions, fonctions et signes reconnus par le BASIC lorsque la ligne a été analysée à l'entrée par le clavier (sur cassette, les tokens sont enregistrés, et donc chargés, directement).

    Un token est facilement reconnaissable, son bit de poids fort est à 1. Autrement dit, si le caractère présent est strictmeent supérieur à 127, il s'agit d'un token. D'ailleurs, lorsque l'on entre un programme au clavier, le BASIC fait bien attention de mettre tous les caractères entrés sur 7 bits.

    C'est aussi dans ce traitement que le BASIC va chercher, à l'aide d'une liste de mots-clés et signes connus, à reconnaître et donc de tokeniser la ligne. Dès qu'une suite de caractères est un mot-clé ou un signe connu, la suite de caractères est remplacée, dans la chaîne encodée, par son token.

    Si les caractères lus ne forment pas un mot connu, ceux-ci sont copiés tels quels dans la ligne encodée.

    Par exemple, si BASIC encode la ligne : PRINT"HELLO", le résultat sera la suite $94,'"', 'H','E,'L','L','O','"',$00.

    Le décodage

    Pour reconstituer la ligne, il s'agit donc de prendre les caractères un par un et de vérifier. Est-ce un token ou pas ? Si ce n'est pas un token, on l'écrit directement à l'écran (ou presque, on va voir plus loin). Si c'est un token, on soustrait 128 à sa valeur pour avoir son index.

    Les tokens

    Reste à retrouver le token dans la table située en $209E et donc un extrait à partir du début ressemble à cela :

                 defb     $c5,$4e,$44,$c6,$4f,$52,$ce,$45,$58,$54      ; .ND.OR.EXT
                 defb     $c4,$41,$54,$41,$c9,$4e,$50,$55,$54,$c4      ; .ATA.NPUT.
                 defb     $49,$4d,$d2,$45,$41,$44,$cc,$45,$54,$c7      ; IM.EAD.ET.
                 defb     $4f,$54,$4f,$d2,$55,$4e,$c9,$46,$d2,$45      ; OTO.UN.F.E
                 defb     $53,$54,$4f,$52,$45,$c7,$4f,$53,$55,$42      ; STORE.OSUB
                 defb     $d2,$45,$54,$55,$52,$4e,$d2,$45,$4d,$d3      ; .ETURN.EM.
                 defb     $54,$4f,$50,$cf,$4e,$cc,$50,$52,$49,$4e      ; TOP.N.PRIN
                 defb     $54,$c4,$45,$46,$d0,$4f,$4b,$45,$d0,$52      ; T.EF.OKE.R
                 defb     $49,$4e,$54,$c3,$4f,$4e,$54,$cc,$49,$53      ; INT.ONT.IS
                 defb     $54,$cc,$4c,$49,$53,$54,$c3,$4c,$45,$41      ; T.LIST.LEA
                 ...
    

    Cette table est encodée de la manière suivante : les mots-clés sont les uns à la suite des autres. Chaque premier caractère a son bit de poids fort à 1, ce qui marque la séparation entre les mots.

    Faisons un essai : $94 est le token pour PRINT, soit 148 en décimal. 148 - 128 = 20. Mais attention, le premier token est 128, donc il faut commencer à compter à partir de 0. Cherchez le 21ième mot clé et vous lirez .RINT (attention, le .PRINT précédent est le codage de LPRINT)

    Pour faire plus simple, vous pouvez aussi compter 21 points.

    Dans mon premier essai de programme, j'avais traduit en BASIC ce que fait la ROM en assembleur : à chaque mot-clé, on parcours la table des mots-clés depuis le début. À chaque fois que l'on rencontre un caractère supérieur ou égal a 128, on décroit le numéro du token d'une unité. Arrivé à zéro, on a trouvé, on peut donc recopier à l'écran tous les caractères (sur 7 bits) jusqu'au prochain supérieur ou égal à 128, exclu.

    Ça fonctionne. Mais ce qui est très rapide en assembleur demande beaucoup de manipulations au BASIC. Parcourir un long tableau et faire des comparaisons et des tests est très lourd, même en usant de quelques astuces.

    Lorsqu'il fallait décoder un token élevé, comme le signe = (token 65) ou la fonction MID$ (token 95, le dernier), le décodage devenait très long, très très long.

    J'ai donc finalement opté pour payer le coût de décodage une fois, au détriment d'une consommation mémoire plus importante.

    5000 PRINT"INITIALISATION..."                   : REM Affichage d'un message, parce que c'est un peu long
    5010 DIM K(95)                                  : REM Il y 96 tokens, de 0 à 95
    5020 KT=&"209E"                                 : REM Adresse du début de la table
    5030 KW=0                                       : REM Initialisation avec le Keyword 0
    5040 K(KW)=KT:KW=KW+1                           : REM Adresse du Keyword courant dans le tableau et augmentation de l'index
    5050 KT=KT+1                                    : REM Déplacement à l'adresse suivante de la table
    5060 IF PEEK(KT) AND 128 THEN 5080              : REM Si bit de poids fort, on saute
    5070 GOTO 5050                                  : REM Sinon, on boucle sur l'avancée dans la table
    5080 IF (PEEK(KT) AND 127) = 0 THEN RETURN      : REM Si la caractère est $80, c'est fini !
    5090 GOTO 5040                                  : REM Sinon, on boucle sur l'enregistrement de l'adresse
    

    Note : j'aurai pu écrire ça avec une boucle FOR car pour cette ROM là, je connais la taille de la table, qui ne bougera pas, mais je voulais garder ce bout de code flexible. Il fonctionnera pour une autre tableau d'un autre BASIC-80.

    Décoder le token revient maintenant à aller chercher son adresse dans la table puis de recopier les caractères jusqu'au premier marquant le début du mot suivant.

    1500 REM KT <- ADRESSE TOKEN(V)
    1510 KW=V-128                       : REM Calcul du token à partir du caractère lu
    1520 KT=K(KW)                       : REM Lecture de l'adresse de son nom en mémoire
    1530 REM PRINT(KEYWORD(KT))
    1540 R$="":C=PEEK(KT)               : REM Préparation de la chaîne et lecture du premier caractère
    1550 R$=R$+CHR$(C AND 127)          : REM Construction de la chaîne
    1560 KT=KT+1                        : REM Passage à l'adresse suivante
    1570 C=PEEK(KT)                     : REM Lecture du caractère suivant
    1580 IF C>127 THEN 1600             : REM Si c'est le mot suivant, on saute plus loin
    1590 GOTO 1550                      : REM SInon, on boucle sur la construction
    1600 PRINT R$;:RETURN               : REM Le mot est trouvé, on l'affiche et on revient à l'appelant
    

    Note : j'aurai pu afficher directement chaque caractère plutôt que de construire la chaîne au fur et à mesure. Je n'ai pas vérifié ce qui serait le plus rapide. Cette version là use pas mal la mémoire de chaînes de caractère, c'est certain.

    Les entiers compactés

    Je n'ai aucune idée de si c'est leur nom officiel, mais c'est comme celui que je les appelle. Certains nombres sont encodées par le BASIC dans une forme particulière : le caractère $0E suivi d'un nombre entier codé sur 16 bits (donc deux octets).

    En particulier, les numéros de lignes sont codées de cette manière. Ainsi les instructions de saut comme GOTO ou GOSUB n'ont pas à décoder systématiquement le numéro en paramètre. Il a déjà été décodé et stocké dans la ligne.

    Afficher un entier compacté n'est pas très compliqué grâce à la fonction de lecture d'un entier sur 16 bits.

    2999 REM PRINT(NUM16(I+1))
    3000 I=I+1                    : REM I pointe vers le caractère $0E, l'entier est juste après
    3010 PRINT(FNPK(I));":";
    3020 I=I+2                    : REM On saute les deux caractères lus
    3030 RETURN
    

    Note : lorsque le BASIC utilise un entier compacté, il induit aussi une fin d'instruction. Il n'encode donc pas le séparateur ':' éventuel. Je l'affiche donc ici systématiquement, ce qui n'est pas tout à fait correct car il apparaît parfois en fin de ligne. Ce n'est pas syntaxiquement faux non plus.

    Les différents cas

    Il y a donc trois cas pour un caractère à décoder, auquel j'ajoute une quatrième :

    • Si le caractère est un token, on le décode,
    • Sinon, si le caractère est $0E, on décode un entier compacté,
    • Sinon, s'il est inférieur à 17 ou égal à 30 et 31, c'est un caractère de contrôle, on le remplace par le caractère espace,
    • Sinon, on le recopie tel quel.

    Ce qui donne :

    1300 REM DECODE LA LIGNE ENTRE PT ET NX
    1310 FOR I=PT TO NX-1                           : REM Décodage sur tout le contenu (voir la note juste après)
    1320 V=PEEK(I)                                  : REM Lecture du caractère
    1330 IF V AND 128 THEN GOSUB 1500:GOTO 1370     : REM Est-ce un token ?
    1340 IF V=14 THEN GOSUB 3000:GOTO 1370          : REM Est-ce un entier comptacté ?
    1350 IF V<17 OR V=30 OR V=31 THEN V=32          : REM Est-ce un caractère non affichable ?
    1360 PRINT CHR$(V);                             : REM Affichage du caractère
    1370 NEXT I                                     : REM Fin de la boucle
    1380 PRINT                                      : REM Saut à la ligne quand c'est terminé
    1390 RETURN
    

    Note : la ROM ne fait pas tout à fait comme ça, elle décode jusqu'à trouver le $00. Dans la pratique, les lignes sont stockées dans l'ordre du chaînage, par ordre de ligne croissant. Je décode donc entre le début du contenu de la chaîne tokenisé et la ligne suivante.

    Conclusion

    Voilà donc réunies toutes les pièces qui permettent de lister le programme en mémoire... et donc le programme lui-même.

    L'instruction LIST incluse dans la ROM du VG5000µ est meilleure que cela : les commentaires sont affichés d'une couleur différente, il n'y a pas d'espace avant les numéros de lignes, pas de ':' qui traînent derrière les GOTO et GOSUB en fin de ligne.

    Mais l'idée était surtout de montrer comment était encodé le listing en mémoire.

    Et pour finir, le programme en entier.

    10 GOTO 10000
    
    1300 REM DECODE LA LIGNE ENTRE PT ET NX
    1310 FOR I=PT TO NX-1
    1320 V=PEEK(I)
    1330 IF V AND 128 THEN GOSUB 1500:GOTO 1370
    1340 IF V=14 THEN GOSUB 3000:GOTO 1370
    1350 IF V<17 OR V=30 OR V=31 THEN V=32
    1360 PRINT CHR$(V);
    1370 NEXT I
    1380 PRINT
    1390 RETURN
    
    1500 REM KT <- ADRESSE TOKEN(V)
    1510 KW=V-128
    1520 KT=K(KW)
    1530 REM PRINT(KEYWORD(KT))
    1540 R$="":C=PEEK(KT)
    1550 R$=R$+CHR$(C AND 127)
    1560 KT=KT+1
    1570 C=PEEK(KT)
    1580 IF C>127 THEN 1600
    1590 GOTO 1550
    1600 PRINT R$;:RETURN
    
    2999 REM PRINT(NUM16(I+1))
    3000 I=I+1
    3010 PRINT(FNPK(I));":";
    3020 I=I+2
    3030 RETURN
    
    5000 PRINT"INITIALISATION..."
    5010 DIM K(94)
    5020 KT=&"209E"
    5030 KW=0
    5040 K(KW)=KT:KW=KW+1
    5050 KT=KT+1
    5060 IF PEEK(KT) AND 128 THEN 5080
    5070 GOTO 5050
    5080 IF (PEEK(KT) AND 127) = 0 THEN RETURN
    5090 GOTO 5040
    
    10000 REM DEMARRAGE
    10010 GOSUB 5000
    10020 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
    10030 PT=FNPK(&"488E")
    10040 NX=FNPK(PT)
    10050 IF NX=0 THEN 10200
    10060 PT=PT+2:LI=FNPK(PT)
    10070 PRINT LI;" ";
    10080 PT=PT+2:GOSUB 1300
    10090 PT=NX
    10100 GOTO 10040
    
    10200 PRINT"FINI"
    
    11000 END
    

  • VG5000µ, une Cartographie de la Mémoire BASIC ()

    Quand j'ai commencé à étudier les documentations sur le VG5000µ, des incohérences sur certains points me sont apparues. Les différentes sources ne donnaient pas les même renseignements. Ou alors je lisais mal. Mais un truc ne collait pas.

    En étudiant la ROM, là encore, ça ne collait pas avec les documentations. Mais le code ne ment pas, j'ai donc débuté une quête de la vérité : que se passe-t-il vraiment ?

    ... ce n'était peut-être pas si épique, mais bon...

    L'anomalie qui m'intéresse aujourd'hui est celle de la gestion de la mémoire par le BASIC. Et voici les pièces qui ont provoqué mon étonnement.

    Première pièce

    La manuel de l'utilisateur, page 46, indique que le premier paramètre de la commande définit la totalité de l'espace occupé par les chaînes de caractères et doit être compris entre -32768 et 32767. Spécifier une mémoire à réserver négative est un peu étrange, et un test rapide montre que passer une valeur négative est rejetée par le BASIC.

    Au passage, un coup d'œil dans la ROM montre que l'instruction CLEAR utilise une routine qui ne décode que des entiers positifs...

    Plus étonnant, le second paramètre, qui indique la plus haute adresse possible atteignable par le BASIC doit être inférieur à 32767 pour le VG5000 standard, et 42864 pour l'extension mémoire 16ko. Et ne mentionne pas 32ko d'extension.

    42864... Ça ne correspond pas à grand chose. Avec l'extension 16ko, je vois plutôt une adresse maximale à 49151. En effet, la ROM prend les 16 premiers ko, et les 16+16 ko de RAM sont à la suite. 48 * 1024 donne 49152 octets.

    Bref, louche. De plus, un essai direct d'un CLEAR 50, 42864 ne fonctionne pas, un paramètre invalide est indiqué. La routine pour décoder le second paramètre étant une routine qui prend des entiers entre -32768 et 32767.

    Une cartographie de la mémoire page 86 indique que la mémoire peut bien aller jusqu'à 49151, qu'il faut spécifier cette valeur en complément à 2 sous une forme négative. C'est incohérent avec la description de CLEAR mais l'expérience montre que la cartographie a raison. Elle indique aussi des valeur de pointeur (stktop), (fretop), (arytab) et (txttab), mais n'indique pas où les trouver.

    Deuxième pièce

    La deuxième source est « Clefs pour VG5000 », un livre des éditions du P.S.I.

    Page 15, la description de CLEAR est globalement reprise avec les mêmes renseignements erronés.

    Page 69, une cartographie de la mémoire est disponible et indique des noms de variables pointeurs sur des zones mémoire. Plus loin, ces variables systèmes sont décrites, avec leurs adresses. C'est par ici que j'avais commencé à regarder le fonctionnement.

    Deux choses me paraissaient bizarre. Une flèche entre arytab et fretop sans nom. Soit. Mais aussi une variable fretop qui est sous la pile. C'est bizarre car le bas de la pile, c'est déjà le registre SP, quel intérêt de garder une information comme ça ? Pour protéger la pile ?

    C'est cependant cohérent avec le manuel d'utilisateur et donne une information en plus sur le fait que (memsiz) n'est pas initialisé tout en haut de la RAM disponible mais trois octets avant. Il ajoute une variable de système : (vartab).

    C'est à ce moment là que j'ai commencé à étudier la ROM et voir que (fretop) était initialisé avec la valeur (memsiz), et que lors d'allocation de chaînes de caractères, l'algorithme partait du principe que (fretop) était toujours supérieur à (stktop).

    En fait, l'erreur d'allocation de chaîne était lancée, si je ne me trompais pas (mais le doute est permis quand on regarde des pages de code assembleur), lorsque (fretop) atteignant (stktop) par le haut. Autrement dit, tant que (fretop)-(stktop) était positif, il y avait de la place. Je détaille ça plus loin.

    Et donc, ça ne collait pas avec le schéma.

    Les descriptions des variables sont les suivantes :

    • $488E : adresse de début du programme Basic (txttab)
    • $4895 : Adresse du haut de la pile (stktop)
    • $49C3 : adresse du haut de la zone "chaînes" (fretop)
    • $49D8 : adresse de début de la zone "variables" (vartab)
    • $49DA : adresse de début de la zone "chaînes" (arytab) (c'est incohérent d'après le schéma, mais on se doute bien que ary signifie array est qu'il s'agit en fait de la zone pour les tableaux DIM)
    • $49DC : adresse de fin du stockage en cours, n'est pas nommé. Je ne le savais pas à ce moment-là, mais il s'agit de (strend) et c'est le nom qu'il manque entre (arytab) et (fretop)... il faut dire que la description n'est pas hyper parlante. Quel stockage ? En cours de quoi ?

    Troisième pièce

    Le troisième document dans lequel je me suis plongé est celui du manuel technique, en anglais. Page 6, on trouve le tableau source des deux précédents documents., mais là encore, (fretop) est sous la pile...

    Plus loin, page 9, la liste des variables systèmes, là encore probablement la source du livre P.S.I, est donné. On y trouve (txttab), (stktop), (fretop), (vartab), (arytab) avec sa description correcte mentionnant des tableaux, et (fretop), et (strend) indiquant « end of storage in use ».

    La description de (strend) est un peu plus éclairante que sa traduction dans le livre P.S.I, même si ça manque de détail. Il n’apparaît pas dans la cartographie de la mémoire.

    Il n'y a pas d'autres mentions du fonctionnement des allocations mémoire dans ce document, à part un peu à propos de manipulation de (txttab) et (vartab) quand on veut lancer un programme BASIC depuis un ROM d'extension.

    Quatrième pièce

    De tous les documents sur le VG5000µ, les plus dignes de confiances, ceux qui vont un peu plus dans le détail et dans lesquels je n'ai jamais trouvé d'erreur jusqu'à maintenant, ce sont les « Technical Bulletin** ». Sur celui du 14 juin 1984, à propos de « BASIC Text Relocation » il est mentionné que (strend) sera bougé, avec (vartab), (arytab) et (temp), si (txttab) est modifié et que le BASIC exécute une des routines qui replace tout ça.

    Pas tellement plus d'information cependant.

    Dans le bulletin du 11 septembre 1984, une mention de (stktop) et (memsiz) est fait, qui donne les bonnes adresses en fonction des différentes configuration de mémoire.

    Au final...

    Au final... et bien le mieux est d'aller voir dans la ROM ce qu'il se passe. Et pour chercher, je pars sur deux pistes : où donc est-ce que (fretop) est utilisé, et qu'est-ce que (strend) ?

    (fretop) est utilisé a de nombreux endroits, mais quelques-uns de ses usages sont suffisant pour comprendre.

    L’initialisation

    La fonction suivante est appelé lorsqu'il s'agit de réinitialiser la mémoire. Au lancement de la machine, par un appel à NEW, mais aussi directement en reset_vars lors d'un RUN ou en init_vars lors d'un CLEAR.

    reset_mem:   ld       hl,(txttab)          ; $2ed9
    reset_mem_2: xor      a,a
                 jp       reset_mem_3
    
    reset_mem_4: inc      hl
                 ld       (vartab),hl
    reset_vars:  ld       hl,(txttab)
                 dec      hl
    init_vars:   ld       (temp),hl
                 ld       hl,(memsiz)
                 ld       (fretop),hl
                 xor      a,a
                 call     inst_restore
                 ld       hl,(vartab)
                 ld       (arytab),hl
                 ld       (strend),hl
    

    Tout commence avec comme point de repère (txttab), qui est le début du programme BASIC en cours.

    Le jp reset_mem_3 renvoie en reset_mem_4 après avoir coupé le lien sur la première ligne de BASIC, effaçant par la même occasion l'accès au programme (qui est toujours là...).

    Cette gymnastique avec reset_mem_3, qui n'est jamais appelé par ailleurs, vient à mon avis du patch de la ROM 1.1, qui fait tout pour rester stable dans les adresses de routines. Je vérifierai cette théorie plus tard.

    (vartab) est placé deux octets plus loin que (txttab) (HL est aussi incrémenté par le code de reset_mem_3)

    (temp) est placé un octet avec (txttab)

    (fretop) est initialisé à la même adresse que (memsiz), ce qui montre bien que, au moins là, les cartographie mémoire sont fausses.

    L'appel à inst_restore est l'exécution de l'instruction RESTORE du BASIC, qui va chercher et stocker la première ligne contenant des DATA.

    Puis enfin, (arytab) et (strend) prennent la valeur de (vartab).

    Un peu plus loin, les lignes suivantes remettent le registre SP à l'adresse en haut de sa zone, stockée dans (stktop).

                 ld       hl,(stktop)          ; $2eff
                 ld       sp,hl
    

    La conclusion sur l’initialisation de la mémoire est que les adresses de références sont (txttab), (memsiz) et (stktop). Les autres pointeurs sont placés en fonction de ces adresses.

    Vérification mémoire de chaînes

    Lorsqu'une chaîne de caractère est sur le point d'être créé, la routine suivante est appelée en premier lieu :

                 ld       hl,(stktop)          ; $36bf
                 ex       de,hl
                 ld       hl,(fretop)
                 cpl
                 ld       c,a
                 ld       b,$ff
                 add      hl,bc
                 inc      hl
                 rst      de_compare
                 jr       c,out_str_mem
    

    À l'entrée de cette routine, A contient le nombre de caractères de la chaîne à créer.

    Après les trois premières lignes, on se retrouve avec (stktop) dans DE et (fretop) dans HL. Les trois lignes suivantes mettent dans BC l'inverse de A (en complément à 2).

    Via le add, HL contient donc (fretop) moins le nombre de caractères dont on a besoin, (le inc qui suit est la correction de la soustraction en complément à 2).

    rst de_compare est une routine qui compare les valeurs de HL et DE, si HL est inférieur à DE, alors le flag Carry est levé, ce qui provoque le saut qui suit vers l'erreur indiquant qu'il n'y a pas assez de mémoire dans l'espace réservé aux chaînes de caractères.

    Ce qui est important ici, c'est que ce calcul montre que (fretop) doit être supérieur à (stktop). Ce qui montre encore que les schémas sont faux.

    FRE(" ")

    Dernière vérification, histoire d'être vraiment certain, en analysant la fonction FRE(" "). Cette commande avec une chaîne en paramètre n'est pas documentée dans le manuel d'instruction, mais on la trouve dans les « bulletins ».

    Lorsque FRE a un paramètre numérique (peu importe lequel), la fonction renvoie la place restante en mémoire BASIC... pour tout ce qui n'est pas stockage des caractères de chaînes.

    Avec un paramètre alphanumérique, FRE renvoie l'espace restant dans la mémoire réservée aux caractères.

    Et en voici le code :

    inst_fre:    ld       hl,(strend)          ; $38b1
                 ex       de,hl
                 ld       hl,$0000
                 add      hl,sp
                 ld       a,(valtyp)
                 or       a,a
                 jp       z,inst_fre_2
                 call     gstrcu
                 call     call36e3
                 ld       de,(stktop)
                 ld       hl,(fretop)
                 jp       inst_fre_2
    
    inst_fre_2:  ld       a,l
                 sub      a,e
                 ld       c,a
                 ld       a,h
                 sbc      a,d
    

    Tout commence en plaçant (strend) dans DE (via HL). Puis en mettant SP dans HL (via le add).

    (valtyp) contient le type de l'expression qui vient d'être évaluée. À l'appel d'une fonction, il s'agit de la valeur du paramètre. Si cette valeur est numérique (= 0), la suite se passe en inst_fre_2 où le résultat de l'opération HL - DE est mis dans le couple de registres A et C.

    Au passage, notons que la mémoire restante est calculée comme la différence entre le pointeur de pile courant et (strend), ce qui permet de situer correctement la fonction de (strend) comme marquant la fin (adresse haute) du stockage BASIC « listing + variables + tableaux ».

    Si l'expression était alphanumérique, alors il y a deux appels (dont un auquel je n'ai pas encore donné de nom) qui permettent d'appeler le ramasse miettes (Garbage Collection) sur les chaînes de caractères. Passons.

    Puis (stktop) est placé dans DE et (fretop) dans HL, avant d'effectuer le même calcul que précédemment HL - DE.

    Ce qui montre à nouveau que (fretop) doit être supérieur à (stktop) et qui indique même que cet espace correspond à la zone de mémoire libre pour les chaînes. Autrement dit, (fretop) présente l'adresse la plus basse des chaînes de caractères, toutes stockées au-dessus. Le nom a probablement provoqué la confusion de sa description comme étant l'adresse du haut, mais c'est en fait une zone qui croît vers le bas.

    En donc ?

    (fretop) est un pointeur qui est valide entre (memsiz) et (stktop). L'espace entre (memsiz) et (stktop) est déterminé par le premier paramètre de CLEAR et (memsiz) est déterminé par le second paramètre de CLEAR.

    Et grâce à ces informations, nous pouvons cartographier, la mémoire avec ses pointeurs de manière correcte et complète, du moins je l'espère.

    Cartographie Mémoire VG5000µ


  • Récréation 3D, Z80 du VG5000µ ()

    Deux ans déjà que j'avais créé quelques modèles 3D... Le temps passe vite. Et l'envie m'a repris.

    Voici donc une petite recréation du Z80 présent dans le VG5000µ. Fait depuis des images et je ne suis donc pas complètement certains des mesures. J'irai vérifier la prochaine fois que j'en démonte un, si j'y pense.

    Z80 présent dans le VG5000µ

    Update: nouvelle version, corrigée avec des dimensions DIP plus correctes (mais le boitier du SGS est plat... ça fait donc un mélange)

    Z80 présent dans le VG5000µ


« (précédent) Page 12 / 23 (suivant) »

Tous les tags

3d (14), 6809 (1), 8bits (1), Affichage (24), AgonLight (2), Altaïr (1), Amstrad CPC (1), Apple (1), Aquarius (2), ASM (30), Atari (1), Atari 800 (1), Atari ST (2), Automatisation (4), BASIC (31), BASIC-80 (4), C (3), Calculs (1), CDC (1), Clion (1), cmake (1), Commodore (1), Commodore PET (1), CPU (1), Debug (5), Dithering (2), Divers (1), EF9345 (1), Émulation (7), Famicom (1), Forth (3), Game Jam (1), Hector (3), Histoire (1), Hooks (4), i8008 (1), Image (16), Jeu (14), Jeu Vidéo (4), Livre (1), Logo (2), Machine virtuelle (2), Magazine (1), MAME (1), Matra Alice (3), MDLC (7), Micral (2), Motorola (1), MSX (1), Musée (2), Nintendo Switch (1), Nombres (3), Optimisation (1), Outils (3), Pascaline (1), Peertube (1), Photo (2), programmation (4), Python (1), ROM (15), RPUfOS (5), Salon (1), SC-3000 (1), Schéma (5), Synthèse (14), Tortue (1), Triceraprog (1), VG5000 (62), VIC-20 (1), Vidéo (1), Z80 (20), z88dk (1)

Les derniers articles

Family BASIC, le BASIC sur Famicom
Instance Peertube pour Triceraprog
Environnement de développement pour Picthorix
Un jeu en Forth pour Hector HRX : Picthorix
Yeno SC-3000 et condensateurs
Suite de tests pour VG5000µ
Un peu d'Atari ST
Le Forth sur Hector HRX
J'MSX 24 et un micro jeu
Récréation 3D, Matra Alice

Atom Feed

Réseaux