Site logo

Triceraprog
La programmation depuis le Crétacé

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