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),ypour l'octet de poids faible avecy = REG_PSP(ParamStack),ypour l'octet de poids fort avecy = 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.