Adressage mémoire sur NES et utilisation des mappers

De T.R.A.F - Wiki
Aller à : navigation, rechercher

Introduction

Dans la vie des bidouilleurs de ROM que nous sommes, le problème de la place disponible dans le jeu que l'on modifie se pose très souvent. Y a-t-il assez de place pour insérer le texte traduit dans la ROM? Pourrais-je faire rentrer mes nouveaux menus flamboyants mais qui occupent deux fois plus de place que les originaux?

Il est donc bien naturel de se demander s'il n'est pas possible de passer outre cette barrière presque inviolable de la taille de la ROM! Cette pratique est fort heureusement réalisable, et souvent mise en oeuvre, notamment sur SNES (voir Les pointeurs NES et Super NES). C'est ce que l'on appelle plus communément l'extension de ROM.

Mais, alors que les jeux SNES sont très souvent étendus, cette pratique reste plutôt sporadique lors du hacking de jeux NES. Flemme du romhackeur? Problème lié à la NES en elle-même? C'est ce que nous allons tenter, dans un premier temps, de découvrir ensemble.

Quelques rappels sur le fonctionnement de la NES

Il est nécessaire de présenter le fonctionnement de la NES afin de bien comprendre les méchanismes mis en oeuvre par celle-ci pour exploiter les données des ROMS.

Architecture simplifiée

Quelques définitions

Le processeur central (CPU) : C'est le coeur de la bête. C'est lui qui "exécute" les instructions qui composent le programme du jeu. Il manipule des données, en particulier des octets de données dans le cas de la NES (c'est une console dite "8 bits"), par l'intermédiaire de ses registres.

Registres : Ce sont les "petites mains" du processeur. Ils peuvent s'apparenter à des cases mémoires spéciales, sur lesquelles le processeur peut effectuer des opérations. Chacun de ces registres peut stocker et manipuler des valeurs codées sur 8 bits.

La mémoire de travail (WRAM) : Le processeur s'en sert pour stocker les données qu'il manipule au cours de l'exécution du programme.

Le processeur graphique (PPU) : Le second processeur de la console, dédié au rendu des graphismes. Il dispose de sa propre mémoire nommée VRAM.

Bus de données : C'est par son intermédiaire que les données "circulent" entre les différents composants de la console. Il a une largeur de 8bits, à l'instar des registres du processeur.

Bus d'adresse : Utilisé par le processeur pour communiquer les adresses auquelles il souhaite accéder. Ce bus a une largeur de 16 bits sur la NES.

Architecture

Nous allons maintenant jeter un oeil à l'architecture de la NES (simplifiée pour l'occasion) ainsi que les différents composants principaux.

   +-----------------------------------+ 
   ¦            Bus d'adresses         ¦
   +  +-------------+  +------------+  +
   ¦  ¦             ¦  ¦            ¦  ¦
   \  /             ¦  ¦            \  /
    \/              ¦  ¦             \/
+-----------+    +--+--+----+    +----------+
¦ Registres ¦    ¦          ¦    ¦          ¦
¦   E/S     ¦    ¦          ¦    ¦          ¦
¦ +-------+ ¦    ¦          ¦    ¦          ¦
¦ ¦  PPU  ¦ ¦    ¦   CPU    ¦    ¦   ROM    ¦
¦ +-------+ ¦    ¦          ¦    ¦          ¦
¦ +-------+ ¦    ¦          ¦    ¦          ¦
¦ ¦Autres ¦ ¦    ¦          ¦    ¦          ¦
¦ +-------+ ¦    ¦          ¦    ¦          ¦
+-----------+    +----------+    +----------+
     /\               /\             ¦  ¦
    /__\             /__\            ¦  ¦
    ¦__¦             ¦__¦            ¦__¦
    \  /             \  /            \  /
     \/               \/              \/
+-------------------------------------------+ 
¦               Bus de données              ¦
+-------------------------------------------+

Le processeur est connecté aux autres composants de la console par l'intermédiaire du bus de données et du bus d'adresses. Lorsqu'il souhaite lire un octet de donnée dans la mémoire de travail, il doit écrire l'adresse de cet octet sur le bus d'adresse, ce qui lui permet de récupérer la valeur de l'octet en question sur le bus de données.

