Site logo

Triceraprog
La programmation depuis le Crétacé

  • Forth sur 6502, épisode 10 ()

    Un curseur et des branches

    Le clavier est pour le moment implémenté avec deux mots. L'un qui sera gardé, KBDSCAN et l'autre qui est là en attendant de pouvoir écrire la même chose en Forth, KBDPROCESS. L'objectif premier est de transformer KBDPROCESS en son équivalent Forth que je placerai dans ma boucle principale (pas encore QUIT, qui n'est pas encore prêt).

    Mais avant toute chose, j'ai quelque chose à corriger avec le curseur. Pour le moment, j'affiche le curseur avec EMIT, ce qui fait avancer la prochaine position d'affichage de caractère. Ce que je veux, c'est afficher le caractère reçu du clavier avec EMIT puis afficher le caractère du curseur sans faire avancer la position d'affichage. Ainsi, le caractère du curseur sera toujours une position après le dernier caractère affiché.

    Pour cela, j'ai un peu remanié le code afin de séparer l'envoi du caractère à afficher et la mise à jour de la position d'affichage. Le code de EMIT appelle les deux fonctions et j'ai ajouté le mot PUTCHR qui affiche le caractère à la position courante sans avancer la position du curseur.

    Ainsi, l'affichage d'un caractère reçu depuis le clavier devient :

    KEY             \ Le caractère reçu du clavier est mis sur la pile
    EMIT            \ Affiche le caractère présent sur la pile et avance le curseur
    CURSOR_CODE @   \ Récupère le code du caractère du curseur
    PUTCHR          \ Affiche le caractère du curseur sans avancer le curseur
    

    Le fait de mettre le code du curseur dans une variable pointée par CURSOR_CODE permettra de changer la forme du curseur en fonction du mode (pour indiquer si on est en mode KANA ou non par exemple).

    Avec un mot KEY bloquant, cela serait suffisant. Ce bout de code serait mis dans la boucle infinie et puis voilà. Seulement, je veux pouvoir opérer d'autres traitements dans la boucle d'attente de caractère. Faire clignoter le curseur par exemple, sans gestion par interruption. Et puis, c'est une bonne occasion d'implémenter une branche conditionnelle.

    Le standard Forth actuel définit IF, ELSE et THEN de manière haut niveau, en indiquant le comportement attendu. Dans les Forth plus anciens, comme le FIG-Forth dont je m'inspire, le détail d'implémentation est d'utiliser les mots bas niveau 0BRANCH et BRANCH pour déplacer IP (le pointeur d'instruction).

    J'ai déjà BRANCH, c'est le mot qui me permet d'avoir ma boucle infinie, en reculant IP de façon inconditionnelle. QUIT, actuellement, ressemble à ceci :

        MAIN_LOOP       \ Appel du mot dans lequel je fais mes tests
        BRANCH          \ Déplacement inconditionnel de IP en ajoutant le mot qui suit en mémoire
        $fffc           \ le mot qui suit en mémoire : -4. Ce qui ramène IP au niveau de MAIN_LOOP
    

    0BRANCH fonctionne de la même manière, mais ne déplace IP que si la valeur au sommet de la pile est zéro. Dans le cas contraire, l'instruction ne fait rien d'autre que déplacer l'IP pour passer par-dessus le mot qui suit en mémoire, puis donne le contrôle à l'instruction suivante. Dans tous les cas, la valeur sur la pile est retirée de la pile.

    Le mot est assez simple à implémenter avec mes briques existantes. Voici le code assembleur :

        ; 0BRANCH, branches if TOS is zero, else continues
    DEFINE_CODE_WORD_WITH_SYMBOL ZERO_BRANCH, "0BRANCH", BRANCH, 0
        ; Pop TOS into Temp
        POP_PARAM_STACK_TO_REG Temp
    
        ; If Temp (16 bits) is zero, do the branch, call branch
        lda Temp
        ora Temp + 1
        beq BRANCH_word_pfa
    
        ; Not zero, advance IP by 2 (to skip the branch offset)
        inc_reg REG_IP
        inc_reg REG_IP
        END_TO_NEXT
    

    (rappel : TOS signifie "Top Of Stack", le sommet de la pile de données)

    Avec 0BRANCH, le traitement et affichage du clavier devient :

        KBDSCAN         \ Scan du clavier et mise à jour des tampons
        KEY?            \ Y a-t-il un caractère disponible ?
        0BRANCH         \ Si non, branchement
        $000c           \ vers l'instruction après PUTCHR (12 octets plus loin)
        KEY             \ Récupération du caractère
        EMIT            \ Affichage du caractère et avancée du curseur
        CURSOR_CODE @   \ Récupération du code du curseur
        PUTCHR          \ Affichage du curseur sans avancer le curseur
        \ Suite du code de la boucle principale
    

    Avec ce morceau de code, j'ai un curseur qui fonctionne et je peux l'initialiser dans COLD. J'en profite pour implémenter LIT qui permet de pousser la valeur qui suit le mot en mémoire sur la pile. Cela me permet d'écrire des valeurs immédiates dans le code Forth. Jusqu'à maintenant, j'utilisais des constantes qui contenaient les valeurs initiales.

    Il n'est pas impossible que je revienne sur l'initialisation (et le déplacement) du curseur dans le futur pour deux raisons liées. La première est qu'écrire sur les bords de l'écran sur ces machines n'est pas une bonne idée : les télés cathodiques avaient tendance à couper les bords de l'image. Pour le moment, je n'écris ni sur la première ni sur la dernière colonne. Mais en regardant Family BASIC, je vois que ce sont deux colonnes entières qui sont laissées comme marges. Ainsi que les deux premières lignes.

    Ce qui m'amène à la seconde raison liée : permettre de changer la géométrie de la fenêtre de texte par l'utilisateur. C'est une commande qui existe sur l'Hector HRX par exemple. Avec ce système, ce mot WINDOW permettrait de définir les marges haut, bas, gauche et droite. À noter pour plus tard... car cela compliquera la gestion du scroll lors de l'arrivée en bas de la zone.

    Entrer du texte

    Le texte s'affiche donc à l'écran, très bien. Mais il n'est pas encore prêt pour être traité par le système Forth. Pour cela, FIG-Forth utilise un buffer dont l'adresse est maintenue dans la variable adresse TIB (Text Input Buffer), ainsi que sa taille dans la variable adresse #TIB, comme indiqué dans le livre Starting Forth dans le chapitre Under the Hood.

    Je ne sais pas encore où placer ce buffer. Je vois plusieurs options possibles :

    • Je maintiens un buffer en RAM des caractères entrés et édités, en parallèle de ce qui est envoyé à l'écran. Cela limite l'édition à une ligne logique, pas possible de se balader sur un écran pleine page. Et maintenir en synchro deux systèmes parallèles n'est pas souvent une bonne idée.
    • J'utilise l'écran lui-même comme buffer et je limite l'édition à une ligne logique. Le fait que les données à l'écran ne soient pas continues à cause des réservations des premières et dernières colonnes complique un peu les choses. Il faut aussi ramener les informations présentes dans la mémoire vidéo vers la RAM pour traitement, ce qui est assez lent et nécessite d'être fait pendant la VSync.
    • J'utilise l'écran lui-même comme buffer et je permets l'édition pleine page. C'est globalement la même chose que l'option précédente, avec en plus un algorithme pour déterminer où commence la ligne logique lorsque l'on appuie sur RETURN.

    Dans l'idéal, j'aimerais la troisième option. Cependant si cette fonctionnalité est très pratique en BASIC où on peut revenir sur des lignes précédentes pour les éditer, l'intérêt est moins évident en Forth, où il faut « oublier » les mots avant de les redéfinir.

    La première option m'ennuie à cause du double traitement. Je pense que la seconde option sera mon choix de base. Si je vectorise le mot ACCEPT, il sera possible pour un utilisateur avancé de redéfinir le comportement pour faire de l'édition pleine page, en fonction de la RAM disponible sur la cartouche... je m'emballe.

    Et les tests ?

    L'affichage initial du curseur était testé par mes tests unitaires. Indirectement, le mot LIT que j'utilise à l'initialisation est testé. Par contre, j'ai à présent du texte en entrée et une boucle interactive avec du branchement conditionnel. Et pour ça, je n'ai qu'un test manuel : lancer le programme et vérifier que le clavier fonctionne. Et j'ai encore plus de mots standards que je devrais ajouter à un moment comme DUP, DROP, SWAP, OVER, etc. que je voudrais pouvoir tester depuis Forth.

    Il existe un framework de test Forth, extrêmement simple, du nom de ttester, de John Hayes, ainsi que des versions dérivées. J'ai hâte de pouvoir l'utiliser. J'ai deux options. La première est de réécrire le code avec mon système de macros. Jouable, mais un peu long et fastidieux. La seconde est d'attendre d'avoir un interpréteur Forth fonctionnel... mais cela va m'obliger à écrire de nombreux mots sans tests, ce qui me déplaît. Je vais réfléchir à la question.

    Pour la partie gestion du clavier, je pense ajouter à mon framework de test une injection de caractères. Il restera le test du clavier à tester physiquement, mais la boucle principale pourra être testée avec des entrées simulées.

    La suite !

    Mon étape suivante va être de compléter le traitement du clavier avec les majuscules, minuscules et kanas. Puis le traitement des touches de directions, insertions, suppressions, etc. Sur cette partie, je ne pense pas faire d'article, cela va être très mécanique. Cependant, je devrai me diriger vers l'implémentation du mot ACCEPT, qui gère l'entrée clavier et la fournit dans le buffer TIB. Cela m'obligera à choisir une des options évoquées plus haut, et ce sera donc probablement le sujet de l'article suivant.

    Ah, et il sera peut-être temps de vérifier si tout cela fonctionne sur une vraie Famicom avec un vrai clavier Family BASIC !

    En attendant, voici une capture sur émulateur :

    Forth sur 6502 - écriture interactive à l'écran 2


  • Forth sur 6502, épisode 9 ()

    Froid, abandon, quitte...

    C'est triste un démarrage de Forth... COLD, ABORT, QUIT. On aurait pu imaginer des mots comme WARMUP, READY, LOOP. Mais je n'ai pas prévu de renommer les mots standards de Forth. Et comme indiqué dans l'article précédent, il est temps de déplacer le code de démarrage vers les mots officiels.

    Et à vrai dire, comme prévu, il n'y avait pas grand chose à faire.

    Tout d'abord, le code de démarrage devient juste initialiser l'interpréteur avec le mot COLD :

    boot_forth:
        ; Set the Work Register to the first word to execute:
        lda #<COLD_word_cfa
        sta REG_W
        lda #>COLD_word_cfa
        sta REG_W + 1
    
        ; And use it
        jmp docol
    

    Et pour faire simple, ABORT et QUIT appellent juste des mots cachés qui contiennent le code assembleur que j'avais déjà. Cela donne :

        ; COLD
        DEFINE_FORTH_WORD COLD, 0, 0
        .word RESET_ENV_word_cfa
        .word ABORT_word_cfa
        ; ABORT never returns, no need for DO_SEMI
    
        ; ABORT
        DEFINE_FORTH_WORD ABORT, COLD, 0
        .word RESET_STACK_word_cfa
        .word QUIT_word_cfa
        ; QUIT never returns, no need for DO_SEMI
    

    Reste QUIT qui est la boucle infinie. J'avais déjà une boucle infinie sous forme de mot assembleur, je l'ai renommée et voilà, terminé !

    Certes, ce n'est pas encore un vrai mot QUIT, qui doit traiter les entrées utilisateur et exécuter les commandes. Mais en tout cas, la structure est en place.

    Lire le clavier

    Et donc puisque tout cela n'était pas bien compliqué, et que QUIT va avoir besoin de lire le clavier, passons à la lecture du clavier.

    Accès au clavier

    Le clavier Family BASIC est connecté à la Famicom via le port d'extension. Celui qui est devant dans une Famicom classique. Avant de l'interroger, il faut écrire à l'adresse $4016 puis lire depuis l'adresse $4017.

    L'écriture à l'adresse $4016 contrôle le balayage du clavier. En effet, le clavier est une matrice de touches sur 9 lignes et 2 colonnes. La matrice est visible sur NESDev.

    L'écriture à $4016 utilise les 3 premiers bits pour contrôler le scan.

    Bit Description
    0 0 = scan normal ; 1 = réinitialiser le scan ligne 0, colonne 0
    1 Sélection de colonne
    2 Activer le clavier (1 = activé)
    3-7 inutilisés

    La lecture depuis $4017 récupère les données du clavier.

    Bit Description
    0 Inutilisé
    1-4 Données de colonne
    5-7 Inutilisés

    Un délai doit être respecté entre l'écriture à $4016 et la lecture depuis $4017 pour permettre au clavier de préparer les lignes de données.

    Scan dans le Forth

    Pour le scan dans le Forth, j'utilise trois tampons en mémoire qui sont chacun de taille 9 octets (un par ligne). Pour chaque ligne les informations des deux colonnes sont regroupées dans un seul octet.

    • Bits 0-3 : données de la colonne 0
    • Bits 4-7 : données de la colonne 1

    Le premier tampon sert au scan en cours. Un second sert à stocker le résultat du scan précédent. Le troisième sert à détecter les changements entre les deux scans (en faisant un XOR entre les deux autres tampons).

    Une fois les tampons remplis, le tampon de changements est analysé pour détecter les changements d'état des touches. Si un bit a changé dans le tampon de changements, l'état pressé/relâché est déterminé à partir du tampon de matrice actuel. Ceci est fait pour l'octet entier, donc si plusieurs bits ont changé dans une ligne, ils sont tous traités.

    Puis pour chaque bit modifié qui est aussi détecté comme un appui, une recherche dans une table de caractères fait la correspondance. Si le caractère est affichable, il est placé dans une file de caractères qui pourra être interrogée plus tard par le mot Forth KEY, qu'il reste à implémenter.

    Pour chaque bit modifié, je calcule aussi un code de touche ((ligne << 4) | numéro_de_bit), mais je ne l'utilise pas. Cela vient d'une idée précédente où je voulais tout d'abord générer les évènements de touches puis les décoder dans un second temps. Au final, j'ai trouvé ça bien compliqué pour cette machine.

    Ce qu'il reste à faire

    Plein de choses. Même en se limitant au clavier. Mais je pense que le plus long a été fait : le principe général de scan et de décodage. Il me reste à traiter les touches spéciales (comme SHIFT, CTRL, etc) et à implémenter le mot Forth KEY.

    J'ai prévu d'implémenter aussi le mot KEY? qui permet de savoir si une touche est disponible. Ainsi que KEYPRESSED? et KEYDOWN? pour interroger l'état de touches spécifiques.

    On peut écrire !

    En branchant rapidement la détection des touches avec un appel codé en dur à EMIT dans la boucle principale, je peux écrire à l'écran ! Le curseur n'avance pas, car lui aussi était un appel codé en dur, cependant, c'est assez satisfaisant.

    D'ailleurs, je pense que la prochaine étape sera le traitement de l'avancée du curseur depuis la boucle principale en Forth. Cela va m'obliger à implémenter quelques nouveaux mots, comme 0BRANCH (afin de faire des tests conditionnels).

    Forth sur 6502 - écriture interactive à l'écran


  • Forth sur 6502, épisode 8 ()

    Afficher des caractères

    Lors du précédent article, l'ajout de variables et de la pile des paramètres nous a approché de l'objectif actuel : afficher des caractères à l'écran. Ou plus exactement, remplacer l'affichage de la chaîne de caractères depuis l'assembleur au démarrage du programme vers la partie Forth.

    Pour commencer et s'assurer que j'ai du code qui peut transformer des coordonnées en adresse PPU et afficher un caractère, je crée un mot TEST_EMIT qui va prendre ces coordonnées et afficher un unique caractère comme curseur. Comme je n'ai que 26 lettres majuscules actuellement dans mes données graphiques, il est temps d'aller modifier les données. J'ajoute en caractères 255 un carré plein. Cela fera un bon curseur.

    Il me faut aussi deux variables pour stocker les coordonnées du curseur : CURSOR_X et CURSOR_Y que j'initialise à l'emplacement où je veux afficher le curseur au démarrage (en effet, je n'ai pas encore de moyen de modifier le contenu d'une variable facilement à partir d'un nombre).

        DEFINE_VARIABLE CURSOR_X, POST_LOGIC
        .word $0001
    
        DEFINE_VARIABLE CURSOR_Y, CURSOR_X
        .word $0002
    

    On peut noter que 16 bits pour des coordonnées d'écran en caractères, c'est beaucoup. On peut imaginer « compresser » les deux coordonnées dans une seule variable. Ça compliquera un peu l'écriture cependant. Je me laisse ça en note pour plus tard.

    Ah oui, autre chose. Peut-être que vous l'aviez remarqué avant moi, mais les variables que je définis sont actuellement dans le programme, et donc en ROM ! Pas terrible pour des variables. Je me note ça aussi pour plus tard (mais dans pas longtemps, car sinon, le curseur ne pourra pas bouger).

    Presque EMIT

    Quant à TEST_EMIT, il est en assembleur et il ressemble à ceci :

        ; Test word for hardcoded EMIT
        DEFINE_CODE_WORD TEST_EMIT, CURSOR_Y, 0
        jsr compute_screen_address  ; Now Addr has the screen address in PPU
    
        ; Warning, address endianness is swapped for vram_queue_set_ppu
        ; High byte first
        lda Addr
        sta VramQueuePtr+1
        lda Addr+1
        sta VramQueuePtr
        lda #$00        ; Horizontal increment
        jsr vram_queue_set_ppu
    
        ; Write character to buffer
        vram_t_set_buffer 1
        lda #'C'        ; Hardcoded character
        ldy #$00
        sta (VramQueuePtr), y
    
        END_TO_NEXT
    

    compute_screen_address est une sous-routine qui prend les coordonnées du curseur et calcule l'adresse PPU correspondante. C'est une partie dont je veux m'assurer avoir compris le fonctionnement (et l'écriture en 6502).

    J'utilise ensuite le système du template de projet pour communiquer avec le PPU.

    • VramQueuePtr sert dans un premier temps de variable paramètre pour vram_queue_set_ppu, qui positionne le registre d'adresse PPU. Plus exactement qui prépare la commande dans la liste d'affichage.
    • la macro vram_t_set_buffer réserve de l'espace (ici 1 octet) dans la liste d'affichage et fait pointer VramQueuePtr au début de cet espace. Le contenu de ce buffer sera transféré à l'adresse PPU spécifiée.

    À noter que l'allocation de l'espace retourne sans rien faire s'il n'y a plus de place. Comme je ne vérifie rien, cela peut être assez catastrophique. Pour le moment, j'affiche un seul caractère par rafraîchissement, ça va passer. Le système du template limite la taille d'un transfert à 64 octets et le buffer complet est de 256 octets. Je ne sais pas encore si je mettrai des protections en place ou bien si je laisse l'utilisateur gérer cela. Je n'aime pas trop l'idée de se retrouver avec un programme qui plante sans explication, mais d'un autre côté, c'est du Forth, pas du BASIC.

    Et ça marche, voici le résultat :

    Affichage du curseur sur Family Forth

    Des variables... pour de vrai ?

    Comme indiqué plus haut, le contenu de mes variables sont en ROM. Pas terrible. Comment faire pour avoir des variables définies par la ROM, mais dont le contenu est en RAM ? Une méthode classique est de transformer les variables en constantes. Je sais, dit comme ça, ça semble bizarre. Mais ça se comprend : on transforme une variable, qui contient donc sa propre valeur, en une constante qui contient l'adresse mémoire où la valeur sera stockée. Cet espace sera réservé en RAM.

    Après tout, le fonctionnement d'une variable en Forth n'est rien d'autre qu'un mot qui place une adresse mémoire sur la pile des paramètres. La variable place son propre PFA. Une constante place une valeur spécifiée.

    EMIT... pour de vrai aussi

    Pour le moment TEST_EMIT code en dur le caractère. EMIT, lui, doit envoyer à l'affichage le caractère dont le code est présent sur la pile. Pour avoir un vrai EMIT, il faut donc modifier TEST_EMIT pour qu'il récupère le code caractère depuis la pile des paramètres. Et cette valeur devra y être placée auparavant. Cela tombe bien, je viens d'implémenter le concept de constantes.

    Voici donc mon curseur :

        DEFINE_CONSTANT CURSOR_CODE, TEST_ADDR
        .word $FF
    

    Et la boucle principale Forth devient :

        DEFINE_FORTH_WORD MAIN_LOOP, MEMORY_TEST, 0
        .word READ_JOY_SAFE_word_cfa
        .word MEMORY_TEST_word_cfa
        .word CURSOR_CODE_word_cfa      ; On place le code caractère sur la pile
        .word EMIT_word_cfa             ; On appelle EMIT pour afficher ce caractère
        .word POST_LOGIC_word_cfa
        .word DO_SEMI_word_cfa
    

    Ça fonctionne. Pour un même résultat.

    Cependant, EMIT est censé aussi avancer la position du curseur. Ce que je peux brancher, mais qui va poser problème. Car la position avançant à chaque affichage, le curseur se retrouve affiché à chaque position de l'écran, et l'écran devient rapidement entièrement rempli d'une couleur unie.

    En attendant une vraie gestion de curseur, je replace le curseur à la position initiale à chaque itération de la boucle principale.

        ; MAIN LOOP word
        DEFINE_FORTH_WORD MAIN_LOOP, MEMORY_TEST, 0
        .word READ_JOY_SAFE_word_cfa
        .word MEMORY_TEST_word_cfa
    
        .word CURSOR_INIT_X_word_cfa
        .word CURSOR_X_word_cfa
        .word STORE_word_cfa
        .word CURSOR_INIT_Y_word_cfa
        .word CURSOR_Y_word_cfa
        .word STORE_word_cfa
    
        .word CURSOR_CODE_word_cfa
        .word EMIT_word_cfa
        .word POST_LOGIC_word_cfa
        .word DO_SEMI_word_cfa
    

    Ce qui équivaut en Forth à :

        : MAIN_LOOP
            READ_JOY_SAFE
            MEMORY_TEST
    
            CURSOR_INIT_X CURSOR_X !
            CURSOR_INIT_Y CURSOR_Y !
    
            CURSOR_CODE EMIT
            POST_LOGIC
        ;
    

    Moving Forth, épisode 6

    Revenons un peu à la lecture de la série d'articles Moving Forth avec la sixième partie. Après avoir présenté les primitives du langage, ce qui forme le « kernel », l'auteur aborde la notion de « high-level kernel ». Il s'agit du reste du système, celui qui permet l'interactivité et donc la compilation de nouveaux mots et leur exécution. Ce « high-level kernel » est écrit en Forth et se veut portable, contrairement au « kernel » contenant les primitives, écrit en assembleur.

    On y apprend que le démarrage de Forth est effectué par

    • un mot appelé COLD, qui s'occupe de l'initialisation des variables internes (de l'environnement je dirais),
    • qui lui-même appelle ABORT, qui réinitialise la pile de paramètres,
    • qui lui-même appelle QUIT, qui réinitialise la pile de retour, l'état de l'interpréteur, et lance la boucle principale de l'interpréteur.

    QUIT est une boucle infinie qui lit l'entrée utilisateur et appelle le mot INTERPRET, qui s'occupe de l'analyse de l'entrée.

    J'ai déjà le contenu de COLD et ABORT dans mon bootstrap de Forth. Il me faudra les implémenter en tant que mots Forth.

    Pour implémenter QUIT, il me faudra implémenter la réception de l'entrée utilisateur, et donc du clavier de la Famicom. Puis pas mal d'autres mots qui seront nécessaires pour INTERPRET.

    De là, l'article mentionne sans entrer dans le détail le concept de « vocabulaire », qui permet d'avoir plusieurs dictionnaires de mots, regroupés par fonctionnalités. Comme le Forth de l'article n'en a pas, il n'y a pas plus d'explications. J'ai prévu de mon côté d'implémenter cette notion.

    Puis l'auteur revient sur l'entête d'un mot et du drapeau « IMMEDIATE » que j'avais évoqué lors de la sixième partie et la création des mots complets. Lors de la compilation d'un mot (on verra ça plus tard), les mots trouvés sont normalement « compilés » dans le mot en train d'être définis. Sauf si le mot est marqué comme « IMMEDIATE », auquel cas il est exécuté immédiatement. Cela permet de contrôler la compilation. On peut considérer que c'est de la « méta-compilation », comme des macros qui vont effectuer des opérations pendant la compilation.

    Par exemple, IF est un mot « IMMEDIATE » qui va insérer des instructions de branchement conditionnel dans le mot en cours de définition, conjointement avec ELSE et THEN, qui sont aussi des mots « IMMEDIATE ».

    L'article décrit d'ailleurs rapidement la manière dont la compilation fonctionne :

    • la compilation démarre avec le mot :,
    • le mot est créé avec CREATE,
    • le CFA est initialisé pour pointer sur le DOCOL,
    • on passe en mode « compilation »,
    • chaque mot trouvé voit son CFA ajouté à la fin du PFA du mot en cours de définition,
    • lorsque le mot ; est trouvé, on ajoute le CFA de ;S (EXIT) à la fin du PFA
    • on repasse en mode « interprétation ».

    Il y a des choses en plus qui sont faites, comme rendre le mot visible à la fin de la compilation, l'insérer dans le dictionnaire du vocabulaire courant, traiter les mots « IMMEDIATE », etc... Mais le principe est là.

    Après avoir indiqué les mots qui sont impactés par le modèle d'exécution choisi, la fin de l'article montre le layout mémoire sur CP/M pour le Forth implémenté, ce qui ne m'intéresse pas trop ici, ainsi que différent types de layout de mots Forth.

    Je ne les reproduis pas ici, vous pouvez aller les voir dans l'article original. On y voit quatre Forth différents qui font chacun des choix différents. Mes choix sont globalement ceux de FIG-Forth, ce qui n'est pas étonnant puisque c'est le type de Forth que j'ai pu aborder dans mes expériences précédentes

    La suite

    On s'approche d'un système interactif, mais il reste encore un gros morceau côté Famicom plus que côté Forth : la gestion du clavier. Détecter qu'il est là, scanner les touches, les interpréter, en particulier les touches spéciales (peut-être dans un second temps). Si l'appui sur une touche provoque l'affichage d'un caractère à l'écran, on aura presque la première partie de l'interpréteur.

    Côté Moving Forth, c'est terminé. Il reste en fait deux articles, mais qui traitent de spécificités sur des architectures particulières. Cela s'éloigne trop du projet que j'ai. Je parcourrai peut-être une autre référence comme le livre « Starting Forth », qui me servira probablement de guide pour l'implémentation de QUIT et INTERPRET.

    À bientôt pour la suite !


  • Forth sur 6502, épisode 7 ()

    En route pour l'affichage

    Afficher du texte à l'écran

    L'étape d'aujourd'hui va permettre de se diriger vers l'affichage d'un texte à l'écran depuis la boucle Forth. Pour cela, il faut revenir un peu sur le fonctionnement de la Famicom.

    Le processeur qui s'occupe de l'affichage est le PPU (Picture Processing Unit). Ce processeur a son espace d'adressage mémoire propre de 16 ko dont le routage est configuré par la cartouche insérée. Dans la console, 2 ko de RAM sont dédiés au PPU, assez pour stocker les informations de deux écrans (index de caractères et attributs). La cartouche doit apporter a minima les informations de caractères (en ROM généralement, mais peut aussi offrir un espace RAM pour les construire) ; elle peut aussi étendre le nombre d'écrans (jusqu'à 4) ou ajouter un système de banking de pages.

    De manière générale, tout le mapping de la mémoire du PPU est contrôlé par la cartouche, à part les palettes. La cartouche la plus simple possible n'offrira que les 8 ko de ROM nécessaires aux deux tables de définition de caractères, mais on peut faire beaucoup plus complexe.

    À cela s'ajoute 256 octets de mémoire pour la gestion des sprites.

    Le PPU utilise toutes ses informations pour composer l'image à l'écran. Cependant le processeur 6502 n'a pas accès à cette mémoire. Pour y accéder, il existe des registres mappés dans l'espace d'adressage du CPU. Mais... il y a un mais. Lorsque le PPU est en train de faire le rendu, il a un besoin exclusif d'accès à sa mémoire. Le CPU doit donc attendre que le PPU soit dans une période où il n'a pas besoin d'accéder à sa mémoire : la période de « VBlank » (Vertical Blank), qui correspond au moment où l'écran n'est pas en train d'être rafraîchi (le balayage vertical revient en haut de l'écran).

    Par sécurité, pendant cette période, il faut désactiver le PPU, puis faire les modifications nécessaires et enfin réactiver le PPU. Il y a donc un temps assez bref pendant lequel on peut faire des modifications à la mémoire du PPU.

    Le squelette de projet que j'ai choisi utilise une pratique assez courante : construire une liste de commandes à envoyer au PPU. Lorsque la synchronisation verticale se produit, la liste préparée est traitée. Cette liste est limitée en taille, puisque le temps d'accès est limité.

    Tout cela pour dire que pour afficher un caractère à l'écran, il va falloir envoyer plusieurs commandes au PPU.

    • Tout d'abord positionner le registre d'adresse de la prochaine écriture en mémoire PPU.
    • Puis envoyer le caractère à écrire.

    Le mot Forth EMIT, qui affiche un caractère, devra donc faire tout cela à chaque fois, ce qui n'est pas très efficace. Mais c'est le plus simple à implémenter. Il est possible d'utiliser une incrémentation automatique de l'adresse d'écriture par le PPU, et celle-ci pourra être utilisée pour optimiser TYPE ou ." plus tard. Mais cela ne sera pas si simple, car le buffer de commandes est limité en taille et il faudra traiter la césure de chaînes trop longues.

    À voir. Le principe du projet est d'avoir un équivalent Forth au Family BASIC, pas un système optimisé.

    EMIT va donc ressembler à ceci :

    • récupération des coordonnées du curseur,
    • calcul de l'adresse PPU en fonction des coordonnées,
    • positionnement du registre d'adresse PPU (dans la liste de commandes),
    • écriture du caractère à l'adresse PPU (dans la liste de commandes),
    • mise à jour des coordonnées du curseur,
    • NEXT.

    Il faut donc maintenir les coordonnées du curseur quelque part, et cela signifie l'utilisation de variables.

    Les variables

    Les variables en Forth sont, comme tout le reste, des mots dans le dictionnaire. Lorsqu'une variable est exécutée, elle place l'adresse réservée pour son contenu sur la pile des paramètres, et c'est tout.

    Pour cela, un mot de type variable a pour CFA une routine qui place l'adresse PFA sur la pile des paramètres. Le Parameter Field, lui, est un espace mémoire réservé de deux octets qui contiendra la valeur de la variable.

    Avec la totalité d'un mot de type variable, cela fait 2 octets pour la valeur, 2 octets pour le CFA, 2 octets pour le chaînage, 1 octet pour la longueur du nom, et le nom lui-même. Soit un minimum de 8 octets par variable pour une variable à 16 bits. Cela peut sembler cher payer, mais ce système est autonome.

    Cela permet d'écrire, si la variable s'appelle CURSOR_X par exemple :

        CURSOR_X @  \ lit la valeur de la variable
        CURSOR_X !  \ écrit la valeur dans la variable
    

    Le mot @ (fetch) lit une valeur à l'adresse située au sommet de la pile des paramètres et place cette valeur sur la pile.

    Le mot ! (store) prend une adresse et une valeur sur la pile des paramètres, et écrit la valeur à l'adresse.

    Il nous faut donc écrire :

    • le code assembleur pointé par un mot de type variable (que j'appellerai DOVAR)
    • le mot @
    • le mot !
    • ... et donc s'occuper du fonctionnement de la pile des paramètres.

    Je n'implémenterai pas encore le mot VARIABLE qui permet de définir une nouvelle variable depuis Forth. Je vais écrire les variables directement en assembleur pour le moment.

    Pas si vite !

    Cela fait déjà beaucoup de choses à faire avant d'implémenter EMIT. J'ai besoin d'une étape intermédiaire pour m'assurer que tout fonctionne correctement. Pour cela, je vais réécrire le mot de test en Forth.

    Pour rappel, voici à quoi ressemble le mot de test en assembleur :

       ; A test word that writes $42 to $7FF
        DEFINE_CODE_WORD TEST, BRANCH, 0
        lda #$42
        sta $7FF
        END_TO_NEXT
    

    En Forth, cela s'écrit simplement (pourvu que la base des nombres soit en hexadécimal) :

        : TEST
            42 7FF C! ;
    

    Mais comme c'est le système de pile de paramètres et de variables que je veux tester, je vais plutôt écrire :

        : TEST
            TEST_VALUE @    \ lit la valeur de la variable TEST_VALUE
            TEST_ADDR @     \ lit la valeur de la variable TEST_ADDR
            !               \ écrit cette valeur de TEST_VALUE à l'adresse TEST_ADDR
            ;
    

    Retour à la pile des paramètres

    La pile des paramètres est une zone de mémoire en RAM, de 256 octets. Cela permet de l'indexer facilement en mode indirect avec le registre Y. Pour ne pas monopoliser le registre, je garde une variable d'un octet en Page Zéro. Cela nécessite cependant d'aller chercher et modifier la variable à chaque opération de pile.

    Ainsi, le haut de la pile sera à tout moment :

    • (ParamStack),y pour l'octet de poids faible avec y = REG_PSP
    • (ParamStack),y pour l'octet de poids fort avec y = REG_PSP + 1

    La pile grandit vers les adresses mémoire basses, avec l'index initialisé à 0 et décrémenté lors des PUSH, incrémenté lors des POP. Le premier emplacement de valeur 16 bits sera donc aux adresses indexées par $FE et $FF.

    Je modifie un peu mes macros de définition de mots car FETCH et STORE doivent s'encoder en tant que mots avec les symboles @ et !, mais la génération des labels pour l'assembleur ne peut pas utiliser ces symboles. J'ajoute donc la possibilité de différencier le nom du mot et le symbole utilisé dans le dictionnaire.

    FETCH fait les actions suivantes :

    • récupérer l'adresse au sommet de la pile des paramètres
    • la placer dans un registre d'adresse Page Zero temporaire
    • lire la valeur à cette adresse (2 octets)
    • placer cette valeur sur la pile des paramètres

    STORE fait les actions suivantes :

    • récupérer l'adresse au sommet de la pile des paramètres
    • la placer dans un registre d'adresse Page Zero temporaire
    • récupérer la valeur suivante sur la pile des paramètres (2 octets)
    • écrire cette valeur à l'adresse dans le registre temporaire

    Le mot de test en Forth

    Le mot final de test en Forth devient, accompagné du code des deux variables :

        ; TEST_VALUE, a variable to hold a test value
        DEFINE_VARIABLE TEST_VALUE, STORE
        .word $4200
    
        ; TEST_ADDR, a variable to hold a test address
        DEFINE_VARIABLE TEST_ADDR, TEST_VALUE
        .word $07FE
    
        ; A test word that writes $4242 to $7FE (16 bits)
        DEFINE_FORTH_WORD NEW_TEST, TEST_ADDR, 0
        .word TEST_VALUE_word_cfa
        .word FETCH_word_cfa
        .word TEST_ADDR_word_cfa
        .word FETCH_word_cfa
        .word STORE_word_cfa
        .word DO_SEMI_word_cfa
    

    C'est... beaucoup plus long que les deux instructions d'assembleur initiales. Cela ne fait pas non plus exactement la même chose, car j'ai implémenté tous ces mots pour des valeurs de 16 bits. Plutôt que d'écrire $42 à l'adresse $7FF, j'écris $4200 à l'adresse $7FE (avec l'endianness, cela écrit bien $42 à $7FF). Cela fait passer le test et même si c'est un peu différent, cela teste bien le fonctionnement de la pile des paramètres et des variables.

    Conclusion

    Pas de lecture d'article Moving Forth aujourd'hui, il y a déjà assez de matière ici. J'ai une pile de paramètres fonctionnelle (mais non protégée), un système de variables, deux instructions élémentaires de manipulation de la pile.

    Je m'approche du nécessaire pour l'implémentation de EMIT, qui sera très probablement le sujet du prochain article.


  • Forth sur 6502, épisode 6 ()

    Des mots complets

    Depuis le début de l'implémentation de ce Forth sur 6502, j'ai parlé de « pseudo mots ». Ces mots ont un CFA (Code Field Address) et un PFA (Parameter Field Address), mais pas d'entête de mot ni de principe de dictionnaire.

    Or pour qu'un mot soit complet en Forth, il faut ces deux autres concepts. Dans cette implémentation je vais utiliser deux autres sections : le NFA (Name Field Address) et le LFA (Link Field Address).

    Ce qui donne la structure complète suivante :

    • NFA : entête du mot, dont le premier octet est la longueur du nom du mot, suivi du nom lui-même. Seuls les 5 bits de poids faible sont utilisés pour la longueur (ce qui limite la longueur des noms à 31 caractères). Le bit de poids fort est toujours à 1, les deux autres bits seront vus plus tard. De plus, le dernier octet du nom a son bit de poids fort à 1 pour indiquer la fin du nom.
    • LFA : adresse du mot précédent dans la liste chaînée des mots. Tous les mots disponibles dans ce Forth forment une liste chaînée, chaque mot pointant vers le mot défini avant lui. Le dernier mot défini pointe vers l'adresse 0 pour indiquer la fin de chaîne.
    • CFA : adresse du code machine qui sera exécuté lorsque le mot sera appelé.
    • PFA : paramètres du mot, s'il en a.

    Précision : cette implémentation d'un mot complet est celle que l'on trouve dans des Forth historiques 8 bits. Mais elle n'est pas obligatoire, il peut y avoir des variantes. Rien n'empêche d'inverser l'ordre du NFA et du LFA. Ou de terminer le nom par un octet nul plutôt que par un marqueur dans le bit de poids fort.

    En regardant des normes plus récentes (section 3.3), on voit qu'il existe un « name space » un « code space » et un « parameter space », et que les mots sont organisés dans un « dictionnaire », cependant, il est laissé à l'implémentation de faire les choix de représentation.

    Implémentation

    Pour implémenter ces mots complets, j'ajoute des macros pour faciliter l'écriture (et surtout la réécriture en cas de changement d'idée).

    Mon mot de test, qui permet de valider que le programme exécute bien la boucle, était comme ceci :

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

    Il devient maintenant :

        DEFINE_CODE_WORD TEST, BRANCH, 0
        lda #$42
        sta $7FF
        END_TO_NEXT
    

    La macro DEFINE_CODE_WORD prend trois paramètres : le nom du mot, le mot précédent (pour le chaînage) et un drapeau pour les mots immédiats, que l'on n'a pas encore abordé. La macro END_TO_NEXT termine le mot en appelant NEXT.

    Ainsi, pour le mots Forth définis par du code assembleur, il ne reste apparent que le code lui-même, encadré par les macros.

    J'ai une seconde macro qui permet de définir des mots exécutés par « DOCOL », c'est-à-dire des mots écrits comme une série de pointeurs vers d'autres mots.

    La boucle principale devient :

        DEFINE_FORTH_WORD MAIN_LOOP, TEST, 0
        .word READ_JOY_SAFE_word_cfa
        .word TEST_word_cfa
        .word POST_LOGIC_word_cfa
        .word DO_SEMI_word_cfa
    

    Moving Forth, épisode 5

    Avançons dans la lecture de la série d'articles Moving Forth avec la cinquième partie.

    L'auteur présente le code source d'un ANSI Forth, CamelForth, pour les processeurs Z80, 8051 et 6809. Passons. Ce qui m'intéresse le plus, c'est la notion de « kernel ». À partir de quel ensembles de mots de base écrits peut-on construire un Forth complet ? Et surtout, combien de mots doit-on implémenter en assembleur avant de construire le reste par dessus ?

    C'est une question que je me posais dès le premier article dans la conclusion. Je me disais que pour faciliter les choix d'implémentation, il serait intéressant d'avoir un minimum de mots implémentés en assembleur.

    Cependant, ce dont nous prévient l'article, c'est que même s'il est théoriquement possible de se limiter à 13 primitives, dans la pratique, faire ce choix entraînera un Forth peu véloce.

    L'auteur propose une liste de critères pour décider si un mot doit être implémenté en assembleur ou en Forth :

    • la base arithmétique, logique et mémoire le sont. Cela semble évident.
    • si le mot est difficile à écrire en Forth (ou de manière trop complexe), alors il devrait être en assembleur.
    • si le mot est très souvent utilisé, il devrait être en assembleur.
    • si le code assembleur est plus compact (ou vraiment plus efficace) que le code Forth, il devrait être en assembleur.
    • si la logique du mot est complexe, il devrait être en Forth.

    Avec ces critères, l'auteur arrive à une liste de 70 primitives dans son implémentation. Cela donne une idée d'échelle.

    Il décrit enfin une boucle de développement pour l'implémentation d'un Forth. Je suis déjà parti sur la mienne, mais cela est intéressant à regarder.

    L'auteur implémente toute sa base en premier, puis assemble et corrige les erreurs. Puis écrit du code qui affiche un premier caractère une fois l'initialisation complète. Puis il écrit un mot entièrement en Forth et vérifie le fonctionnement de NEXT, DOCOL et ;S/EXIT.

    Enfin, il implémente les branchements et DODOES globalement en même temps qu'avoir un interpréteur interactif rudimentaire afin de tester ses mots.

    Globalement, c'est similaire à ce que je suis en train de faire. J'ai écrit un octet en mémoire plutôt qu'un caractère à l'écran, car c'était plus simple. Mais l'idée est la même.

    De plus, je teste de manière automatique, et je compte bien continuer. Même si on va voir que ça va se compliquer un peu.

    Conclusion et prochain épisode

    J'ai à présent une liste de mots de deux types différents formant un dictionnaire. Ce que je voudrais maintenant est afficher un message à l'écran. J'ai déplacé et changé le message écrit par le squelette de projet pour me donner un modèle, et je veux à présent faire la même chose, mais dans la boucle Forth.

    J'ai commencé quelques essais et des questions se posent sur la granularité des mots. Je vais devoir implémenter le mot EMIT. Celui-ci aura besoin de conserver une position à l'écran. Pour cela, je vais avoir besoin de variables.

    Pour transformer ces variables en adresse mémoire, je peux soit tout écrire en assembleur (ce que je fais probablement faire dans un premier temps), mais aussi en profiter pour implémenter quelques mots de traitement arithmétique.

    Bref, à la prochaine !


Page 1 / 26 (suivant) »

Tous les tags

3d (15), 6502 (10), 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 (7), Forth (13), 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 (15), 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 10
Forth sur 6502, épisode 9
Forth sur 6502, épisode 8
Forth sur 6502, épisode 7
Forth sur 6502, épisode 6
Forth sur 6502, épisode 5
Forth sur 6502, épisode 4
Forth sur 6502, épisode 3
Forth sur 6502, épisode 2
Forth sur 6502, épisode 1

Atom Feed

Réseaux