Site logo

Triceraprog
La programmation depuis le Crétacé

  • VG5000µ, nombres aléatoires ()

    Comment donc les nombres aléatoires sont-ils générés sur un VG5000µ. C'est ce que je vous propose de suivre aujourd'hui en décortiquant le code.

    Afin de suivre, il est important de comprendre comment les nombres sont stockés sur VG5000µ, et je vous propose pour cela un petit détour par cet article.

    Petit rappel avant de commencer : un générateur de nombres aléatoires est une procédure qui émet une suite de nombres sur un intervalle, cette suite tentant d'avoir des propriétés intéressantes qui donnent l'illusion de l'aléatoire. La suite est cependant parfaitement définie, même si pas toujours simple à suivre, et c'est ce que nous allons voir par la suite.

    L'initialisation

    Tout commence très tôt pour le générateur de nombres aléatoires. Dès l’initialisation de la machine, une série de valeurs est copiée depuis la ROM vers les variables systèmes. Cela se passe en $1071, juste après l'initialisation de l'affichage.

                 ld       hl,initvalues
                 ld       bc,$0065
                 ld       de,ramlow
                 ldir
    

    Il y a donc 101 valeurs ($65) copiées depuis initvalues ($1194) vers ramlow ($4830). Parmi celles-ci, les suivantes sont copiés vers $4844 et nous intéressent aujourd'hui.

                 defb     $00,$00,$00
                 defb     $35,$4a,$ca,$99
                 defb     $39,$1c,$76,$98
                 defb     $22,$95,$b3,$98
                 defb     $0a,$dd,$47,$98
                 defb     $53,$d1,$99,$99
                 defb     $0a,$1a,$9f,$98
                 defb     $65,$bc,$cd,$98
                 defb     $d6,$77,$3e,$98
                 defb     $52,$c7,$4f,$80
    

    Les trois premiers octets sont les trois index avec lesquels le générateur va jouer. Nous les appellerons les trois seeds. Suivent 8 nombres plutôt grands, positifs et négatifs (le premier vaut -26514538). Et enfin vient le nombre d'origine, qui vaut 0.8116351366043091, que vous pouvez retrouver sous sa forme arrondie en tapant PRINT RND(0) dès l'allumage du VG5000µ.

    Tête sur VG5000µ

    Cette table n'est pas la seule qui va être utilisée par le générateur. Il en existe une autre, qui sera utilisée depuis la ROM, en $093d, d'une longueur de 3.

                 defb     $68,$b1,$46,$68
                 defb     $99,$e9,$92,$69
                 defb     $10,$d1,$75,$68
    

    L'instruction RND

    L'instruction RND commence en $090d par un petit préambule qui vérifie l'argument passé à la fonction. Cet argument est disponible dans FAC, l'accumulateur flottant (voir précédemment)

    inst_rnd:    rst      getsign
                 ld       hl,rnd_seed_2
                 jp       m,reseed
                 ld       hl,rnd_gen
                 call     hl_to_fac
                 ld       hl,rnd_seed_2
                 ret      z
    

    Ce préambule teste en premier lieu le signe de l'argument. S'il est négatif, la routine branche vers reseed, que nous verrons plus loin. C'est au passage un comportement qui n'est indiqué ni dans le manuel d'utilisation du VG5000µ, ni dans les « Clés pour VG5000 », qui donnent de fausses informations (je vous laisse regarder).

    Dans le cas du branchement, HL point vers la seed 2 (et je ne vois aucun intérêt à ce que cela ne soit pas fait après le branchement...)

    Si l'argument est nul ou positif, alors la nombre pointé par HL, qui est la variable système du dernier nombre généré ,est copié dans FAC. Souvenez-vous, c'est à cette adresse qu'a été placé à l'initialisation le nombre 0.8116351.

    On refait pointer HL vers la seed 2 puis, si l’argument était 0, on sort de la routine immédiatement (le flag Zéro a été conservé depuis rst getsign).

    Le calcul

    Puisqu'on est à présent dans le cas où l’argument est positif, il convient de générer un nouveau nombre. Ce nouveau nombre est basé sur, d'une part, le nombre généré précédemment, et d'autre part, les trois index qui avaient été initialisés à zéro (voir ci-dessus).

    Première étape
                 add      a,(hl)
                 and      a,$07
                 ld       b,$00
                 ld       (hl),a
                 inc      hl
    
                 add      a,a
                 add      a,a
                 ld       c,a
                 add      hl,bc
                 call     hl_to_bcde
                 call     fp_mul
    

    La première section du code précédent récupère l'index seed 2 en l'incrémentant de 1. En effet, A contient 1 depuis l'appel à getsign. Le résultat est pris modulo 8 et remonté en RAM.

    Au passage, B est initialisé à 0 pour que BC puisse servir d'index, et HL va pointer un cran plus loin, sur le début de la table des coefficients initialisés au boot (la table des 8 valeurs).

    La seconde section calcul le pointeur dans cette table en quadruplant A, qui est l'index, en format BC comme index à ajouter au pointeur de base HL.

    L'appel hl_to_bcde copie le nombre pointé dans la table vers BCDE, puis l'appel à fp_mul effectue la multiplication avec le contenu de FAC.

    Résumé : cette première étape est donc une multiplication du précédent nombre généré par un autre nombre, fixe, pris dans une table dans 8 valeurs tour à tour.

    Le tout premier appel à RND(1) va multiplier 16129081 ($98 $76 $1c $39) avec0.8116351366043091($80 $4f $c7 $52`).

    Cela donne 13090929 ($98 $47$c0 $71). Cela peut se vérifier dans FAC ($49e6). Attention, le nombre est octet par octet dans le sens inverse à celui que j'utilise ici.

    Seconde étape
                 ld       a,(rnd_seed_1)
                 inc      a
                 and      a,$03
                 ld       b,$00
                 cp       a,$01
                 adc      a,b
                 ld       (rnd_seed_1),a
    
                 ld       hl,rnd_add - 4
                 add      a,a
                 add      a,a
                 ld       c,a
                 add      hl,bc
                 call     fp_add_hl
    afterreseed: call     fac_to_bcde
    

    La seconde étape se divise elle aussi en deux sections.

    Dans la première section, on récupère la seed 1, qui est incrémentée de 1 et modulo 4. Cependant, la valeur 0 est interdite. Par une comparaison avec 1 et un ajout à 0 (via B) avec retenue, si l'index était à 0, alors il est poussé à 1.

    C'est donc en fait un index modulo 3 que l'on obtient.

    Et cet index forme un pointeur via HL de manière similaire à l'étape précédente, dans la table de trois valeurs de la ROM mentionné au début de l'article.

    Cette valeur est alors ajoutée à FAC. L'appel à fp_add_hl se charge de l'étape intermédiaire de chargement de la valeur dans BCDE. Puis le résultat est ramené dans BCDE.

    Le label est un branchement venant du reseed que nous verrons plus loin.

    Résumé : cette seconde étape est une addition du nombre obtenue à la première étape avec un des trois nombres pris dans la deuxième table, pris tour à tour.

    Le tout premier appel additionne 13090929 ($98 $47 $c0 $71 ) avec 4.626181e-08 ($68 $b1 $46 $68). Ce second nombre est bien trop petit par rapport au premier. Cette addition ne change rien... dans ce cas-ci. Nous verrons plus tard à quoi cette addition peut servir.

    Troisième étape

                 ld       a,e
                 ld       e,c
                 xor      a,$4f
                 ld       c,a
    

    Dans cette troisième étape, le générateur fait des mélanges. Les 8 bits de poids les plus faibles sont mis de côté et les 8 bits de poids fort sont placés dans les 8 bits de poids faible.

    Les 8 bits mis de côté sont XORés avec b01001111. Ce qui signifie que certains bits sont inversés. Puis le résultat est placé dans les 8 bits de poids fort.

    Cette opération ne me semble pas avoir de sens arithmétique. Cela semble être juste un mélange. Peut-être pour amener de l'entropie dans les bits de poids faible... Peut-être.

    Résumé : cet étape mélange les parties de la mantisse et change quelques bits.

    Le nombre est à présent 12501063 ($98 $3e $c0 $47).

    Étape intermédiaire

                 ld       (hl),$80
                 dec      hl
                 ld       b,(hl)
                 ld       (hl),$80
    

    Cette étape profite que HL pointe actuellement (depuis la récupération de FAC dans BCDE) sur l'octet après FAC pour préparer le terrain pour plus tard. Cet octet contient le complément à 1 du bit de signe du nombre de FAC. En mettant cet octet à $80, on force la valeur à être positive.

    HL pointe ensuite sur l'octet précédent, l'exposant, et met celui-ci à $80, c'est-à-dire $2^ 0$ (voir l'article sur les nombres, toujours).

    Résumé : le nombre final sera un nombre positif et l'exposant est fixé à $2^0 = 1$.

    Pas d’influence, pour le moment sur le nombre tenu dans BCDE.

    Quatrième étape*

                 ld       hl,rnd_seed_0
                 inc      (hl)
                 ld       a,(hl)
    
                 sub      a,$ab
                 jr       nz,rnd_cnt
                 ld       (hl),a
                 inc      c
                 dec      d
                 inc      e
    

    C'est la dernière étape du calcul. Dans la première partie, la seed 0 est incrémentée et récupérée dans A.

    Si la soustraction par 171 ($ab) n'est pas nulle, on branche plus loin à l'étape finale. Sinon, le résultat (0) est replacé dans la seed 0. C'est donc un compteur jusqu'à 171 qui, lorsqu'il atteint cette valeur, modifie légèrement la mantisse.

    Le premier et le troisième octets de la mantisse sont incrémentés, celui du milieu décrémenté, sans se soucier de débordements éventuels.

    Résumé : une fois tous les 171 tirages, la mantisse est modifiée légèrement.

    Comme ici, c'est le premier tirage, il ne se passe rien.

    Étape finale

    rnd_cnt:     call     bcde_norm
                 ld       hl,rnd_gen
                 jp       cpy_faclsb_hl
    

    La mantisse a été générée, l'exposant est . Mais tout nombre dans FAC en sorti de routine doit être normalisé.

    Comme indiqué dans l'article précédent sur la représentation des nombres, cela signifie que la mantisse et l'exposant vont être modifiée afin d'obtenir une mantisse avec un premier bit à 1 implicite, lui-même remplacé par le bit de signe.

    MAIS ! Il y a un twist ! La routine bcde_norm n'attend pas en entrée un nombre BCDE, mais une mantisse 32 bits CDEB. L'exposant du nombre actuel va donc se retrouver... en partie la moins significative du nombre, afin de nourrir, en quelque sorte, la partie droite de la mantisse lors de l'éventuel décalage vers la gauche.

    C'est peut-être un peu obscure : je donne un exemple dans le résumé.

    En sortie de normalisation, le nombre est bien dans FAC. Le contenu de FAC est alors copié à l'emplacement du dernier nombre généré.

    C'est terminé !

    Résumé : mise en forme du nombre, à la fois dans FAC comme résultat de la fonction, et de côté pour servir de base au prochain nombre généré (ou pour être retourné en case de RND(0)).

    Nous en étions à $98 $3e $c0 $47. Mais la normalisation s'attend à une mantisse 32 bits, et c'est donc comme ça que va être perçue la mantisse : $3e $c0 $47 $98.

    La normalisation doit déplacer la mantisse à gauche jusqu'à ce que le bit de poids fort soit à 1. Il va falloir deux étapes pour cela :

    • de $3ec04798 à $7d808f30, puis
    • de $7d808f30 à $fb011e60

    Comme le bit de poids fort du dernier octet n'est pas à 1, il n'y a pas d'arrondi. Cet octet est abandonné, le bit de signe et l'exposant corrigé par le nombre d'étapes ($80 - 2 donne $7e).

    Au final, nous avons obtenu : $7e $7b $01 $1e, soit 0.245121

    Tête sur VG5000µ

    Suppléments

    Re-seed

    Si l'argument de RND() est négatif, alors un branchement a lieu sur une routine de réinitialisation du générateur.

    reseed:      ld       (hl),a
                 dec      hl
                 ld       (hl),a
                 dec      hl
                 ld       (hl),a
                 jr       afterreseed
    

    À l'arrivée dans cette partie, HL pointe sur seed 2 et A est égal à $FF. Ce qui a pour résultat de mettre les trois octets à $FF.

    Le branchement ramène dans le générateur à la fin de la seconde étape, c'est-à-dire après la multiplication et l'addition. Ce qui est dans FAC (l'argument négatif de RND()) est ramené dans BCDE et le reste des étapes est effectué.

    C'est donc une nouvelle séquence qui démarre, dépendante de l'argument passé à RND().

    Cas de l'addition

    Revenons sur la seconde étape, l'addition avec un nombre tout petit. Dans l'exemple que nous avons suivi pendant l'article, l'addition ne servait à rien, car la différence entre les exposants était trop grand et donc le nombre à addition non significatif.

    La question à se poser est donc : dans quels cas ces nombres deviennent-ils significatifs ?

    Le plus petit d'entre eux est : $68 $46 $b1 $68 qui a pour exposant $68. C'est-à-dire $80 - 24.

    Il faut donc un nombre strictement inférieur à $00 $00 $00 $80 (0.5) pour que l'addition soit intéressante.

    Sauf que... juste avant l'addition, la multiplication a été faite avec un nombre dont l'exposant était au minimum $98. Puisque dans une multiplication, les exposants s'ajoutent, cela implique que seuls des nombres avec un exposants à '$68' initialement vont être assez petits après la multiplication et être modifiés par l'addition.

    C'est quelque chose de facile à déclencher en modifiant à la main le dernier nombre généré en mémoire. Mais est-ce que cela se passe si on laisse le générateur se dérouler normalement ?

    Un expérience simple avec un debuggeur en mettant un point d'arrêt dans le code d'addition et en faisant tourner le générateur montre que... non. Cela n'arrive pas. Ou alors assez rarement pour résister à l'expérience.

    Il est temps de se poser la question des bornes maximales et minimales des nombres générés.

    Mais vu la longueur de l'article, ce sera pour le prochain...


  • VG5000µ, jouer avec les nombres ()

    Suite à l'article précédent, j'ai mis sur le dépôt GitHub un petit utilitaire Python qui reproduit les conversions entre la valeur du nombre et son codage en 4 octets.

    Parfois, voir du code est plus simple qu'un long discours.

    Et parce qu'on ne sait jamais trop quel sera la vie future du dépôt, voici le code des deux principales fonctions de l'outil.

    import math
    
    
    def get_byte(number):
        """Takes the current number, and returns the next byte encoding it with the reminder of the number to encode. """
    
        number *= 256
        result = int(number)
        return result, (number - result)
    
    
    def encode(number):
        """Gets a number, returns it's encoded four bytes (memory layout, so exponent at the end)."""
    
        # If the number is zero, the encoding is immediate.
        # In fact, only the exponent has to be 0.
        if number == 0:
            return [0, 0, 0, 0]
    
        # Gets the sign from the number for later encoding
        sign = 0x80 if number < 0 else 0
    
        # We encode only positive numbers
        number = abs(number)
    
        # Shift the number so that the first fractional part bit
        # of the mantissa is 1 (0.1 binary is 0.5 decimal)
        exp = 0
        while number >= 0.5:
            number /= 2
            exp += 1
        while number < 0.5:
            number *= 2
            exp -= 1
    
        # Gets the three bytes encoding the mantissa
        o1, number = get_byte(number)
        o2, number = get_byte(number)
        o3, number = get_byte(number)
    
        # Clears the most significant bit
        # and replace it by the sign bit
        o1 &= 0x7F
        o1 |= sign
    
        # Encode exponent
        exp += 128
    
        # Returns an array (Z80 memory layout)
        return [o3, o2, o1, exp]
    
    
    def decode(encoded):
        """ Takes four encoded bytes in the memory layout, and returns the decoded value. """
    
        # Gets the exponent
        exp = encoded[3]
    
        # If it's 0, we're done. The value is 0.
        if exp == 0:
            return 0
    
        # Extract value from the exponent
        exp -= 128
    
        # Extract the sign bit from MSB
        sign = encoded[2] & 0x80
    
        # Sets the most significant bit implied 1 in the mantissa
        encoded[2] = encoded[2] | 0x80
    
        # Reconstruct the mantissa
        mantissa = encoded[2]
        mantissa *= 256
        mantissa += encoded[1]
        mantissa *= 256
        mantissa += encoded[0]
    
        # Divide the number by the mantissa, corrected
        # by the 24 bits we just shifted while reconstructing it
        mantissa /= math.pow(2, 24 - exp)
    
        # Apply the sign to the whole value
        if sign:
            mantissa = -mantissa
    
        return mantissa
    

  • VG5000µ, traitement des nombres ()

    Lors d'une discussion sur le forum system-cfg à propos de la fonction RND une question a été posée sur le format des nombres dans le VG5000µ. C'est une question qui revient et que je voulais documenter pour mémoire, me posant régulièrement la question et oubliant juste après...

    Différents formats

    Distinguons déjà deux choses : les nombres manipulés par le système, et les nombres manipulés par le BASIC. Les premiers sont de diverses formes en fonction des besoins, de type entier, signé ou pas, sur 8 ou 16 bits la plupart du temps. Il n'y a pas grand chose à dire sur eux.

    Les seconds sont ceux manipulés par le BASIC, qui est un BASIC Microsoft sur VG5000µ, et ce qui sera valide dans cet article le sera pour d'autres machines avec BASIC Microsoft et un processeur Z80. Au moins dans les grandes lignes, mais pour ce que j'ai vu en comparant une paire d'entre eux, souvent jusque dans les détails. Ces BASIC ne différent que de petites modifications ici ou là, de l'ordre de l'optimisation.

    Le BASIC lui-même traite plusieurs types de nombres. On a déjà vu la manière dont il traitant de manière particulière les numéros de lignes après un GOTO ou un GOSUB dans l'article sur l'arrangement des lignes.

    Le format qui nous intéresse aujourd'hui est celui utilisé pour les calculs, ainsi que celui utilisé dans les variables numériques. Dans l'article sur les variables, j'étudiais comment les variables étaient créées, avec leur nom suivis de 4 octets. Mais que contiennent ces 4 octets ?

    Le format flottant

    Tous les nombres traités par le BASIC VG5000µ pour les calculs et les variables sont dans un format a virgule flottante. C'était déjà le cas avec le BASIC créé initialement à Dartmouth. L'idée était que les utilisateurs n'avaient pas à se poser de question sur le format interne, et pouvaient utiliser les nombres naturellement.

    Plus tard, pour des raisons de performance (vitesse et consommation mémoire), des versions de BASIC ont ajouté un typage sur ces nombres. Comme le suffit % des variables numériques qui indiquent que la variable contient un nombre entier.

    Sur le VG5000µ, pas de typage de nombre. Tout est au même format. Je nommerai ce format le format BCDE par la suite. BCDE car il est manipulé la plupart du temps à travers cette paire de registres (BC et DE). BCDE désigne donc aussi l'emplacement du nombre traité dans certaines situations.

    Pour les calculs, le BASIC utilise un accumulateur flottant, que je nommerai FAC par la suite (Floating point ACumulator). L'essentiel du format est le même que lorsqu'il est au format BCDE.

    L'accumulateur FAC est l'endroit où se situe « le nombre en cours ». À la sortie d'une fonction, le résultat s'y trouve et est donc disponible pour les calculs ou fonctions suivants.

    Ainsi, dans une expression comme INT(4 * 0.2), le FAC va d'abord contenir 4, puis après la multiplication 0.8, puis après INT(), contiendra 0. Des instructions comme PRINT ou LET (explicite ou implicite), iront chercher cette valeur pour la traiter.

    Les mouvements

    Puisque BCDE et FAC sont étroitement liés, il existe des fonctions pour transférer le contenu de BCDE vers FAC (\$05d2) et inversement (\$05dd). Il est possible aussi de récupérer dans BCDE un nombre pointé par HL (\$05e0), et de monter dans FAC un nombre pointé par HL (\$05cf), en utilisant BCDE au passage.

    Le format

    Voici en premier lieu un tableau auquel je vais me référer pour expliquer le format.

    B C D E
    Exp. S|MSB Milieu LSB
    $49e9 $49e8 $49e7 $49e6
    • Exp. est l'exposant du nombre (la puissance de 2 par laquelle est multipliée la mantisse).
    • MSB (Most Significant Byte), Milieu et LSB (Least Significant Byte) forment la mantisse.
    • S est le bit de signe et se trouve en bit 7 du MSB.
    • Les adresses représentent les positions dans FAC l'accumulateur flottant.

    Attention : les adresses indiquées partent de la plus haute à la place basse. Lorsque vous inspectez la mémoire octet par octet, que ce soit dans FAC ou pour une valeur de variable, le nombre est donc dans l'autre sens EDCB, avec l'exposant en dernière position.

    La mantisse

    La mantisse couvre donc 23 bits. Le 24ième bit, le plus significatif, est implicitement à 1. Dans le format codé, il est donc remplacé par un bit de signe. Un bit à 1 indique un nombre négatif, positif sinon.

    La mantisse est lue comme 0.1xxxxxx xxxxxxxx xxxxxxxx en binaire. Les x étant pris dans les 23 bits formés par MSB, Milieu et LSB.

    L'exposant

    L'exposant est centré sur 128 (ou $80 en hexadécimal). Cela signifie que 128 est l'exposant nul. Pour 129 l'exposant est 1, 127, et pour l'exposant est -1.

    La valeur 0 ($00) pour l'exposant est spéciale : elle représente le nombre 0. Peu importe les autres octets, la valeur est nulle. Dans certains codage de nombre flottants, cet exposant zéro est utilisé pour représenter des valeurs particulières qui viennent compléter celles accessibles depuis un exposant non nul. Ici, ce n'est pas le cas. Un exposant à 0 signifie le nombre 0.

    La valeur du nombre

    La valeur du nombre représenté est égal à : $-1 . signe . 2^{exp.} . mantisse$.

    Ceci mérite quelques exemples.

    Exemple 1 : 0, codé 00 xx xx xx. Comme je l'ai écrit ci-dessus, peut importe la valeur des autres octets. Si l'exposant est à 0, c'est le nombre 0.

    Exemple 2 : 1, codé 81 00 00 00. En effet, 1 est égal à $b0.1 . 2^1$ (je préfixe les nombres en binaire par b, pour faire la différence avec les nombres en base 10 que je ne préfixe pas).

    L'exposant est donc 1, codé en 128 + 1 = 129, \$81 en hexadécimal. La mantisse est 'b0.1', codé en 00 00 00 puisque le premier 1 est implicite dans le codage.

    Example 3 : 2, codé 82 00 00 00. Sur le même principe, 2 est égal à $b0.1 . 2^2$. Donc exposant 2 donne \$82 et la mantisse est là aussi 0 puisque b0.1 est implicite.

    Exemple 4 : -2, codé 82 80 00 00. Le code est similaire à celui de 2, mais le bit de signe négatif est placé sur le 7ième bit de l'octet le plus significatif de la mantisse.

    Exemple 5 : 0.1', code ``7d 4c cc cc. Là, c'est un peu plus compliqué. 0.1 n'est pas une valeur qui tombe juste en binaire. Puisque la précision est limitée, il faut bien s'arrêter quelque part ; il faut bien comprendre que le nombre 'retenu' n'est pas vraiment 0.1, juste un nombre approchant.

    Le nombre s'écrit $2^{-3} . b0.110011001100110011001100$... ; 128 - 3 donne \$7d en hex. Une fois enlevé le premier bit de la mantisse, le reste s'écrit comme indiqué.

    Aparté 1

    Il est possible d'écrire

    LET A = 0.1
    PRINT A
    

    Et le nombre affiché sera bien 0.1. Pourtant, ce n'est pas le nombre encodé, qui est plus quelque chose comme : 0.09999999403953552

    Mais ce nombre est arrondi lors de l'affichage.

    Aparté 2

    À noter aussi que chaque opération effectuée par le BASIC, qui en interne utilise 32 bits, se termine par un encodage du nombre, qui est arrondi aux 24 bits disponibles. Si les calculs ont besoins de plus de 24 bits significatifs, alors les résultats ne seront pas exacts. Il faut se méfier des nombres flottants.

    Aparté 3

    Rappelez-vous qu'en lisant la mémoire octet par octet, les nombres codés vont apparaître dans l'autre sens. Ainsi, si vous assignez à une variable le nombre 1 et que vous regardez dans la section de variable la valeur suivant le nom, vous y verrez 00 00 00 81.

    Aparté 4

    En examinant la mémoire de FAC et en traçant les opérations, vous pourrez constater que l'octet suivant (\$49ea) bouge aussi. Il s'agit d'un octet utilisé pour stocker le signe des opérations. En effet, afin de faire des opérations sur les mantisses, il faut en extraire le bit de signe et y remettre le bit implicite. Un bit de signe est stocké à cet endroit (attention, il s'agit de son complément).

    Une opération

    Pour se faire une idée de comment sont traités les nombres, je vous invite à suivre le processus d'addition.

    Tout commence en $0705 par la récupération sur la pile du nombre qui vient d'être décodé depuis la ligne du BASIC. Dans FAC est déjà positionnée l'opérande précédente de l'opération.

    eval_add:    pop      bc
                 pop      de
                 jp       fp_bcde_add
    

    La nouvelle opérande est maintenance dans BCDE dans le format expliqué dans les paragraphes précédents. Il faut donc maintenant ajouter BCDE et FAC.

    En $0310, la routine commence par un test. Si l'exposant, qui est initialement dans B, est égal à 0, on peut s’arrêter là. Ajouter 0 à FAC signifie ne pas le modifier. Le retour est immédiat :

    fp_bcde_add: ld       a,b
                 or       a,a
                 ret      z
    

    Deuxième cas simple, si la valeur de l'exposant de FAC est 0, le résultat est le nombre présent dans BCDE, il suffit donc de le transférer dans FAC et c'est terminé :

                 ld       a,(fac_exp)
                 or       a,a
                 jp       z,bcde_to_fac
    

    L'étape suivante est de s'assurer que le nombre dans FAC a un exposant plus grand que celui de BCDE. Si ça le cas, c'est parfait, sinon, on échange les deux nombres. L'opération utilise la pile temporairement. A qui contient la différence entre les deux exposants, prend la valeur de son opposée, pour rester cohérente.

                 sub      a,b
                 jr       nc,no_swap
                 cpl
                 inc      a
                 ex       de,hl
                 call     fac_to_stck
                 ex       de,hl
                 call     bcde_to_fac
                 pop      bc
                 pop      de
    

    À ce niveau, on a :

    • dans FAC le nombre dont l'exposant est le plus grand,
    • dans BCDE, le nombre dont l'exposant est le plus petit,
    • dans A, la différence entre ces deux exposants.

    Si le nombre d'exposant le plus petit est insignifiant par rapport au plus grand, on peut s'arrêter là, le résultat de l'addition sera le plus grand des grands nombres. Puisque les nombres sont codés sur 24 bits significatifs, si la différences entre les exposants est de 25 ou plus, on peut sortir de la fonction :

    no_swap:     cp       a,$19
                 ret      nc
    

    À présent que les nombres sont en place et qu'il est intéressant de les additionner, il faut les préparer. Pour le moment, le bit de signe est toujours codé dans les deux nombres. Ils sont aussi potentiellement à des exposants différents. Deux raisons pour lesquelles on ne peut pas additionner les mantisses pour le moment.

    La première opération est d'extraire les signes et de renvoyer une indication sur l'opération à faire. Plus d'explication sur cette indication juste après.

    Puis d'aligner les deux mantisses sur l'exposant le plus grand.

                 push     af
                 call     ext_sign
                 ld       h,a
                 pop      af
                 call     div_mant
    

    Je n'entre pas dans la routine ext_sign, mais voici ce qu'elle fait. Elle prend le bit 7 de l'octet le plus significatif de FAC et le remplace par le 1 implicite de la mantisse codée. Le bit de signe est inversé et est mis de côté, dans l'octet suivant l'exposant de FAC.

    Puis la même opération est faite sur BCDE : extraction de signe, remplacement par 1. Le signe n'est pas mis de côté, mais est utilisé pour renvoyer dans A une indication : 0 si les signes étaient identiques, 1 s'ils étaient opposés.

    Cette indication, qui est sauvée dans H pour pouvoir récupérer la valeur de AF sauvée sur la pile (A contient la différence entre les exposants), servira bientôt.

    L'opération div_mant décale vers la droite la mantisse de BCDE d'autant de positions qu'indiquées par A. Attention : en sortie de cette fonction, la mantisse est disponible sur CDEB.

    En effet, l'exposant n'est plus nécessaire, puisqu'il est identique à celui de FAC. On peut donc réutiliser B pour récupérer les bits résultants du décalage à droite et éviter de perdre trop de précision. La mantisse est temporairement sur 32 bits (même si seuls 24 sont toujours significatifs, puisque issus du nombre initial).

    Il reste donc à passer à l'addition... enfin presque.

    Dans la partie suivante, on récupère dans A l'indicateur donné précédemment par les signes. Si les signes étaient identiques, on passe à la suite. S'ils étaient différents, c'est une soustraction qui sera faite, en allant vers min_bcde.

                 ld       a,h
                 or       a,a
                 ld       hl,fac_lsb
                 jp       p,min_bcde
    

    Pourquoi cette différence de traitement en fonction des signes ?

    • Si les signes des deux nombres sont positifs, il faut additionner les mantisses et le nombre sera positif.
    • Si les signes des deux nombres sont négatifs, on peut aussi additionner les mantisses comme des nombres positifs, et pendre l'opposée du tout. $(-a) + (-b)$ est la même chose que $-(a+b)$.
    • Si les signes des deux nombres sont opposés, on peut soustraire les mantisses et corriger le signe du résultat. En effet $a + (-b)$ est la même chose que $(a - b)$ et $(-a + b)$ est la même chose que $-(a-b)$.

    Le cas de l'addition des mantisses est le suivant :

                 call     add_bcde
                 jr       nc,round
                 inc      hl
                 inc      (hl)
                 jp       z,overflow
                 ld       l,$01
                 call     shft_right
                 jr       round
    

    Après l'appel à add_bcde qui ajoute la mantisse CDE avec celle de FAC octet par octet, un test est fait sur la retenue. Si l'addition n'a pas générée de retenue, alors on va vers la routine d'arrondi de FAC, qui terminera l'opération.

    S'il y a eu une retenue, il faut augmenter l'exposant de 1, ce qui est fait en pointant HL un cran plus loin que la mantisse et augmentant la valeur de l'exposant qui s'y trouve. Si cette incrémentation a ramené l'exposant à 0, c'est qu'il était déjà à son maximum. Le nombre est trop grand, une erreur de dépassement de capacité est lancée.

    Sinon, il faut corriger la mantisse avec un décalage vers la droite de 1 bit (paramètre indiqué par L) puis brancher vers la routine d'arrondi.

    Le cas de la soustraction des mantisses est le suivant :

    min_bcde:    xor      a,a
                 sub      a,b
                 ld       b,a
                 ld       a,(hl)
                 sbc      a,e
                 ld       e,a
                 inc      hl
                 ld       a,(hl)
                 sbc      a,d
                 ld       d,a
                 inc      hl
                 ld       a,(hl)
                 sbc      a,c
                 ld       c,a
                 call     c,compl2
    

    Toute la première partie, jusqu'au call, est la soustraction de la mantisse de 'FAC' par la mantisse 'CDEB' octet par octet. Le résultat se trouve dans 'CDE'. L'exposant du nombre final est toujours celui de FAC.

    Si le résultat de la soustraction est positif, alors il ne se passe rien de plus. Si une retenue a eue lieu pendant la soustraction, alors on prend son opposée en appelant compl2, afin de rendre la mantisse positive. Cette routine inverse aussi le bit de signe qui avait était extrait dans l'octet suivant FAC.

    La routine qui suit dans le code étant la normalisation et arrondi d'un nombre, pas besoin de brancher, c'est terminée.

    Oui mais le signe ?

    Les opérations pour obtenir le signe du résultat final ne sont pas forcément évidente à suivre. Voici ce qu'il se passe :

    • Dans le cas où on a utilisé l'addition des mantisses, le signe de FAC a été extrait de la première opérande.
      • Dans le cas $a + b$, $a$ était positif, et le résultat est positif.
      • Dans le cas $-a-b$ qui est devenu $-(a+b)$, $a$ était négatif, et ce signe négatif sera hérité par le résultat. Transformant $a+b$ en $-(a+b)$.
    • Dans le cas où on a utilisé la soustraction des mantisses, là encore, le signe de FAC a été extrait de la première opérande.
      • Dans le cas $a - b$, le signe du résultat dépend de cette opération. Si $a > b$, compl2 n'a pas été appelé et le résultat est positif. Dans le cas contraire, compl2 a changé le bit de signe et le résultat est bien négatif, puisque la mantisse est positive.
      • Dans le cas $-a+b$, qui a été exécutée comme $-(a-b)$, initialement, le signe de FAC portait l'indication négatif, et le résultat de la soustraction l'aura, en fonction des deux cas, laissé comme tel, ou inversé.

    Re-codage final

    La routine qui suit, de normalisation, s'assure que le bit de poids fort est toujours 1 (si ce n'est pas possible, c'est que le nombre est 0) et ajuste l'exposant en conséquence, puis arrondi le nombre. L'arrondi peut générer un dépassement de capacité.

    L'addition n'a pas besoin de normalisation. Le résultat de l'addition des mantisses assure toujours que le bit de poids fort est 1 (au moins l'un des deux nombres était normalisé avec un bit de poids fort à 1, et si les deux l'étaient, alors cela a provoqué l'augmentation de l'exposant et le bit de poids fort est toujours 1, la retenu de 1 + 1 en binaire).

    Le résultat de l'addition est à présent dans FAC, place à la suite des opérations...


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

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

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

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

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

    Tortue Jeulin T2 Tortue Jeulin T2


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

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

    Routines en sorties

    Voici les trois premières :

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

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

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

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

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

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

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

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

    Précautions

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

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

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

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

    Routine en entrée

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

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

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

    Le manque d'idée...

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

    Installation

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

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

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

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

    Comptage

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

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

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

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

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

    On restaure HL et on revient. Vraiment simple.

    L'affichage des compteurs

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

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

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

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

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

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

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

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

    BASIC

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

    10 CLEAR 50,&"79FF"
    20 S=&"7A00"
    30 READ A$
    40 IF A$="FIN" THEN END
    50 A$="&"+CHR$(34)+A$+CHR$(34):A=VAL(A$)
    60 POKE S,A
    70 S=S+1
    80 GOTO 30
    300 DATA F5,E5,21,37,7A,CD,AA,36,21,6D,7A,CD,26,7,3E,C3,32,DF,47,32
    310 DATA E2,47,32,E5,47,32,EB,47,21,45,7A,22,E0,47,21,4F,7A,22,E3,47
    320 DATA 21,59,7A,22,E6,47,21,63,7A,22,EC,47,E1,F1,C9,43,41,4C,4C,20
    330 DATA 0,0,0,0,0,0,0,0,0,E5,2A,3D,7A,23,22,3D,7A,E1,C9,E5
    340 DATA 2A,3F,7A,23,22,3F,7A,E1,C9,E5,2A,41,7A,23,22,41,7A,E1,C9,E5
    350 DATA 2A,43,7A,23,22,43,7A,E1,C9,E5,C5,D5,F5,2A,5,48,E5,21,20,0
    360 DATA 22,5,48,2A,3D,7A,CD,26,7,21,20,1,22,5,48,2A,3F,7A,CD,26
    370 DATA 7,21,20,2,22,5,48,2A,41,7A,CD,26,7,21,20,3,22,5,48,2A
    380 DATA 43,7A,CD,26,7,E1,22,5,48,F1,D1,C1,E1,C9
    1000 DATA FIN
    RUN
    CALL &"7A00"
    

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

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

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

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

    Le résultat

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

    Un CALL avec paramètres


« (précédent) Page 10 / 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