De même, l'accès aux données contenues dans la ROM ou même la communication avec les autres périphériques de la console (PPU, ...) se fait par des lectures et écritures de données à des adresses particulières. Par exemple, pour envoyer des instructions au processeur graphique (PPU), il faudra écrire des données à des adresses spéciales. Ce sont les "mapped I/O port".

Il existe donc une cartographie complète des adresses accessibles par le processeur, que l'on nomme l'espace adressable.

Espace adressable de la NES

Comme nous venons de le voir, le bus d'adresse de la NES a une largeur de 16 bits. Cela revient à dire que la console peut accéder à 2^16, soit 65535 (10000h) adresses différentes. Les différentes mémoires accessibles par la NES (RAM, ROM) ainsi que les différents périphériques (PPU, manettes) se partagent donc cet espace adressable.

Par la suite les adresses seront écrites sous forme hexadécimale, par exemple 1234h. La taille de certaines plages d'adresses sera notée en Kilo-octets, par exemple 8K (8K = 8192 = 2000h).

Le schéma suivant présente cette répartition (de manière simplifiée) :

Début-Fin    Taille    Accès    Description
0000h-07FFh    2K       L,E     La mémoire de travail (WRAM)
2000h-2007h     8       L,E*    Registres PPU (Pour les graphismes)
4000h-4017h    18       L,E*    Registres APU (Pour le son) et controlleur manettes 
4018h-5FFFh   ~8K       L,E**   Zone d'extension mémoire
6000h-7FFFh    8K       L,E**   WRAM/SRAM cartouche
8000h-FFFFh   32K        L      ROM
L : Accessible en lecture
E : Accessible en écriture
* : Cela dépend des registres
** : Pas forcément "remplie"

0800h-0FFFh, 1000h-17FFh et 1800h-1FFFh correspondent à des mirroirs de la zone 0000h-07FFh. C'est à dire que tout accès à ces adresses correspondent à un accès à 0000h-07FFh. De même, les adresses 2008h-3FFFh sont des mirroirs de 2000h-2007h.

Nous ne détaillerons pas les différents registres PPU, APU, etc de la machine. Ceux-ci sont très bien décrits dans la documents suivants : ...

