Site logo

Triceraprog
La programmation depuis le Crétacé

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 !