Site logo

Triceraprog
La programmation depuis le Crétacé

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


  • Bonne Année 2019 ! ()

    Bonjour à tous. Cette année, j'espère pouvoir faire aboutir un projet autour du VG5000µ que j'ai commencé il y a longtemps et que j'avance petit pas par petit pas.

    En attendant, je vous souhaite :

    10 B$="AAHGGGGGCB00A@GGGGCBCB00998898=<10"
    20 LB=LEN(B$)
    30 FOR I=1 TO LB STEP 2
    40 A=ASC(MID$(B$,I,1))+ASC(MID$(B$,I+1,1))-64
    50 PRINT CHR$(A);
    60 NEXT I
    

    À essayer sur votre ancienne machine sous BASIC préférée. Ça devrait être portable sur à peu près toutes les machines avec interpréteur BASIC et des capactités par trop limitées sur les chaînes de caractères (adieu ZX81...). Sur certains, il faudra ajouter LET aux lignes 10, 20 et 40 pour les assignations de variable.


« (précédent) Page 12 / 22 (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 (30), 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), 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 (3), 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

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
Tuiles des plus très-curieuses

Atom Feed

Réseaux