L'information la plus importante à noter dans cette cartographie mémoire est sans doute l'espace adressable alloué à la ROM. En effet, les données de la ROM seront accessible (en lecture seule, car c'est une Read Only Memory) grâce à la plage d'adresses qui s'étend de l'adresse 8000h à l'adresse FFFFh. C'est là que toi, lecteur à l'esprit vif et aiguisé, tu réfléchis 30 secondes et tu me dis "Mais ça ne fait que 32 Kilo-octets pour la ROM, c'est trois fois rien!". Et cette remarque est extrêment pertinente : matériellement, la taille des programmes, des jeux, est limitée à 32K par l'architecture de la console. Hors il s'agit là d'une contrainte très forte imposées aux développeurs, et limitant l'envergure des jeux réalisés.

Fort heureusement, Nintendo et les développeurs ont bien vite trouvé des moyens de contourner ce problème, et permis l'écriture de programme de plus de 32K. Ce sont les "mappers", dont nous allons détailler le fonctionnement dans la section suivante.


Les mappers

Face à la limite de l'espace adressable de la NES inhérente à l'architecture de celle-ci, les développeurs ont trouvé un moyen pour que le processeur de la console puisse accéder à plus que 32KB de ROM. Le principe est simple : seuls 32KB de ROM sont accessibles à un instant donné, mais il y a peut-être moyen d'utiliser des morceaux de 32KB différents au fil de l'exécution du programme. C'est exactement ce que fait un mapper. Ce petit composant supplémentaire situé dans la cartouche du jeu permet de changer à la demande des blocs de mémoire dans l'espace adressable dédié à la ROM.

Nous utiliserons à partir de maintenant le terme "bank" pour désigner un bloc de mémoire que manipule un mapper. Une bank correspond à un composant mémoire dans la cartouche. Les mappers (car il y en a plusieurs types comme nous le verrons bientôt) manipulent des blocs de mémoire de tailles pré-définies qui correspondent toujours à des puissances de 2 : 8K, 16K ou 32K.

Le mapper s'interface entre la console et le contenu de la cartouche du jeu. Lorsque le programme tente d'accéder à des données de la cartouche, le mapper, en fonction de sa configuration actuelle, "aiguille" la demande vers un des composants mémoire de la cartouche.

                   +->Bank 0
@ ===> Mapper -----+  Bank 1
                       ...
                      Bank N

Le processeur peut envoyer au mapper des instructions de changement de bank par l'intermédiare de registres d'entrée/sortie spécifiques au mapper. Ces registres sont accessibles à des adresses particulières dans l'espace adressable. Pour la plupart des mappers, ce sont des adresses dans l'espace adressable de la ROM. En lecture ces adresses pointent sur les données de la ROM, et en écriture sur les registres du mapper.

Un exemple théorique

Afin d'illustrer un peu tout ça, prenons un exemple très simple. Supposons que notre mapper manipule des banks de 16K et que le jeu qui nous intéresse fait 64K. Seule la moitié du jeu peut donc "tenir en mémoire" à un instant donné, et le jeu est composé lui-même de 4 banks.

L'espace adressable alloué à la ROM faisant 32K, et les banks manipulées faisant 16K, deux banks peuvent donc être chargées en même temps dans cet espace.

Tout ceci peut être schématisé de la sorte :

Mémoire NES
8000h-BFFFh : Emplacement 0
C000h-FFFFh : Emplacement 1
ROM
0000h-3FFFh : Bank 0
4000h-7FFFh : Bank 1
8000h-BFFFh : Bank 2
C000h-FFFFh : Bank 3

Supposons maintenant que notre mapper dispose de deux registres :

  • Le registre INDEX, accessible en écriture à l'adresse 8000h ;
  • Le registre NUMERO_BANK, accessible en écriture à l'adresse 8001h.

Le premier sert à indiquer au mapper à quel endroit on souhaite "charger" une bank, et le second à indiquer quelle bank doit être chargée.

Admettons que par défaut les deux premières banks de la ROM sont chargées aux adresses 8000h-FFFFh. Nous sommes dans la configuration suivante :

Mémoire NES               ROM
                          
8000h-BFFFh --------->    0000h-3FFFh : Bank 0
C000h-FFFFh --------->    4000h-7FFFh : Bank 1
                          8000h-BFFFh : Bank 2
                          C000h-FFFFh : Bank 3

Dans cette configuration, une lecture par le processeur à l'adresse D000h accèdera à l'adresse 1000h de la bank 1 (soit l'adresse 5000h dans la ROM).

Imaginons qu'à un certain moment, le jeu ait besoin de charger une chaîne de caractère stockée dans la bank 2. Cette bank n'est pas accessible et il faut donc configurer le mapper pour qu'elle le devienne. La procédure pour "charger" une nouvelle bank est en deux étapes :

  • indiquer dans quel emplacement on souhaite charger la nouvelle bank (dans notre cas, il y a deux possibilités, 8000h ou C000h) ;
  • indiquer quelle bank va être chargée à cet emplacement.

Supposons que l'on veuille charger la bank 2 à l'emplacement C000h-FFFFh. La suite d'instructions nécessaire sera donc de la forme :

  • Ecrire la valeur 1 (on charge la bank dans le second emplacement) dans le registre INDEX ;
  • Ecrire la valeur 2 (on charge la bank numéro 2) dans le registre NUMERO_BANK.

Ceci nous amène à la configuration suivante :

Mémoire NES               ROM
                          
8000h-BFFFh --------->    0000h-3FFFh : Bank 0
                          4000h-7FFFh : Bank 1
C000h-FFFFh --------->    8000h-BFFFh : Bank 2
                          C000h-FFFFh : Bank 3

Maintenant, une lecture à l'adresse D000h accèdera à l'adresse 1000h de la bank 2 (soit l'adresse 9000h dans la ROM).

A titre informatif, la séquence d'instruction en assembleur 6502 pour ce chargement de bank pourrait ressembler à :

LDA #$01  	; emplacement concerné
STA $8000 	; écrire la valeur dans le registre INDEX
LDA #$02 	; numéro de la nouvelle bank
STA $8001 	; écrire la valeur dans le registre NUMERO_BANK

Cet exemple illustre de manière très basique le fonctionnement d'un mapper. Cependant, il existe un très grand nombre de mappers différents, tous disposants de leurs propres fonctionnalités et modes de fonctionnement. Il est alors indispensable de se référer à la documentation relative au mapper que l'on manipule. *donner liens sur documents*


Quelques généralités

Les banks figées

Il est d'ores et déjà important de noter qu'il n'est pas possible ou en tout cas fort peu recommandé de changer la bank dans laquelle se trouvent les instructions du programme en train d'être exécutées. Cependant, très souvent un des emplacements mémoire pointe toujours vers la même bank de la ROM au cours de l'exécution du jeu. On dit que la bank est branchée en dur à cet emplacement ("hard-wired" en anglais). Bien souvent cette bank contient les morceaux de codes qui permettent de changer de bank.

Emplacements variables

Selon la taille des banks manipulées par le mapper (8K, 16K, 32K, ...), l'espace adressable concernant la ROM ne se découpe pas de la même manière. Voici quelques cas de figure :

Taille bank   Emplacements
8K            8000h-9FFFh, A000h-BFFFh, C000h-DFFFh, E000h-FFFFh  
16K           8000h-BFFFh, C000h-FFFFh
32K           8000h-FFFFh

Le format de fichier iNES

Il s'agit du format de fichier d'image de ROM NES le plus courant. Il est intéressant de le présenter ici car il permet de comprendre comment les différentes banks du programme sont stockées à l'intérieur de la ROM. Sa structure générale est la suivante :

  • En-tête (header) de 10h octets ;
  • Les banks du programmes (PRG-ROM) ;
  • Les banks de données graphiques (CHR-ROM).

L'en-tête contient entre autres l'identifiant du mapper utilisé, le nombre de banks de PRG-ROM et le nombre de banks de CHR-ROM. Nous n'avons jusqu'à présent pas parlé du fonctionnement de la mémoire vidéo et des banks de données graphiques, mais le fonctionnement est similaire à celui des données du programme.

Autres fonctionnalités des mappers

Les fonctionnalités des mappers ne s'arrêtent pas à l'adressage de différents blocs mémoire. De nombreux mappers permettent par exemple, à l'instar de ce qui est fait avec les données du programme, de charger à l'exécution des banks de données graphiques utilisées par la PPU. Les mappers peuvent aussi servir à générer des interruptions lorsque certains évênements se produisent.

Dans la section suivante nous allons présenter un des mappers les plus courants : le mapper 4 ou MMC3.

Le mapper 4 : MMC3

C'est sans conteste le mapper le plus utilisé par les jeux NES, et non sans raisons comme nous allons le voir. Les fonctionnalités du MMC3 sont les suivantes :

  • Changement de PRG-ROM ;
  • Changement de CHR-ROM ;
  • Changement du mode de "screen mirroring" ;
  • Utilisation d'une extension de mémoire (SRAM/WRAM) ;
  • Génération d'interruption en fonction de la scanline.


Le MMC3 manipule des banks de PRG-ROM de 8K de taille. Le nombre maximal de ces banks est 64. Au total, il est donc possible d'utiliser jusqu'à 512K de PRG-ROM. En outre, il est possible d'utiliser jusqu'à 8K de PRG-RAM, qui est une extension de mémoire de travail (WRAM) stockée sur la cartouche. Enfin, concernant la mémoire graphique, le MMC3 peut manipuler jusqu'à 256K de CHR-ROM ou 8K de CHR-RAM (mémoire graphique accessible en écriture). Les banks de CHR-ROM sont soit de taille 1K ou 2K.

Les registres du MMC3

Le MMC3 dispose de 8 registres répartis en 4 paires de registres. Chacune de ces paires de registres est accessible dans une plage d'adresses donnée. Le premier registre de la paire sera accessible via toutes les adresses paires de la paire, et le second registre via les adresses impaires.

Par exemple, le premier registre sera accessible par toutes les adresses paires entre 8000h et 9FFEh, soit : 8000h, 8002h, 8004h, ..., 9FFEh.

Nous allons détailler ces registres au fur et à mesure, en voyant les différentes fonctionnalités du mapper MMC3.

Changement de PRG-ROM

Registres concernant le changement de banks --

Registre de sélection de bank - 8000h-9FFEh, adresses paires

7 6 5 4  3 2 1 0
C P . .  . A A A
| |        | | |
| |        +-+-+----> Adresse de la bank à changer (0 .. 7)
| +-----------------> Mode de sélection de PRG-ROM (0 ou 1)
+-------------------> Mode de sélection de CHR-ROM (0 ou 1)

Les valeurs de A comprises entre 0 et 5 désignent un changement de bank de CHR-ROM. Seules les valeurs 6 et 7 désignent un changement de bank de PRG-ROM.

Registre de données (numéro de la bank à charger) - 8001h-9FFFh, adresses impaires

7 6 5 4  3 2 1 0
D D D D  D D D D
+-+-+-+--+-+-+-+----> Numéro de la bank à charger à l'emplacement sélectionné grâce au registre de sélection de bank.

Selon le mode de sélection de PRG-ROM choisit, la valeur AAA du registre de sélection de bank ne désigne pas les mêmes emplacements mémoire.

Mode C=0     Fixe
8000h-9FFFh         Valeur du registre de données D pour A=6
A000h-BFFFh         Valeur du registre de données D pour A=7 
C000h-DFFFh    *    Avant dernière bank
E000h-FFFFh    *    Dernière bank

Dans ce mode, seules les deux premières plages mémoire peuvent pointer vers différentes banks. Les deux dernières pointent toujours vers l'avant dernière et la dernière bank respectivement.

Mode C=1     Fixe
8000h-9FFFh    *    Avant dernière bank
A000h-BFFFh         Valeur du registre de données D pour A=7 
C000h-DFFFh         Valeur du registre de données D pour A=6
E000h-FFFFh    *    Dernière bank

De manière similaire, selon le mode de sélection de CHR-ROM, la valeur AAA désigne différents emplacements de mémoire graphique. Pour plus de détail, voir la documentation complète du MMC3.

Protocole d'utilisation

De manière similaire à ce que nous avons vu dans le chapitre d'introduction aux mappers, le changement de bank se passe en deux étapes: indiquer l'emplacement mémoire où l'on souhaite charger une bank, et dans un deuxième temps, indiquer la bank à charger.

Il faut donc d'abord sélectionner l'emplacement mémoire dans lequel on désire charger la bank de PRG-ROM grâce au registre de sélection de bank (8000h). La valeur à écrire dans ce registre dépend du mode de sélection de PRG-ROM, ainsi que de la bank que l'on souhaite changer (parmi les deux banks possibles).

Exemple pratique

Supposons que l'on utilise le mode 0 de sélection de PRG-ROM (P=0). Les deux dernières banks de 8K sont donc fixes, et l'on ne peux changer que les deux premières (8000h-9FFFh et A000h-BFFFh). Le jeu en question est composé pour sa part de 8 banks de 8K (numérotées de 0 à 7).

On suppose par ailleurs que le mode de sélection de CHR-ROM est le mode 1 (C=1).

Dans la configuration initiale, l'espace adressable de la ROM est le suivant :

8000h-9FFFh ----> Bank 0
A000h-BFFFh ----> Bank 1
                  Bank 2
                  Bank 3
                  Bank 4
                  Bank 5
C000h-DFFFh ----> Bank 6 *
E000h-FFFFh ----> Bank 7 *
* Dans ce mode, ces banks ne peuvent être changées.

Supposons maintenant que l'on souhaite rendre accessible la bank 3 de la ROM dans la plage d'adresse A000h-BFFFh (soit le deuxième emplacement mémoire).

La valeur à écrire dans le registre de sélection de bank est donc une combinaison (OU logique) du bit C, du bit P, et de la valeur A. Dans notre cas, la valeur de A nécessaire pour changer la bank à l'emplacement A000h-BFFFh, en mode P=0, est 7 (cf. ci-dessus).

La valeur finale est donc en binaire :

C P . . . A A A
1 0 0 0 0 1 1 1

soit 87h en hexadécimal.

Une fois cette valeur écrite dans le registre de sélection de bank (8000h), il suffit d'écrire l'indice de la nouvelle bank à charger dans le registre de données (8001h), soit la valeur 3.

Le code équivalent en assembleur est :

LDA #$87
STA $8000
LDA #$03
STA $8001

On arrive finalement dans la configuration suivante :

8000h-9FFFh ----> Bank 0
                  Bank 1
                  Bank 2
A000h-BFFFh ----> Bank 3
                  Bank 4
                  Bank 5
C000h-DFFFh ----> Bank 6 *
E000h-FFFFh ----> Bank 7 *

Changement de CHR-ROM

L'utilisation du MMC3 pour changer les banks de données graphiques accessibles dans l'espace mémoire vidéo fonctionne exactement de la même façon. L'emplacement où charger la nouvelle bank est désigné par une combinaison du mode de sélection de CHR-ROM et de l'adresse de la ROM : A. Pour plus d'informations à ce sujet, se référer à un des documents présenté en annexe.

Sélection du mode de "Mirroring"

Cette fonctionnalité concernant le rendu graphique de la console, elle ne sera pas présentée en détail ici. Pour plus de renseignements sur les différents modes de "mirroring", merci de se référer à la documentation de la PPU de la NES.

Registre de sélection de mode de "mirroring" - A000h-BFFEh, adresses paires

7 6 5 4  3 2 1 0
. . . .  . . . M
               +----> 0 : Mirroring vertical
               +----> 1 : Mirroring horizontal

Par ailleurs, la valeur de ce registre sera ignorée si le jeu utilise 4 écrans.


Utilisation d'une extension mémoire

Le MMC3 permet de charger une bank de mémoire de travail supplémentaire (PRG-RAM) dans l'espace adressable de la console. Celle-ci, d'une taille maximale de 8K, sera localisée dans la plage d'adresse 6000h-7FFFh. Le MMC3 dispose d'un registre pour autoriser/interdire les accès à cette mémoire supplémentaire, ainsi que pour bloquer les accès en écriture.

Registre d'utilisation de la WRAM supplémentaire - A001h-BFFFh, adresses impaires

7 6 5 4  3 2 1 0
E W . .  . . . .
| |
| +------------------> Protection en écriture (0 : écritures autorisées, 1 : écritures interdites)
+--------------------> Activation de la WRAM (0 : desactivée, 1 : activée)


Génération d'interruptions

La génération d'interruptions basées sur la scanline actuelle permet de faire varier le rendu entre différentes parties verticales de l'écran. Un exemple d'utilisation simple est la barre de statut de Super Mario Bros 3. Celle-ci est "statique" et reste toujours à la même position sur l'écran, alors même que le niveau de jeu défile. Cet effet est réalisé grâce aux interruptions du MMC3.

Le MMC3 dispose de 4 registres pour configurer et utiliser ces interruptions.

Cette fonctionnalité, de par sa complexité, ne sera pas présentée ici en détail. Pour apprendre à l'utiliser, il est conseillé de consulter le document de Disch! concernant le MMC3.


Exemple complet

Maintenant que nous avons abordé les différentes fonctionnalités du mapper MMC3, il est temps de nous intéresser à un exemple complet de modification d'un jeu l'utilisant.

Problèmes courants et solutions

Identifier le mapper utilisé et les routines associées

Changer la bank dans laquelle on se trouve

Modifier l'adresse de retour dans la pile

Exécuter du code depuis la RAM

Le jeu n'utilise pas de mapper ou pas le bon mapper

ajouter / changer le mapper

Conclusion