Site logo

Triceraprog
La programmation depuis le Crétacé

  • Forth sur 6502, épisode 5 ()

    La boucle, pour de vrai !

    Jusqu'à présent, la boucle principale du programme en Forth était basée sur un hack. Ce hack consistait à réinitialiser l'IP (Instruction Pointer) à la fin du traitement. C'est un peu comme si à chaque fois qu'on avait fini le traitement, on relançait le programme depuis le début.

    Il est temps d'implémenter un nouveau mot Forth : BRANCH. Ce mot, suivi dans le PFA par un offset, opère un saut inconditionnel en additionnant cet offset à l'IP courant. En choisissant un offset négatif, l'exécution va revenir en arrière et donc créer une boucle infinie.

    Le pseudo code est le suivant :

        (IP) -> X       lit l'offset pointé par l'Instruction Pointer
                        L'IP ayant été positionné sur l'emplacement après le mot BRANCH
                        par NEXT.
        IP + X -> IP    ajoute l'offset à l'IP
        NEXT            appelle NEXT pour continuer l'exécution
    

    Du fait du traitement de mots en 16 bits, cela donne un code assez long en 6502. Mais je crois que ce sera le cas pour toute cette aventure. Le 6502 est un processeur foncièrement 8 bits. Encore une fois, modulo ma méconnaissance de ce CPU. Je reviendrai peut-être dessus plus tard en le maîtrisant mieux. Ou on me signalera des optimisations.

    Avec le mot BRANCH implémenté, la liste de mots de ma boucle principale devient :

    loop_words:
        .word main_loop_cfa
        .word branch_cfa
        .word $fffc ; -4 in little-endian to create an infinite loop
    

    Plus besoin de reset_code dont je retire le label et le pseudo PFA. Le hack est enlevé et j'ai enfin ma boucle complète en Forth. Puisque loop_words est maintenant un (pseudo-)mot Forth complet, je lui ajoute un CFA et le bootstrap change en initialisant le registre W avec ce CFA et en appelant DOCOL.

    C'est un peu plus élégant je trouve... mais cela fait perdre une place dans la pile de retour, puisque la première valeur de IP (même pas initialisée) y sera poussée par DOCOL. À voir lequel du bootstrap par IP + NEXT ou par W + DOCOL est le plus intéressant.

    Moving Forth, épisode 4

    C'est un tout petit article que cette quatrième partie de la série Moving Forth, tout comme ma partie d'implémentation ci-dessus.

    Cette partie pose la question : comment est-ce qu'on démarre l'implémentation d'un Forth ? Cool ! Mais j'ai déjà commencé. En vrai, j'ai déjà lu plusieurs fois cette série d'articles, il n'y a donc pas de surprise et cette partie entre autres m'a guidé dans mes choix initiaux.

    L'article commence par mettre sur la table deux choix d'implémentation : une implémentation en assembleur en direct sur la machine hôte, ou une implémentation par méta-compilateur, c'est-à-dire un autre Forth qui génère le code machine du Forth cible. Pas de surprise si vous avez suivi jusque-là, c'est la version assembleur que j'ai choisie. L'auteur lui, après avoir indiqué que la voie du méta-compilateur est plus complexe, préfère néanmoins celle-ci, plus portable et plus moderne (même pour 1993).

    J'avais l'idée éventuellement d'avoir aussi un méta-compilateur avec lequel des programmes pourraient être écrits en Forth pour la machine cible, ici la Famicom. Mais je ne suis pas certain de l'intérêt. Ma démarche est celle d'offrir à la Famicom un parallèle Forth au Family BASIC, avec un interpréteur interactif sur la machine et son clavier. Pour créer des jeux modernes sur Famicom, il y a déjà des outils je pense plus adaptés.

    Donc : Forth en assembleur ! Tout du moins son noyau.

    Même si un méta-compilateur serait intéressant pour développer les mots au-delà du noyau... mmmhhhh... Bon on verra.

    L'article continue en parlant de Forth en C comme d'une question qui ne peut pas ne pas se poser. Peut-être parce qu'en 1993, le C est le langage sérieux pour le développement système ? C'est un chemin que j'avais pris il y a quelques années quand je m'étais intéressé au Forth. Mais rapidement, j'étais arrivé à la conclusion que ce sont deux langages aux philosophies (et modèles) très différents, et l'implémentation faisait un peu gymnastique de contorsion.

    Au final, pourquoi pas, en implémentant une machine virtuelle Forth en C, avec quelques commandes de base du noyau. Ça fait partie des choses sur lesquelles je voudrais revenir maintenant que je maîtrise mieux le Forth. Vous le sentez venir le rabbit hole ? Allez, sur la pile !

    Prochain épisode

    Un article très court donc. Le suivant sera plus intéressant pour la suite de l'implémentation car il traitera de ce qui forme un noyau Forth : les mots de base, les primitives, sur lequel le reste est construit.

    Et je pense qu'il sera temps de faire un détour vers le passage à des mots Forth complets, avec leur entête et leur chaînage.


  • Forth sur 6502, épisode 4 ()

    NEXT

    Nous y voilà ! Après avoir mis en place un framework de tests dans l'article précédent, il est temps d'implémenter le mot NEXT.

    Un petit rappel du premier article de Moving Forth : NEXT est le mot qui permet de faire avancer l'exécution d'une séquence de pointeurs vers des mots Forth. Son pseudo-code (en modèle Indirect Threaded Code) est le suivant :

        (IP) -> W   récupère la mémoire pointée par IP dans le registre "W"
                    -> W contient maintenant l'adresse du Code Field
        IP+2 -> IP  avance IP, le compteur de programme
        (W) ->  X   récupère la mémoire pointée par W dans le registre "X"
                    -> X contient maintenant l'adresse du code d'exécution
        JP (X)      saute à l'adresse présente dans le registre X
    

    Implémentation

    En 6502, le code est un peu long, car l'adressage indirect nécessite de passer par des registres en page zéro. Et les manipuler demande plusieurs instructions. Je ne suis pas très versé en 6502, on verra s'il y a moyen de faire mieux plus tard.

    Dans certains Forth, NEXT est tellement court qu'il se pose la question d'en faire une macro. Ici, la réponse est assez simple : c'est non. Le code est assez long, et donc NEXT sera appelé avec un JMP à son adresse.

    Très bien, j'ai donc NEXT. Il me faut aussi une liste de mots à appeler. Le but étant d'implémenter l'écriture d'une valeur en mémoire, je vais créer un pseudo-mot TEST-WORD. Puis comme il faut faire cela en boucle, un second mot qui réinitialisera l'IP (Instruction Pointer) au début de la séquence, que je vais appeler RESET-CODE.

    Cela donne quelque chose comme ceci :

    loop_words:
        .word test_word_cfa
        .word reset_code_cfa
    

    Les pointeurs sont vers les CFA (Code Field Address) des mots. Pour rappel, le CFA est l'adresse où se trouve le code machine qui sera en charge d'exécuter le mot suivant (oui, c'est bien du Indirect Threaded Code, c'est indirect).

    Par exemple, pour TEST-WORD, le CFA est l'adresse de la première instruction assembleur qui compose le mot :

    test_word_cfa:
        .word test_word_pfa
    test_word_pfa:
        lda #$42
        sta $7FF
        jmp next
    

    NEXT va lire le CFA, puis sauter à l'adresse indiquée, qui est test_word_pfa. Et donc exécuter le code qui place la valeur $42 à l'adresse $07FF, avant de redonner la main à NEXT.

    J'ai dit plus haut que c'était un pseudo-mot. En effet, le CFA et le PFA ne sont qu'une partie de la définition d'un mot complet en Forth. Mais comme cette partie n'a pas été abordée pour le moment, et qu'elle n'est pas nécessaire pour faire fonctionner NEXT, je laisse ceci de côté.

    Reste à bootstrapper l'IP pour qu'il pointe sur loop_words au démarrage du programme. J'ajoute donc un petit code d'initialisation qui... sera aussi le code du mot RESET-CODE :

    boot_forth:
    
        ; Set the Instruction Pointer to the start of temporary Forth code
    reset_code:
        lda #<loop_words
        sta REG_IP
        lda #>loop_words
        sta REG_IP + 1
    
        ; Jump to the Forth NEXT loop
        jmp next
    

    Plus tard, il faudra aussi initialiser d'autres registres dans ce code. Mais pour le moment, ça suffit.

    J'enlève l'ancien code d'écriture en mémoire de la boucle principale, je remplace par un appel à boot_forth, et voilà ! Les tests passent... Mais le code d'affichage n'affiche plus le HELLO FAMICOM du squelette. C'est normal puisque la boucle principale Forth n'appelle pas le code de synchro et d'affichage.

    La boucle complète

    J'ai tous les éléments pour remettre en place la boucle principale. Il suffit de créer deux mots (ou plutôt pseudo-mots) qui feront juste un appel aux deux routines présentes initialement dans la boucle principale.

    read_joy_safe_word_cfa:
        .word read_joy_safe_word_pfa
    read_joy_safe_word_pfa:
        jsr read_joy_safe
        jmp next
    
    post_logic_word_cfa:
        .word post_logic_word_pfa
    post_logic_word_pfa:
        jsr post_logic
        jmp next
    

    Et de modifier la liste des mots à appeler dans loop_words :

    loop_words:
        .word read_joy_safe_word_cfa
        .word test_word_cfa
        .word post_logic_word_cfa
        .word reset_code_cfa
    

    Et l'affichage remarche. Avec beaucoup plus d'indirections et donc plus lent, c'est vrai. Je verrai plus tard comment regrouper les appels. Ou comment modifier les sous-routines pour les implémenter directement comme des mots.

    En tout cas pour le moment, j'ai retrouvé le comportement initial. Parfait.

    DOCOL et ;S

    Dans la partie 2 de cette série, j'avais listé les mots DOCOL et ;S comme prioritaires à implémenter. En effet, ces mots permettent d'appeler des mots définis par d'autres mots. C'est exactement ce qui est fait avec loop_words. Sauf que cela n'est pas un vrai mot. Il n'a même pas de CFA. Cette liste est démarrée manuellement par le code d'initialisation, que je rappelle en boucle.

    Mais cette boucle est aussi un petit hack avec le RESET-CODE. Je ne peux pas l'utiliser proprement pour démontrer DOCOL et ;S. Je vais donc créer un autre pseudo-mot

    main_loop_cfa:
        .word docol
    main_loop_pfa:
        .word read_joy_safe_word_cfa
        .word test_word_cfa
        .word post_logic_word_cfa
        .word do_semi_cfa
    

    docol étant le code machine de DOCOL et do_semi_cfa le pointeur sur le CFA de ;S.

    La boucle « hack » se retrouve donc remplacée par :

    loop_words:
        .word main_loop_cfa
        .word reset_code
    

    Pseudo-code pour les deux mots

    Le pseudo-code de DOCOL est le suivant :

       PUSH IP      pousse IP sur la pile de retour
       W+2 -> IP    puisque W pointait sur le Code Field,
                    W+2 est l'adresse du Parameter Field.
                    Le Parameter Field contient la liste des
                    adresses des mots à exécuter. IP pointe
                    donc sur cette liste.
       JUMP NEXT    saute à NEXT pour continuer l'exécution
    

    Le pseudo-code de ;S est le suivant :

       POP IP       récupère l'adresse de retour depuis
                    la pile de retour et la place dans IP
       JUMP NEXT    saute à NEXT pour continuer l'exécution
    

    Et ça fonctionne

    À nouveau, les tests passent et l'affichage fonctionne. Les trois premiers mots Forth sont donc implémentés : NEXT, DOCOL et ;S.

    Moving Forth, article 3

    Jetons à présent un œil sur le troisième article de la série Moving Forth. Son titre est « Demystifying DOES> », mais il en profite surtout pour décortiquer le fonctionnement des CFA et PFA.

    On passe sur l'introduction, qui revient sur des erreurs dans les parties précédentes pour arriver à l'explication du Code Field.

    Le Code Field, c'est le cœur du fonctionnement d'un mot en Forth. C'est un indicateur de la nature du mot, de la façon dont il va se comporter, être exécuté. Le Code Field indique quoi faire avec les paramètres du mot, les paramètres étant situés dans le Parameter Field, qui suit le Code Field.

    Ainsi, on peut définir différents fonctionnements :

    • un mot qui exécute les paramètres comme du code machine,
    • un mot qui traite les paramètres comme des référence à d'autres mots,
    • un mot qui traite les paramètres comme un emplacement mémoire réservé pour une variable,
    • etc...

    En quelque sorte, le Code Field représente une routine avec un paramètre implicite : le Parameter Field.

    L'article fait un parallèle avec la programmation objet, où le Code Field serait la méthode unique d'une classe, et le Parameter Field l'instance de cette classe pour chaque mot utilisant ce Code Field. Je ne suis pas certain que ce parallèle aide beaucoup... mais pourquoi pas.

    Code Field et Indirect Threaded Code

    Dans le modèle ITC (Indirect Threaded Code), le Code Field est une adresse qui pointe vers un code machine chargé d'interpréter le contenu du Parameter Field. On a vu deux cas pour le moment :

    • un Code Field qui pointe vers DOCOL entraîne l'appel successif de tous les mots dont les listés dans le PFA. Ces mots sont eux-mêmes représentés par leur CFA.
    • un Code Field qui pointe vers directement sur l'adresse du Parameter Field. Dans ce cas, le code machine est directement dans le PFA.

    Il existe différents Code Field standards en Forth, mais rien n'empêche l'utilisateur d'en définir de nouveaux. Et c'est exactement ce que fait le mot DOES> mentionné par le titre. Cependant, son fonctionnement nécessitera probablement un article à lui seul. Retenez juste que DOES> permet de définir une nouvelle classe de mots partageant un même Code Field personnalisé.

    L'article mentionne aussi les mots CREATE ou encore ;CODE, qui respectivement permettent de définir un nouveau mot et de définir un mot dont le code est écrit en assembleur. Là encore, ce sont des sujets pour plus tard lorsqu'il sera question de définir des mots par programmation.

    Pour le moment, je n'ai toujours que des pseudo-mots, et ils sont tous définis directement dans le code assembleur.

    L'article est plus long car il aborde ces sujets dans les différents modèles d'exécution de Forth, ainsi que les mots CONSTANT et VARIABLE, qui sont des mots de constructions avec un Code Field particulier, que nous verrons plus tard.

    La suite

    J'ai à présent les premiers pseudo-mots pour mon Forth, mais il m'en manque un pour créer la boucle en entier. En effet, pour le moment je réinitialise l'interpréteur pour relancer la boucle. Ce n'est pas une vraie boucle Forth. Je pense que ma prochaine étape sera donc de créer le mot BRANCH, qui modifie le registre IP.


  • Forth sur 6502, épisode 3 ()

    Des tests

    Depuis le dernier article, je me suis surtout concentré sur la mise en place d'un framework de tests, ainsi que sur une réflexion de « comment commencer » ?

    La lecture de l'article 2 de Moving Forth m'a donné une liste de mots à implémenter en priorité : NEXT, DOCOL et ;S. Cela afin d’arriver rapidement sur une boucle écrite en Forth. J'y reviendrai plus loin dans l'article.

    Côté tests, ça a été une aventure en plusieurs étapes. Dans l'idée d'augmenter le nombre de tests, je voulais m'appuyer sur un framework de tests LUA tout fait. J'en ai trouvé un, luaunit qui m'a semblé tout à fait répondre à mes attentes. Pour l'utiliser, je dois utiliser un require("luaunit") dans mon script de tests. Et là ont commencé les ennuis. Tout d'abord, require() n'est pas permis par défaut dans Mesen2, il faut aller permettre les fonction E/S dans les paramètres.

    Malgré cela, le script ne semble pas être trouvé, alors qu'il est bien dans un répertoire de recherche (vérifié avec le contenu de la variable package.path). En tout cas pas en mode --testrunner, car en debug depuis l'interface graphique, ça fonctionne ! Après étude des sources de Mesen2, je vois le soucis (qui était bien affiché dans mes logs, mais assez subtil pour que je passe à côté) : le mode testrunner et le mode UI résolvent différemment le chemin des scripts, à un séparateur de chemin près.

    La correction est simple, je la fais et je suis prêt à faire un merge request... pour m'apercevoir que le dépôt est fermé aux contributions. Après enquête, il semblerait que le développeur a disparu de la circulation en juillet 2025. Puisque le dépôt est fermé, j'imagine que c'est un retrait volontaire et non un événement malheureux. Je l'espère. On me dit qu'il aurait déjà fait ça dans le passé lorsqu'il avait eu besoin de se concentrer.

    En attendant, j'ai donc créé ma version de Mesen2 avec cette correction (et une autre trouvée dans un autre fork qui me semblait intéressante). Merci l'open source.

    Vient ensuite mon deuxième soucis : luaunit ne va pas convenir. Comme tous (?) les frameworks de tests, le framework gère des contextes cloisonnés, s'occupe de lancer les tests, de capturer les erreurs, etc. Bref, il a les commandes. Sauf que Mesen2 requiert que certaines fonctions de contrôle de l'émulateur soient appelées depuis des callbacks. Autrement dit, Mesen2 veut aussi avoir le contrôle.

    Après avoir retourné le problème quelques temps, je me suis dit que finalement, j'allais faire un petit framework sur mesure adapté à Mesen2. Il aura certainement moins de fonctionnalités qu'une solution complète, mais tant pis. J'ai donc écrit un framework basé sur une machine à états qui fait avancer les tests au fur et à mesure que l'émulateur lance des callbacks. Ça fonctionne, mais c'est un peu verbeux à l'écriture pour le moment. J'espère pouvoir dégager quelques fonctions utilitaires pour simplifier l'écriture des tests par la suite, si besoin.

    Ah, et j'englobe l'appel des tests dans un xpcall(), une fonction bien pratique en LUA qui permet de capturer les erreurs, et donc d'arrêter l'émulateur proprement en cas de problème avec affichage du message d'erreur.

    Mes trois tests

    Pour le moment, j'ai trois tests :

    • Le premier test vérifie que les symboles des registres Forth sont bien présents dans les symboles. Il y a peu de chance que ça ne passe pas, mais c'est ce qu'on se dit avant que ça ne passe pas...
    • Le deuxième test vérifie que le programme arrive jusqu'à la boucle principale. Un smoke test de base.
    • Le troisième test vérifie qu'une adresse mémoire particulière est modifiée. C'est actuellement l'action qui est faite dans la boucle principale.

    En fait, il y a pas mal d'autres choses qui sont faites dans la boucle principale par le squelette de projet que j'utilise. Je verrai si j'ai besoin à un moment de tester cette partie, mais pour le moment, dans une logique TDD, tant que je n'y touche pas, je ne teste pas.

    La suite

    Pas de lecture de la partie 3 de Moving Forth pour le moment. En effet, celle-ci se concentre sur le CFA (Code Field) et le PFA (Parameter Field), puis enchaîne sur les mots de construction. Je n'en suis pas encore là. Je reprendrai les résumés lorsque j'aurai une boucle Forth fonctionnelle.

    Car c'est la prochaine étape : pour le moment, le code contient une boucle assembleur qui :

    • lit les entrées des pads
    • écrit une valeur à une adresse mémoire fixe (pour le test)
    • attend la synchronisation verticale
    • exécute une file d'instruction PPU (pour l'affichage)
    • boucle

    Ma prochaine étape est de remplacer cette boucle par la même chose en Forth. Pour cela, je dois implémenter au moins le mot NEXT qui appellera ce même code suivant le schéma Forth. Autrement dit, ces routines devront se terminer par un appel à NEXT pour continuer l'exécution, plutôt que de se terminer par un RTS (return from subroutine).

    Point d'étape

    J'ai donc à présent un environnement d'assemblage et de tests, une boucle minimale. Le prochain article sera écrit lorsque j'aurai a minima un NEXT fonctionnel.

    Et pour terminer, voici la sortie des tests actuels :

    mesen --testrunner out/nes_template.nes start_tests.lua
    -- STARTING TESTS --
    [1/3] Running: Forth symbols exist
      ✓ PASSED
    [2/3] Running: Program boots and reaches main loop
      ✓ PASSED
    [3/3] Running: Verify memory change at specific address
      ✓ PASSED
    ============================================================
    TEST REPORT
    ============================================================
    Total: 3 | Passed: 3 | Failed: 0
    ============================================================
    

  • Forth sur 6502, épisode 2 ()

    Le projet minimal

    Avant de passer à la lecture de l'article 2 de la série Moving Forth, je veux mettre en place un projet « minimal ». Plus exactement, c'est initialement ce que je voulais faire, avec un petit code source pour Famicom qui affiche un texte, un Makefile et bien entendu, un framework de tests.

    Mais les notes que j'avais prises sur la Famicom remontaient à un petit moment, et il y a quelques trucs à initialiser avant d'afficher un caractère à l'écran. J'ai donc cherché un squelette de projet déjà fait. Sur une machine populaire, ça doit bien exister.

    Mon choix s'est porté sur Nes Template de Mike Moffitt. Le squelette n'est pas minimaliste, mais a une bonne base, avec un système de configuration flexible pour la ROM destination, et assez de quoi écrire un message à l'écran, ainsi que manipuler des banques de mémoire. C'est bien écrit, bien commenté. Ça me convient bien.

    J'y ai ajouté un script Lua pour automatiser des tests en utilisant l'émulateur Mesen2. Le script place une callback à une adresse mémoire spécifique et s'y arrête lorsque cette adresse est atteinte par le registre PC.

    Et voilà ce que cela donne :

    mesen --testrunner out/nes_template.nes tests/tests.lua
    -- STARTING TESTS --
    Setting callback at address: 0xC0D2
    Memory position reached: 0xC0D2
    

    Le mapping mémoire

    À la fin de l'article précédent, j'avais évoqué le mapping mémoire afin de vérifier les contraintes de la Famicom.

    Voici un mapping rapide (que l'ont peut retrouver plus en détail sur Nesdev) :

    $0000-$00FF : Page zéro     (256 octets)
    $0100-$01FF : Pile          (256 octets)
    $0200-$07FF : RAM interne   (1536 octets)
    
    $2000-$2007 : Registres PPU (le processeur graphique)
    $4000-$4017 : Registres APU (le processeur audio) et E/S
    
    $6000-$7FFF : RAM sur cartouche     (si présente)
    $8000-$FFFF : PRG ROM sur cartouche (si présente)
    

    Les deux dernières zones sont dépendantes de la cartouche et du « mapper » utilisé. Dans la configuration par défaut du projet template que j'utilise, il n'y a pas de RAM configurée sur la cartouche. Il y a par contre 16 banques de 16 Ko de PRG ROM (de la ROM accessible par le CPU), mappée de $8000 à $FFFF. Une banque de 16 Ko est fixe (la dernière, de $C000 à $FFFF) et l'une des autres banques peut être mappée de $8000 à $BFFF. Certaines de ces banques sont utilisées pour stocker les ressources graphiques, qui sont ensuite envoyées à de la mémoire vidéo présente sur la cartouche.

    Il faudra très certainement ajouter de la RAM sur cartouche pour faire quelque chose d'intéressant avec Forth. Je verrai à la fin du développement quel mémoire serait nécessaire pour une production sur cartouche. De toute façon, pendant le développement, j'utiliserai un émulateur la plupart du temps et une cartouche de développement pour vérifier sur vrai matériel.

    Le projet template permet de réserver assez facilement des pointeurs dans la page zéro. Il définit d'ailleurs déjà un certain nombre de variables temporaires. Je vais tout simplement utiliser ce système pour déclarer les registres Forth.

    Pour la pile des paramètres, je vais m'inspirer de ce que le template utilise pour sa queue de commandes graphiques : réservation d'un espace mémoire non initialisé dans la RAM interne et définition d'un pointeur associé dans la page zéro.

    Moving Forth, article 2

    Dans le deuxième article de la série Moving Forth, l'auteur commence par lister les mots essentiels au Forth. Les mots sur lesquels le reste repose, et qui nécessitent d'être efficace.

    Les mots

    En premier, il y a les mots qui forment l'interpréteur lui-même :

    • NEXT
    • DOCOL (aussi appelé ENTER dans l'article)
    • ;S (aussi appelé EXIT dans l'article)

    NEXT est le cœur de l'interpréteur, le mot qui fait avancer toute la machinerie. Il récupère l'adresse du mot à exécuter depuis l'instruction pointer (IP), puis incrémente l'IP pour le mot suivant. Ensuite, il récupère l'adresse de la routine associée au mot (dans le cas de l'Indirect Threaded Code, choisi ici, c'est une adresse mémoire qui pointe vers l'adresse de la routine) et saute à cette adresse pour exécuter le mot. Il vaut mieux lire cette ligne lentement...

    C'est évidemment le premier mot à implémenter. Et il est d'ailleurs possible de faire tourner un petit programme avec seulement ce mot. NEXT ne fait qu'appeler une liste de routines. Et ces routines appellent à leur tour NEXT pour continuer l'exécution.

    DOCOL est le mot qui gère l'appel d'un mot défini par d'autres mots. Il sauvegarde l'IP actuel sur la pile de retour, puis positionne l'IP à l'adresse du code du mot appelé. Ensuite, il saute à NEXT pour continuer l'exécution.

    ;S est le mot qui gère le retour d'un mot défini par d'autres mots. Il récupère l'adresse de retour depuis la pile de retour et la place dans l'IP, puis saute à NEXT pour continuer l'exécution.

    Ensuite, l'article liste les deux mots de constructions que sont DOVAR et DOCON, respectivement pour définir des variables et des constantes. Ainsi que LIT, pour charger une valeur littérale sur la pile de paramètres.

    Il liste aussi DODOES que je laisserai de côté dans un premier temps. C'est un mot un peu plus complexe qui permet l'extension des mots définis par BUILD/DOES>>.

    Et bien entendu, les mots standards :

    • @ (fetch)
    • ! (store)
    • + (addition)
    • SWAP
    • OVER
    • ROT
    • 0=
    • +!

    Les études de cas

    L'article aborde ensuite plusieurs études de cas avec différents processeurs. Il ne cite pas le 6502, je ne m'y attarde donc pas.

    Conclusion

    L'étape suivante sera l'implémentation de au moins NEXT, DOCOL et ;S, afin de pouvoir exécuter un petit programme composé de routines en assembleur. Comme je compte tester le programme, il me faudra aussi un framework de test un peu plus sérieux que le petit script Lua que j'ai écrit pour l'instant.

    En attendant, j'ai placé les registres Forth dans la page zéro et réservé un espace mémoire pour la pile des paramètres. Le test donne à présent :

    mesen --testrunner out/nes_template.nes tests/tests.lua
    -- STARTING TESTS --
    Found symbol REG_W at 0x0010
    Found symbol REG_IP at 0x0012
    Found symbol REG_PSP at 0x0014
    Found symbol REG_TOS at 0x0016
    All required symbols found
    Setting callback at address: 0xC0D2
    Memory position reached: 0xC0D2
    

  • Forth sur 6502 ()

    Family Forth, le projet

    En m'intéressant au langage de programmation Forth, j'ai parfois croisé l'affirmation que pour bien comprendre Forth, il fallait en développer un.

    C'est cohérent avec son histoire, qui débute comme une trousse à outils de son inventeur pour lui faciliter les développements qu'il fait pour ses employeurs.

    J'ai lu un certain nombre de documents sur Forth, parcouru quelques implémentations et même développé un jeu dans ce langage pour Hector HRX.

    Plus récemment, en m'amusant avec le BASIC pour Famicom, je me suis demandé ce qui pourrait être développé sur la console pour tirer parti du clavier. Alors, pourquoi pas un Forth interactif sur Famicom ?

    Une rapide recherche m'a montré que des expériences en Forth pour Famicom avaient déjà été faites, mais pas forcément sous l'angle interactif. Et bien entendu, il existe déjà des Forth pour 6502, le processeur qui équipe cette machine (même si c'est une version custom, cela ne change pas grand chose).

    Mais la prémisse reste : l'intérêt est de développer son propre Forth. Et c'est donc ce que je vais faire dans une série d'articles qui me serviront à poser mes idées, comme pense bête. Et pourquoi pas comme inspiration pour d'autres, si vous voulez bien me suivre, qui sait ?

    Pour initier le développement, j'ai décidé de suivre la série d'articles Moving Forth, qui a été publiée à partir du numéro 59 du « The Computer Journal », en janvier/février 1993.

    Cette série d'articles est intéressante car elle étudie pour différentes étapes les options d'implémentations qui s'offrent, avec des références. Elle donne aussi des implémentations pour divers CPUs, mais pas pour 6502, ce qui me laissera un peu de choix dans mes réflexions, même si je doute que ceux-ci soient très originaux.

    Ces articles étant assez anciens, ils renvoient aussi à une conception pas trop moderne de Forth, qui a évolué depuis, et donc plus proche de ce qu'on pourrait attendre sur Famicom. Enfin peut-être, on verra.

    Le 6502 est aussi un CPU que j'ai très peu pratiqué, c'est donc une occasion de gagner en expérience dessus.

    Moving Forth, article 1

    De chaque article de Moving Forth, je vais retirer les informations qui me semblent importantes dans la démarche de développement. Et je commence donc par... le premier article.

    Quelques choix

    Tout d'abord, la taille des mots. Sur un tel processeur, le choix est assez facile : 16 bits est le minimum pour manipuler des adresses mémoire. Les mots seront donc sur 16 bits. L'article rappelle d'ailleurs qu'il vaut mieux utiliser le mot « cellule » (CELL) car un « mot » est quelque chose d'autre en Forth.

    Vient ensuite le choix du modèle de fonctionnement. Il y a plusieurs moyens de faire fonctionner un Forth, l'article mentionne le « Indirect Threaded Code » (ITC), le « Direct Threaded Code » (DTC), le « Subroutine Threaded Code » (STC) et le « Token Threaded Code » (TTC). Il mentionne aussi le « Segment Threaded Code », mais c'est particulier au 8086, donc pas pertinent ici.

    Le choix classique pour des machines 8 bits est le « Indirect Threaded Code ». Je vais suivre le mouvement et choisir cette méthode. L'article indique que ça n'est ni la méthode la plus rapide ni la plus compacte, qu'elle a un avantage de simplicité, mais que c'est surtout une habitude historique.

    Cependant, c'est aussi la méthode dont j'ai l'habitude lorsque j'ai pratiqué Forth sur d'autres machines 8 bits. Je me note cependant dans un coin de peut-être étudier plus tard les autres méthodes.

    Registres

    Il nous faut aussi des registres. On parle là de registres Forth, et de la manière dont ils seront implémentés sur le CPU, ici le 6502.

    Voici la liste des registres pour lesquels il faudra trouver une place, par ordre d'importance décroissante :

    • W (working register)
    • IP (instruction pointer)
    • PSP (parameter stack pointer)
    • RSP (return stack pointer)
    • TEMP (temporary register)

    Rappel, tous ces registres doivent être sur 16 bits.

    L'article mentionne un registre TOS (top of stack), optionnel mais qui peut optimiser certaines opérations. Pour le moment, je vais le laisser de côté.

    Il est aussi fait mention d'un mapping possible des registres sur 6502, que voici :

    * W, emplacement en page zéro
    * IP, emplacement en page zéro
    * PSP, registre `X` du 6502
    * RSP, registre `SP` du 6502
    * TOS, emplacement mémoire
    

    X et SP étant des registres 8 bits, cela limite la taille de la pile de paramètres et de la pile de retour. À voir si c'est un problème ou pas.

    Ce qui me fait penser qu'il y aura tout intérêt d'écrire un maximum de mots Forth à partir d'autres mots Forth et de limiter les mots en assembleur, au moins dans un premier temps. Ceci afin de limiter la réécriture de routines si je change d'avis sur le mapping des registres.

    Conclusion

    Très bien, après tout cela, il est temps de passer à la définition d'un projet minimal sur la machine cible. Entre autre, il faudra vérifier les emplacements déjà réservés, que ce soit dans la page zéro ou ailleurs en mémoire.


Page 1 / 25 (suivant) »

Tous les tags

3d (15), 6502 (5), 6809 (1), 8bits (1), Affichage (24), AgonLight (2), Altaïr (1), Amstrad CPC (1), Apple (1), Aquarius (2), ASM (30), Atari (1), Atari 800 (1), Atari ST (2), Automatisation (4), BASIC (31), BASIC-80 (4), C (3), Calculs (1), CDC (1), Clion (1), cmake (1), Commodore (1), Commodore PET (1), Compression (4), CPU (1), Debug (5), Dithering (2), Divers (1), EF9345 (1), Émulation (7), Famicom (2), Forth (8), Game Jam (1), Hector (3), Histoire (1), Hooks (4), Huffman (1), i8008 (1), Image (17), Jeu (15), Jeu Vidéo (4), Livre (1), Logo (2), LZ (1), 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), PHC-25 (2), Photo (2), Programmation (10), Python (1), RLE (1), ROM (15), RPUfOS (6), Salon (1), SC-3000 (1), Schéma (5), Synthèse (15), Tortue (1), Triceraprog (1), VG5000 (62), VIC-20 (1), Vidéo (1), Z80 (21), z88dk (1), ZX0 (1)

Les derniers articles

Forth sur 6502, épisode 5
Forth sur 6502, épisode 4
Forth sur 6502, épisode 3
Forth sur 6502, épisode 2
Forth sur 6502
Compression de données, Huffman
Compression de données, le format ZX0
Compression de données, référencer les répétitions
Briques Stellaires, jeu pour PHC-25
Compression de données, compter les répétitions

Atom Feed

Réseaux