Site logo

Triceraprog
La programmation depuis le Crétacé

  • VG5000µ, deux mises à jour sur MAME ()

    Lors du commentaire systématique de la ROM du VG5000µ, je suis arrivé sur les routines de lecture et écriture sur cassette. Comme d'habitude, afin de vérifier le fonctionnement de la machine, je fais des tests. Si je fais parfois des tests sur le matériel réel, la plupart du temps, un test sur émulateur suffit, voire est beaucoup plus simple, permettant de dérouler une routine et la suivre avec des données bien choisies.

    J'utilise essentiellement MAME pour cela, qui est muni d'un debuggeur qui répond à mes attentes. J'utilise parfois dcvg5k, qui est plus orienté sur une utilisation de la machine simple et pratique.

    Cependant, pour la cassette, aucun des deux ne convenait. Les deux émulateurs ne savent lire que le format K7, qui a le mérite d'être extrêmement simple et facilement lisible, mais qui a l'inconvénient d'être inadéquat au bon déroulement des routines de lecture et d'écriture.

    La format K7 est une simple liste des octets décodés depuis un enregistrement réel. C'est un format pratique car très compact. L'émulateur dcvg5k s'en sert pour injecter ou extraire les valeurs en court-circuitant la routine de lecture et d'écriture de la ROM. Si j'ai bien compris, l'émulateur intercepte l'exécution normale à des endroits bien choisis et prend la main pour une lecture ou écriture très rapide.

    C'est une idée qui se défend pour une utilisation de la machine en évitant des temps d'attentes associés aux cassettes.

    Dans mon cas d'étude des routines, c'est par contre hors-jeu. J'ai besoin d'un signal audio. Je me suis donc tourné vers MAME pour voir si je pouvais ajouter le traitement d'un fichier .WAV avec ma ROM 1.1 non modifiée.

    Ajout de lecture/écriture audio

    La première opération a été d'ajouter le support du format .WAV sur MAME.

    Comme on peut l'imaginer, la plupart des services de base existent déjà quelque part dans MAME. C'est l'avantage d'un tel mastodonte. Mais être un mastodonte à aussi un inconvénient : il faut savoir ça se trouve et comment ça s'utilise.

    Une fois trouvé, c'est assez simple. Dans le fichier src/lib/formats/vg5k_cas.cpp, à côté de la déclaration du support du type de fichier K7, il suffit d'ajouter la déclaration du support .WAV. Toute la gestion de fichier et de magnétophone virtuel est alors pris en charge.

    CASSETTE_FORMATLIST_START(vg5k_cassette_formats)
        CASSETTE_FORMAT(vg5k_k7_format)
        CASSETTE_FORMAT(wavfile_format)
    CASSETTE_FORMATLIST_END
    

    Premiers problèmes

    Je me doutais que cela n'allait pas fonctionner directement, puisque la machine était annotée comme ne fonctionnant pas, à cause d'un problème de lecture à 1200 bauds. J'avais comme deuxième objectif de corriger cela, afin de pouvoir analyser les routines complètement.

    Avec la prise en charge du format .WAV, l'émulation réussi à lire un fichier audio sans soucis. Mais méfiance, la routine du VG5000µ se sert de l'amorce du fichier, une suite de signaux haut/bas, pour se calibrer.

    Le deuxième test est donc de sauver un fichier .WAV puis de le recharger. Et là, cela ne fonctionne pas. Le fichier est bien sauvé, mais impossible à relire.

    En examinant le fichier obtenu, je vois que les timings sont complètement farfelus. Ils ne correspondent pas à la théorie décrite dans le manuel technique, ils ne correspondent ni à 1200 bauds ni à 2400 bauds, et ils ne correspondent pas à la comparaison avec des fichiers écrits par du matériel réel.

    Le moteur démarre

    Au passage, les manipulations sous MAME sont pénibles, car la gestion du contrôle du moteur du magnétophone n'est pas là. Là encore, la plus grande partie du temps passé est de comprendre comment MAME gère le système. Au final, tout est là, il suffit de le brancher sur le bon signal envoyé sur le port I/O de la cassette.

    Chose faite en modifiant la fonction void vg5k_state::cassette_w(uint8_t data) de src/mame/drivers/vg5k.cpp et en y ajoutant cette ligne :

    m_cassette->change_state(BIT(data, 1) ? CASSETTE_MOTOR_ENABLED : CASSETTE_MOTOR_DISABLED , CASSETTE_MASK_MOTOR);
    

    J'en profite pour réécrire cette petite fonction pour faire ressortir un peu mieux la sémantique des données envoyées sur le port I/O.

    À présent, les fonctions d'accès à la cassette (CLOAD, CSAVE et les autres) contrôlent le moteur simulé du lecteur dans MAME, est c'est bien plus pratique à utiliser !

    Le Z80 doit attendre

    Retour sur les problèmes de timings. Je vous évite tout le cheminement et les tests quand soudain me vient un doute. Est-ce que l'émulation tourne à la bonne vitesse ? Oui, la fréquence déclarée pour le Z80 est la bonne (même si techniquement, elle devrait découler de celle du VDP), mais cela ne suffit pas !

    En effet, le VG5000µ insère un état WAIT supplémentaire pendant sa phase M1 (fetch). Et ça change tout.

    Un petit détour par le Z80 ici. Lors de la phase M1 (opcode fetch), qui s'opère en 4 cycles d'horloge, le 2 premiers cycles placent l'adresse du PC sur le bus d'adresse puis signalent une requête de lecture mémoire. Lors du cycle 3 (T3), la valeur de l'instruction est lue depuis le bus de données. Les cycles T3 et T4 conjointement servent pour le rafraîchissement des mémoires dynamiques, laissons ça de côté.

    Le Z80 prévoit que le matériel puisse ne pas être prêt à temps pour livrer sur le bus de données l'instruction lue en cycle T3. La ligne WAIT est donc vérifiée à la fin de T2. Si la ligne est validée, alors le Z80 passe en mode WAIT en ajoutant des cycles supplémentaires d'attente, jusqu'à ce que la ligne WAIT soit relâchée.

    Et le driver MAME ne respecte pas ça.

    Le VG5000µ tourne trop vite ! Les timings sont faux et l'écriture sur cassette ne fonctionne pas. Reste à savoir comment corriger ça.

    À la recherche du cycle en plus

    Une première piste est d'aller voir comment est gérée cette fonctionnalité dans l'émulation Z80 de MAME. Mauvaise nouvelle, elle ne l'est pas. Ou plutôt, la ligne WAIT est bien émulée, mais uniquement entre les instructions. Cela est bien assez nécessaire, il semblerait, pour l'émulation de son utilisation par des périphériques qui demandent au Z80 d'attendre.

    Mais la détection au cycle T2 de la phase M1 n'est pas là. C'est d'ailleurs indiqué dans les commentaires comme une amélioration possible... Que je ne me sens pas d'ajouter.

    Deuxième piste : allez voir ce que font les autres. Côté Amstrad CPC et MSX, les tables de timings d'opcode sont ajustées. Il semblerait qu'il y ait d'autres raisons que ce seul état d'attente, même si c'est une des raisons. Les réutiliser seraient une option... pourvu qu'elles soient utilisables, ce que je trouve un peu lourd à vérifier.

    Reste que cela m'ennuie car ce n'est pas vraiment ce qu'il se passe dans la machine. Je fouille encore. Je trouve un driver qui annonce que faute d'avoir trouvé comment faire, la machine est trop rapide...

    Et enfin, je trouve une solution qui me plaît, utilisée par un autre driver. Utiliser une fonction de rappel sur le rafraîchissement des mémoires dynamiques, qui, étonnamment, est émulée par MAME. Il suffit alors, dans cette fonction de rappel, de dire à l'émulateur du Z80 qu'il devra exécuter un cycle de plus.

    void vg5k_state::z80_m1_w(uint8_t data)
    {
        m_maincpu->adjust_icount(-1);
    }
    

    Ça fonctionne !

    Et tout à coup, tous les timings cassette que je surveillais sont corrects ! La sauvegarde génère un fichier audio qui ressemble à quelque chose (même s'il est bien trop carré pour être pris pour un vrai signal sorti d'une machine réelle, mais ce n'est pas bien grave), et surtout, ce fichier est relu sans problème par l'émulateur.

    Et ceci est à 2400 bauds comme à 1200 bauds.

    Les fichiers sont aussi compris par l'utilitaire de transformation en format K7 qui accompagne dcvg5k.

    Des fichiers audio écrits depuis un vrai VG5000µ sont lus aussi. Il me reste le dernier test : lire sur du vrai matériel un fichier audio écrit par MAME. J'ai bon espoir que cela fonctionne, mais faute d'avoir effectué de vrais tests, le driver reste en non fonctionnel.

    Support de la touche DELTA

    Le VG5000µ possède sur son clavier cette touche non nommée qui est juste désignée par un triangle dans la documentation, connue aussi sous le nom de touche DELTA.

    Puisque j'étais dans MAME, pourquoi pas ajouter cette fonctionnalité ? En effet, le soft reset qui consiste à remettre le PC à 0, ce que fait MAME par défaut, ne permet pas de simuler le soft reset tel qu'implémenté sur la machine à travers la touche DELTA.

    Or, ce soft reset peut-être routé vers une routine utilisateur pour d'autres usages, comme par exemple, relancer directement un programme.

    Une touche à part

    Cette touche est à part dans sa connexion au système. Chaque touche du clavier forme une matrice qui est lue à travers le bus d'entrée/sortie du Z80. Mais pas celle-ci.

    La touche DELTA est (presque) directement branchée à la ligne NMI du Z80. Autrement dit, appuyer sur la touche provoque une interruption non masquable dans le Z80. Par défaut, cette interruption appelle une routine qui vérifie si la touche CTRL est aussi appuyée et dans ce cas, provoque un soft reset. Tout ceci après avoir appelé une potentielle routine utilisateur qui, de base, ne fait rien.

    Comme d'habitude, tout cela est bien entendu pris en charge par MAME, reste à savoir comment. Je suis allé voir du côté d'ancien matériels, où les switchs étaient branchés directement sur des fonctions, sans vraiment constituer un clavier.

    La solution trouvée est la suivante. Tout d'abord, déclarer un nouveau port d'entrée/sortie qui associe au changement d'état d'une touche (j'ai choisi la touche End/Fin du clavier, mais c'est configurable par l'utilisateur) une fonction de rappel.

        PORT_START("direct")
            PORT_BIT( 0x01, IP_ACTIVE_LOW, IPT_KEYBOARD)        PORT_CODE(KEYCODE_END)                              PORT_NAME("DELTA")          PORT_CHANGED_MEMBER(DEVICE_SELF, vg5k_state, delta_button, 0)
    

    La fonction de rappel signale tout simplement la ligne NMI du Z80 pendant un bref instant :

    INPUT_CHANGED_MEMBER(vg5k_state::delta_button)
    {
        if (!newval) {
            m_maincpu->pulse_input_line(INPUT_LINE_NMI, attotime::zero);
        }
    }
    

    Et voilà. La touche DELTA est émulée. On peut vérifier le fonctionnement des routines utilisateur sur CTRL+DELTA.

    Attention, elle ne l'est que lorsque le clavier est en more réel dans MAME, et non en mode naturel, ce dernier mode cherchant une correspondance naturelle entre le clavier de l'hôte et la machine émulée.

    C'est où ?

    Les deux patchs ont été intégrés dans la branche principale de MAME, c'est donc dès à présent disponible sur le dépôt, ou bien dans une future version officielle de MAME.


  • VG5000µ, deux routines (quasi) identiques ()

    Hier, en continuant le commentaire systématique de la ROM du VG5000µ, j'ai eu une impression de déjà-vu. Il me semblait bien avoir déjà commenté la partie « redéfinir un caractère étendu » (commandes SETE et SETG en BASIC).

    L'impression persiste, voire s’accroît, au fur et à mesure et je me décide à aller voir dans ce que j'ai déjà commenté. J'ai commencé, lentement, ce commentaire en août 2017, avec moult pauses, mais aussi du « butinage ». Il y a donc des parties que je ne me rappelle plus avoir déjà traitées.

    Et effectivement, un peu plus loin, je vois des commentaires similaires à ceux que je suis en train d'écrire. Les termes sont un peu différents, car au fil des commentaires, j'ai fait évoluer certains termes ou la manière de commenter, mais en regardant de plus près, oui, c'est évident, il y a deux fois la même routine dans la ROM.

    Je ne sais pas si c'est quelque chose de connu, alors voici mes commentaires.

    Le VDP

    Le Video Display Processor du VG5000µ, un EF9345, est utilisé par le BASIC de manière généralement indirecte. Les commandes comme les changement de couleur, de type de caractères ou encore de position du curseur, modifient des registres internes en RAM. Ces registres sont juste des emplacements réservés dans la RAM principale, celle accessible au Z80, ils n'ont rien de spécial.

    Ces registres se trouvent à partir de $47fa et pointés en tout temps par le registre IX.

    Le BASIC maintient aussi une représentation de l'écran, en $4000, et afficher quelque chose en BASIC revient à modifier cette représentation d'écran, en se basant sur les registres BASIC.

    À chaque rafraîchissement demandé par le VDP, via l'IRQ du Z80 et si le BASIC considère qu'il est temps de mettre à jour l'affichage, un transfert des données est fait vers le VDP.

    C'est le fonctionnement de base.

    Cependant, la ROM contient aussi des routines qui s'adressent directement au VDP. On peut envoyer un caractère dans la mémoire écran du VDP sans passer par la représentation maintenue par le BASIC. Bien évidemment, si on laisse faire le rafraîchissement du BASIC, cette donnée sera écrasée rapidement. On peut aussi lire un caractère depuis la mémoire écran du VDP.

    Ces quatre routines, putahl, putici, getahl et getici ne sont jamais utilisées par la ROM. Ce sont des routines mises à disposition pour l'utilisateur à une adresse fixe, dont le seul code est un branchement à l'implémentation.

    Une autre routine mise à disposition est setext, qui s'occupe de la redéfinition d'un caractère texte ou graphique dans le VDP. Le VDP est en effet configuré par défaut pour offrir 4 « polices de caractères », dont 2 en ROM, et 2 en RAM (celle accessible directement par le VDP). Les deux en RAM peuvent être modifiées via cette routine setext en fournissant le numéro du caractère à changer et 10 octets qui représentent les 10 lignes d'affichage du caractère.

    setext se trouve en $001b et branche immédiatement sur son implémentation en $0d85.

    setext vs. SETE(T/G)

    Le BASIC offre deux commandes pour redéfinir les caractères : SETET, pour redéfinir un caractère texte, et SETEG, pour redéfinir un caractère graphique. La différence entre caractère texte et graphique sort du périmètre de cet article.

    Du point de vue de l'interpréteur BASIC, il n'y a qu'une seule commande SETE, qui vérifie si le caractère suivant est T ou G. On pourrait dire que la dernière lettre de la commande est vue par l'interpréteur comme son premier paramètre.

    L'exécution de SETE se trouve en $0ced dans la ROM BASIC.

    On pourrait s'attendre à ce que SETE utilise la routine setext. Ou bien que les deux routines aient une partie commune. Ce n'est pas le cas.

    Des 141 octets de l'implémentation de setext, environ 100 (à la louche) sont strictement identiques dans SETE, routine qui elle pèse 160 octets (en incluant sa routine annexe, qu'elle est la seule à appeler).

    Différences

    Les deux routines font la même chose, dans le même ordre :

    • Couper le rafraîchissement de l'écran depuis sa représentation RAM,
    • Déterminer si on veut un caractère graphique ou texte,
    • Récupérer le numéro du caractère,
    • Envoyer les commandes au VDP pour préparer la définition,
    • Envoyer les 10 lignes au VDP,
    • Rétablir le rafraîchissement de l'écran.

    Pour setext, c'est très simple, les informations sont dans le registre A pour le caractère et son type, et HL pointe vers les données.

    Pour SETE, c'est un peu plus complexe. Le type de caractère est déterminé par la présence de T ou G, il y a une vérification de la validité du premier paramètre de la commande (numéro de caractère), puis la chaîne de caractères de description, suite de nombres hexadécimaux en ASCII, doit être décodée. C'est la raison pour laquelle la routine est plus longue.

    Cependant, le corps de la routine qui envoie toutes les commandes est identique, à la récupération de la donnée prêt. Et bien que les deux routines utilisent bien les mêmes appels pour envoyer une commande au VDP ou attendre que celui-ci soit prêt à recevoir une commande, il doit bien y avoir une centaine d'octets pouvant être mis en commun.

    Pourquoi ?

    Je n'ai pas d'explication à la duplication de cette routine. Il reste un peu de place inutilisé dans la ROM, et peut être qu'il n'était plus nécessaire d'optimiser la place prise et que c'est juste resté « comme ça ».

    Ce n'est pas, à mon avis, une question de performances avec setext qui serait plus rapide. Elle l'est, en effet, mais pourrait l'être encore plus. En effet, setext garde de SETE le fait de « retourner » chaque octet des descriptions de ligne pour les passer d'une visualisation humaine au format demandé par le VDP. La suite de commandes à envoyer pourraient aussi être rendue plus rapide à l'aide d'un buffer de commandes et l'utilisation de la routine regst.

    Amélioration ?

    Une amélioration possible de la ROM serait de réécrire ces deux routines. Il y aurait peut-être même moyen de profiter de la place gagnée pour caser deux commandes PSET et PRESET, qui font cruellement défaut sur le VG5000µ.


  • Considérations sur le langage LOGO ()

    Quand je vois passer des mentions du langage LOGO, elles sont généralement peu flatteuses. C'est compréhensible, la plupart des personnes qui se souviennent de ce langage de programmation y ont été exposées pendant leurs jeunes années d'études, lors du Plan Informatique pour Tous. Peu sont ceux qui ont creusé plus tard ce qu'ils avaient découverts au moyen d'ordinateurs poussifs et de cours pas toujours maîtrisés par des enseignants pas toujours bien convaincus.

    Mes quelques souvenirs de séances en salle informatiques sont plutôt sur des activités de manipulation du crayon optique sur « Colorpaint ». Je ne sais plus si j'ai fait du LOGO en classe, mais c'est probable. Je me souviens cependant avec netteté la rencontre avec un professeur, lors de la fréquentation d'un « club informatique » pendant des vacances, qui pour une raison ou une autre (mon intérêt enthousiaste à la programmation ?) m'a donné quelques cours de LOGO. Et lors d'une conversation dans laquelle j'avais du mentionner que je programmais en BASIC, il m'a dit cette phrase qui est restée gravée dans ma mémoire « le LOGO est bien plus puissant que le BASIC ».

    Je me souviens que cette phrase m'avait intriguée, mais pas forcément convaincue. Et de retour chez moi, j'ai continué mes aventures en BASIC. J'ai croisé LOGO quelques autres fois, de loin, sans m'y attarder.

    Ce n'est que beaucoup plus tard, quand j'ai commencé à faire de la programmation mon métier, que j'ai repensé à cette phrase. J'ai regardé ce qu'était LOGO de manière plus expérimentée et j'ai compris. J'ai compris la justesse de l'assertion. LOGO était bien autrement plus puissant que le BASIC. Mais d'une manière que je ne pouvais pas comprendre auparavant.

    C'est que la notion de « puissance » appliquée à un langage, ainsi qu'à son environnement, revêt de nombreux aspects. Cela peut être la vitesse d'exécution du programme, la facilité d'écriture, les possibilités d'interactivité avec la machine, la diversité des structures de code et de données.

    Sur des ordinateurs 8 bits, la vitesse d'exécution du BASIC l'emporte haut la main, même sur des BASIC interprétés comme ils l'étaient en majorité.

    Sur la facilité d'écriture, le BASIC est aussi très simple. C'était son but initial. LOGO n'est pas particulièrement complexe à écrire, mais comporte quelques étrangetés syntaxiques et pièges dont je parlerai dans un autre article. Et LOGO est aussi un peu plus verbeux.

    En possibilité d'interaction avec la machine, BASIC l'emporte probablement. Tous les LOGO ne se valent pas, mais l'orientation pédagogique sur les machines 8 bits poussaient les concepteurs à offrir essentiellement des manipulations de l'écran via la tortue, et quelques sons. BASIC, en tant que porte d'entrée de la machine, était souvent dotés des instructions nécessaires pour en démontrer ses capacités. Avec de notables exceptions cependant, mais ceci est une autre histoire.

    Vient ensuite la structure de code et de données. Et c'est là que LOGO renverse BASIC d'une pichenette. Le BASIC de l'époque est très linéaire, son édition se fait encore par numéro de lignes, sans labels. La structuration est possible bien entendu, mais les outils sont maigres. Niveau structure de donnée, le BASIC connaît le tableau multidimensionnel de taille fixe... et c'est tout.

    LOGO arrive avec ses fonctions qui supportent la récursivité, avec des contextes locaux, un nommage des procédures et une édition sans numéro de lignes, qui facilite les mises au point.

    Côté données, LOGO connaît la liste. Et avec une liste dynamique, de nouveaux horizons s'étendent. Des listes de nombres, des listes de mots, des listes de listes, des listes de commandes à exécuter...

    L'expressivité du langage est beaucoup plus intéressante, et les programmes plus concis, plus clairs. C'est en ce sens que LOGO était bien plus puissant.

    Mais tout ceci nécessite de la puissance de calcul et de la mémoire que les machines familiales de l'époque n'ont pas. Et cette malheureuse empreinte qui va rester principalement : on ne fait rien de bien sérieux avec LOGO.

    Pour donner un petit aperçu de LOGO et de son évolution, j'ai réalisé une petite vidéo (moins de 10 minutes) que vous pouvez voir ici.

    (billet posté aussi sur mon blog perso).


  • Récréation 3D, Cartouche Atari 800 ()

    J'avais une cartouche « Atari Writer » qui traînait sur le bureau depuis quelques temps, et je voulais faire un exercice rapide de modélisation avec Blender.

    Cela donne cette image, assez simple, mais qui a été un bon exercice.

    Tortue Jeulin T2


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


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