Site logo

Triceraprog
La programmation depuis le Crétacé

  • 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


  • VG5000µ, les hooks d'appel ()

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

    Le hook CALL

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

    Le code de cette instruction est extrêmement simple :

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

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

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

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

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

    Récupérer des paramètres

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

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

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

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

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

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

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

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

    Le hook RST

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

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

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

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

    Les différents RST

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

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

    Un RST libre

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

    usrrst:      jp       rsthk
    

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

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

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

    Le résultat

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

    Un CALL avec paramètres


« (précédent) Page 10 / 22 (suivant) »

Tous les tags

3d (14), 6809 (1), 8bits (1), Affichage (24), AgonLight (2), Alice (1), 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 (2), MDLC (7), Micral (2), Motorola (1), MSX (1), Musée (2), Nintendo Switch (1), Nombres (3), Optimisation (1), Outils (3), Pascaline (1), Photo (2), Programmation (3), Python (1), ROM (15), RPUfOS (5), Salon (1), SC-3000 (1), Schéma (5), Synthèse (14), Tortue (1), VG5000 (62), VIC-20 (1), Z80 (20), z88dk (1)

Les derniers articles

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
Notes sur le Motorola 6809

Atom Feed

Réseaux