Cartouche et minuteur
Gameboj – Étape 6

1 Introduction

Le but de cette étape est de compléter votre simulateur en lui ajoutant les éléments suivants :

  • la mémoire de démarrage, qui contient un programme chargé d'initialiser le Game Boy,
  • les cartouches, qui contiennent le code et les données (p.ex. graphiques) des jeux et autres programmes Game Boy,
  • le minuteur, qui permet de mesurer le passage du temps.

Une fois cette étape terminée, il ne manquera à votre simulateur « que » la gestion de l'écran et du clavier, qui feront l'objet des étapes 7 à 11. Toutefois, comme vous le verrez, la gestion de l'écran est relativement complexe et justifie qu'on y consacre plusieurs étapes.

Notez que cette étape devra être rendue deux fois :

  1. pour le rendu testé habituel (délai : 6 avril, 16h30),
  2. pour le rendu intermédiaire (délai : 13 avril, 16h30).

Le deuxième de ces rendus sera corrigé par lecture de votre code pour les étapes 1 à 6, et il vous faudra donc soigner sa qualité et sa documentation.

1.1 Mémoire de démarrage

Comme nous l'avons vu, lorsque le processeur du Game Boy démarre, il commence à exécuter le code se trouvant à l'adresse 0. La question se pose donc de savoir quel programme s'y trouve à l'allumage du système.

Sur le Game Boy, on trouve à l'adresse 0 ce que l'on nomme le programme de démarrage (boot program), dont le but principal est d'initialiser les différents composants du système. Ce programme est stocké dans une petite mémoire morte de 256 octets appelée la mémoire de démarrage (boot ROM) et occupant la plage d'adresses allant de 0 à FF16.

Après avoir initialisé les différents composants du Game Boy, le programme de démarrage fait défiler à l'écran le logo de Nintendo et joue deux notes. Une vidéo montrant cette animation est disponible sur YouTube.

Une fois les notes jouées, le programme de démarrage cède le contrôle au programme stocké sur la cartouche actuellement présente dans le Game Boy, en commençant à exécuter le code se trouvant à l'adresse 10016. Cette adresse fait référence à la mémoire morte se trouvant sur la cartouche, comme on peut le voir sur la carte mémoire présentée à l'étape 1.

Toutefois, juste avant de céder le contrôle, le programme de démarrage s'arrange pour faire disparaître la mémoire de démarrage du bus ! En d'autres termes, dès que le programme se trouvant sur la cartouche commence à s'exécuter, la plage d'adresses 0 à FF16 n'est plus attribuée à la mémoire de démarrage, mais aux 256 premiers octets de la mémoire de la cartouche. C'est pourquoi la carte mémoire de l'étape 1 montre que la plage 0 à FF16 est attribuée à la fois à la mémoire de démarrage et à la mémoire de la cartouche.

La raison pour laquelle il est important que la mémoire de démarrage disparaisse ainsi du bus est qu'elle occupe une plage d'adresses qui est aussi utilisées par les gestionnaires d'interruption et les fonctions appelées par les instructions RST. Or il est capital que le programme se trouvant sur la cartouche puisse, par exemple, définir ses propres gestionnaires d'interruption, ce qui serait impossible si la mémoire de démarrage occupait la plage 0 à FF16 de manière permanente.

Reste à savoir comment le programme de démarrage peut faire disparaître du bus la mémoire qui le contient ! La technique utilisée par Nintendo consiste à réserver une adresse particulière, FF5016, pour cette désactivation. Dès qu'une écriture est effectuée à cette adresse, et quelle que soit la valeur écrite, la mémoire de démarrage disparaît du bus et les accès à la plage 0 à FF16 sont transmis à la cartouche. Le programme de démarrage se termine donc par l'instruction suivante :

LD [$FF00 + $50], A

Cette instruction s'encode en deux octets (E016 5016) qui sont les deux derniers du programme de démarrage, placés aux adresses FE16 et FF16. Dès lors, lorsque le processeur exécute cette instruction, la mémoire de démarrage disparaît du bus, et la prochaine instruction exécutée est celle qui suit, à l'adresse 10016.

Notez que la disparition de la mémoire de démarrage est permanente, et dure tant et aussi longtemps que le Game Boy est allumé. En conséquence, il est impossible pour un programme se trouvant sur une cartouche de lire le contenu de la mémoire de démarrage ! Son contenu exact est donc resté secret durant de nombreuses années, et a finalement été obtenu en photographiant la mémoire au microscope et en y lisant son contenu bit à bit. La photo ci-dessous, provenant du site de la personne ayant effectué ces retranscriptions, donne une idée de la difficulté de la tâche — et de l'aspect d'une ROM vue au microscope.

DMG_ROM_3.jpg

Figure 1 : Photo d'une partie de la mémoire de démarrage du Game Boy

Pour différentes raisons, nous n'utiliserons pas dans ce projet le programme de démarrage original de Nintendo. Au lieu de cela, nous utiliserons celui développé dans le contexte du projet SameBoy, et qui vous est fourni plus bas sous une forme facile à intégrer dans votre code. Les personnes intéressées par la version assembleur de ce programme de démarrage peuvent la consulter sur github.

1.2 Cartouche

Comme cela a déjà été dit, les différents programmes Game Boy — des jeux, principalement — étaient vendus sous la forme de cartouches (cartridges) à enfiler dans le Game Boy. Toutes les cartouches contiennent au moins une mémoire morte dans laquelle se trouve aussi bien le code que les données du jeu. En plus de cette mémoire morte, certaines cartouches contiennent également une mémoire vive, souvent non volatile, sur laquelle des informations comme les meilleurs scores sont sauvegardés.

Dans le cadre de ce projet, nous ne simulerons dans un premier temps que les cartouches les plus simples, composées de :

  • une mémoire morte (ROM) de 32 768 octets,
  • un contrôleur pour cette mémoire morte.

La raison pour laquelle la mémoire morte fait 32 768 octets est que la plage d'adresses allant de 0 à 800016 (exclue) est réservée à la mémoire morte de la cartouche dans la carte mémoire du Game Boy, or 800016 = 32 768.

1.2.1 Contrôleur de banque mémoire

La principale différence existant entre les types de cartouches est la taille de la mémoire morte et la complexité du contrôleur mémoire, les deux étant liés.

Dans les cartouches les plus simples, comme celles que nous simulerons, le contrôleur mémoire est très simple car il y a une correspondance directe entre les adresses de la plage 0 à 7FFF16 et les octets de la mémoire morte.

Dans les cartouches plus complexes, le contrôleur mémoire permet d'accéder à une mémoire morte de plus de 32 768 octets en rendant visible différentes tranches de la mémoire dans la plage allant de 400016 à 7FFF16, en fonction de commandes qu'on lui transmet. On dit alors que la mémoire morte est organisée en banques (banks).

Pour cette raison, le contrôleur de la mémoire morte de la cartouche est généralement appelé contrôleur de banque mémoire (memory bank controller ou MBC) et pas simplement « contrôleur mémoire ». Nous conserverons cette terminologie ici, même si notre « contrôleur de banque mémoire » n'a en réalité aucune notion de banque.

1.2.2 En-tête de la cartouche

Une particularité de la mémoire morte stockée sur la cartouche est qu'elle ne contient pas seulement le code et les données du jeu, mais également un en-tête situé entre les adresses 10416 et 15016 (exclus). Cet en-tête contient différentes informations sur le jeu contenu dans la cartouche. La table ci-dessous résume les parties les plus intéressantes (pour nous) de cet en-tête, une description plus complète étant disponible sur ce site Web.

Adresses Contenu
13416 à 14316 Titre du jeu (encodé en ASCII)
14716 Type de la cartouche

Pour l'instant, seul l'octet à l'adresse 14716 nous intéresse, car il décrit le type de cartouche, donc le type de contrôleur mémoire qu'elle contient. La seule valeur que notre simulateur acceptera à cette adresse est 0, qui représente une cartouche dotée d'une mémoire de 32 768 octets.

1.2.3 Fichiers ROM

Le simulateur Game Boy développé dans le cadre de ce projet étant un programme, il n'est bien entendu pas possible de lui donner une cartouche physique en paramètre.

Au lieu de cela, on lui fournira un fichier ROM (ROM file). Comme son nom l'indique, un fichier ROM contient les données de la mémoire morte d'une cartouche. Nous mettrons à votre disposition différents fichiers ROM au fil des étapes, afin que vous puissiez tester votre simulateur. Certains sont d'ailleurs fournis à la fin de cette étape-ci.

Pour illustrer la structure d'un fichier ROM, les 352 premiers octets de celui contenant le jeu Tetris sont présentés ci-dessous. Chaque ligne donne d'abord la position dans le fichier du premier octet de la ligne, puis la valeur des 16 octets suivants, et finalement leur représentation textuelle lorsqu'on les décode avec l'encodage ASCII — les caractères de contrôle ou invalides étant remplacés par des points. Notez que toutes les valeurs sont en base 16.

000  c3 0c 02 00 00 00 00 00  c3 0c 02 ff ff ff ff ff  |................|
010  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
020  ff ff ff ff ff ff ff ff  87 e1 5f 16 00 19 5e 23  |.........._...^#|
030  56 d5 e1 e9 ff ff ff ff  ff ff ff ff ff ff ff ff  |V...............|
040  c3 7e 01 ff ff ff ff ff  c3 be 26 ff ff ff ff ff  |.~........&.....|
050  c3 be 26 ff ff ff ff ff  c3 5b 00 f5 e5 d5 c5 cd  |..&......[......|
060  6b 00 3e 01 e0 cc c1 d1  e1 f1 d9 f0 cd ef 78 00  |k.>...........x.|
070  9f 00 a4 00 ba 00 ea 27  f0 e1 fe 07 28 08 fe 06  |.......'....(...|
080  c8 3e 06 e0 e1 c9 f0 01  fe 55 20 08 3e 29 e0 cb  |.>.......U .>)..|
090  3e 01 18 08 fe 29 c0 3e  55 e0 cb af e0 02 c9 f0  |>....).>U.......|
0a0  01 e0 d0 c9 f0 01 e0 d0  f0 cb fe 29 c8 f0 cf e0  |...........)....|
0b0  01 3e ff e0 cf 3e 80 e0  02 c9 f0 01 e0 d0 f0 cb  |.>...>..........|
0c0  fe 29 c8 f0 cf e0 01 fb  cd 98 0a 3e 80 e0 02 c9  |.).........>....|
0d0  f0 cd fe 02 c0 af e0 0f  fb c9 ff ff ff ff ff ff  |................|
0e0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
0f0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
100  00 c3 50 01 ce ed 66 66  cc 0d 00 0b 03 73 00 83  |..P...ff.....s..|
110  00 0c 00 0d 00 08 11 1f  88 89 00 0e dc cc 6e e6  |..............n.|
120  dd dd d9 99 bb bb 67 63  6e 0e ec cc dd dc 99 9f  |......gcn.......|
130  bb b9 33 3e 54 45 54 52  49 53 00 00 00 00 00 00  |..3>TETRIS......|
140  00 00 00 00 00 00 00 00  00 00 00 01 01 0a 16 bf  |................|
150  c3 0c 02 cd e3 29 f0 41  e6 03 20 fa 46 f0 41 e6  |.....).A.. .F.A.|

En examinant ce fichier, on constate qu'à la position 14716 — qui correspond à la même adresse dans l'espace d'adressage du Game Boy — se trouve la valeur 0, indiquant qu'il s'agit d'une cartouche simple ne comportant rien d'autre qu'une mémoire morte de 32 768 octets. Bien évidemment, le fichier ROM lui-même fait également cette taille.

D'autre part, entre la position 13416 et 14316, on trouve les octets

54 45 54 52 49 53 00 00 00 00 00 00 00 00 00 00

qui représentent la chaîne de caractères TETRIS encodée en ASCII, visible dans la colonne de droite, suivie de 10 octets nuls.

Finalement, à la position 4016 — adresse du gestionnaire d'interruption 0 (VBLANK) — on trouve les octets

c3 7e 01

qui ne sont rien d'autre que l'encodage de l'instruction JP 17E. Cela signifie que le code chargé de gérer l'interruption VBLANK de Tetris se trouve à l'adresse 17E16. Et ainsi de suite.

1.3 Minuteur

En électronique, un minuteur (timer en anglais) est un composant permettant de mesurer le passage du temps. En général, les minuteurs ne sont rien d'autre que des compteurs incrémentés par l'horloge.

1.3.1 Comportement général

Le minuteur du Game Boy n'échappe pas à cette règle puisque sa composante principale est un compteur 16 bits dont la valeur est incrémentée de 1 unité à chaque battement d'horloge, donc de 4 unités à chaque cycle. Ce compteur « boucle » une fois la valeur maximale atteinte. Cela signifie que si sa valeur vaut FFFF16 à un battement d'horloge donné, elle vaudra 0 au suivant. Etant donné que l'horloge du Game Boy a une fréquence de 222 Hz, le compteur du minuteur revient à zéro 64 fois par secondes (26).

En plus de ce compteur 16 bits, appelé ci-après compteur principal, le minuteur du Game Boy possède un compteur secondaire nommé TIMA. Il s'agit cette fois d'un compteur 8 bits, dont la valeur est déterminée selon une logique plus complexe que celle utilisée pour le compteur principal.

En gros, l'idée est que le compteur secondaire est incrémenté chaque fois qu'un bit donné du compteur principal passe de 1 à 0. Le bit du compteur principal utilisé dépend d'une configuration, et peut être celui d'index 3, 5, 7 ou 9. Par exemple, si le bit 3 est utilisé, alors le compteur secondaire est incrémenté chaque fois que les 4 bits de poids faible du compteur principal passent de 1111 à 0000, ce qui se produit tous les 16 coups d'horloge, donc tous les 4 cycles. Si le bit 5 est utilisé, alors le compteur secondaire est incrémenté chaque fois que les 6 bits de poids faible du compteur principal passent de 111111 à 000000, ce qui se produit tous les 64 coups d'horloge, donc tous les 16 cycles. Et ainsi de suite.

Lorsque le compteur secondaire est à sa valeur maximale (FF16) et qu'il est incrémenté, deux choses se passent :

  • le minuteur lève l'interruption TIMER du processeur,
  • plutôt que d'être réinitialisé à 0 comme le compteur principal, le compteur secondaire est réinitialisé à la valeur stockée dans un de ses registres, nommé TMA.

Ce comportement peut paraître compliqué et arbitraire, mais il permet au programme utilisant le minuteur de configurer assez finement la fréquence à laquelle l'interruption TIMER est levée, en choisissant judicieusement le bit du compteur principal utilisé pour incrémenter le secondaire, ainsi que la valeur de TMA.

1.3.2 Registres

Le minuteur du Game Boy possède un total de quatre registres qui sont tous au moins partiellement « mappés en mémoire », c-à-d exposés sur le bus :

  1. le compteur principal, 16 bits, dont les 8 bits de poids fort uniquement sont exposés à l'adresse FF0416,
  2. le compteur secondaire, 8 bits, nommé TIMA et exposé à l'adresse FF0516,
  3. la valeur de réinitialisation du compteur secondaire, nommée TMA et exposée à l'adresse FF0616,
  4. la configuration du minuteur, nommée TAC et exposée à l'adresse FF0716.

Seuls les 3 bits de poids faible du registre TAC sont utilisés pour configurer le minuteur. Le bit d'index 2 détermine si le minuteur est activé (lorsqu'il vaut 1) ou désactivé (lorsqu'il vaut 0). Les deux bits de poids faible, d'index 1 et 0, déterminent quant à eux quel bit du compteur principal est utilisé pour savoir quand incrémenter le secondaire, selon la table suivante :

TAC[1:0] Bit
00 9
01 3
10 5
11 7

Ces registres peuvent à la fois être lus et écrits via le bus. Notez que l'écriture d'une valeur quelconque à l'adresse FF0416 provoque la remise à zéro de la totalité des bits du compteur principal.

1.3.3 Comportement précis

La manière dont le compteur secondaire est incrémenté est en réalité légèrement plus complexe que celle décrite ci-dessus.

Pour la décrire, appelons l'état du minuteur la conjonction (et) logique entre :

  1. le bit 2 du registre TAC (qui [dés]active le minuteur), et
  2. le bit du compteur principal désigné par les 2 bits de poids faible du registre TAC.

L'état est une valeur booléenne qui peut changer pour différentes raisons : soit parce que l'un des 3 bits de poids faible du registre TAC change, soit parce que le bit du compteur désigné par les 2 bits de poids faible de TAC change. Et c'est justement lorsque cet état passe de vrai à faux que le compteur secondaire est incrémenté.

Notez que cela implique le comportement simplifié décrit plus haut. Par exemple, admettons que les 4 bits du compteur principal valent 1111 et que les 3 bits de poids faible du registre TAC valent 101. A ce moment, l'état du minuteur est vrai (car le bit 2 de TAC est vrai, de même que le bit 3 du compteur). Lorsque le compteur est incrémenté, ses 4 bits de poids faible deviennent 0000, et l'état passe alors à faux. En raison de cette transition de vrai à faux, le compteur secondaire est incrémenté.

Toutefois, il existe aussi des cas dans lesquels le compteur secondaire est incrémenté mais qui ne sont pas couverts par la description simplifiée plus haut. Par exemple, admettons que les 4 bits du compteur principal valent 1000 et les 3 bits de poids faible de TAC valent 101. A ce moment, l'état du minuteur est vrai, pour les mêmes raisons que celles décrites plus haut. Or si la valeur de TAC est changée à ce moment, via une écriture sur le bus, pour faire passer ses 3 bits de poids faible à 001, alors l'état du minuteur devient faux et le compteur secondaire est également incrémenté !

2 Mise en œuvre Java

Les classes à écrire dans le cadre de cette étape se trouvent toutes dans le paquetage ch.epfl.gameboj.component ou l'un de ses sous-paquetages. En particulier, un nouveau sous-paquetage nommé cartridge est à créer afin d'y placer les classes liées à la cartouche.

2.1 Classe MBC0

La classe MBC0 du paquetage ch.epfl.gameboj.component.cartridge, publique et finale, représente un contrôleur de banque mémoire de type 0, c-à-d doté uniquement d'une mémoire morte de 32 768 octets. Elle implémente l'interface Component, malgré le fait qu'elle n'ait pas pour but d'être connectée directement au bus ! Ce choix a été fait dans un soucis de simplicité, mais il aurait également été possible de définir une interface séparée pour représenter les contrôleurs de banque mémoire.

La classe MBC0 offre un unique constructeur public :

  • MBC0(Rom rom), qui construit un contrôleur de type 0 pour la mémoire donnée ; lève l'exception NullPointerException si la mémoire est nulle, et IllegalArgumentException si elle ne contient pas exactement 32 768 octets.

En dehors de ce constructeur, la classe MBC0 ne contient rien d'autre qu'une mise en œuvre des méthodes read et write de Component. Notez que write est triviale, étant donné qu'il est impossible d'écrire quoi que ce soit dans une mémoire morte.

2.2 Classe Cartridge

La classe Cartridge du paquetage ch.epfl.gameboj.component.cartridge, publique et finale, représente une cartouche. Elle implémente l'interface Component même si, comme nous le verrons ci-dessous, elle n'est pas non plus connectée directement au bus du Game Boy.

La classe Cartridge est dotée d'un unique constructeur privé prenant en argument un contrôleur de banque mémoire (de type Component) et construisant une cartouche contenant ce contrôleur et la mémoire morte qui lui est attachée. Ce constructeur étant privé, il ne peut être utilisé de l'extérieur, et une méthode de construction séparée est donc fournie. Elle est bien entendu publique et statique :

  • Cartridge ofFile(File romFile), qui retourne une cartouche dont la mémoire morte contient les octets du fichier donné ; lève l'exception IOException en cas d'erreur d'entrée-sortie, y compris si le fichier donné n'existe pas, et l'exception IllegalArgumentException si le fichier en question ne contient pas 0 à la position 14716.

En dehors de cette méthode de construction, la classe Cartridge n'offre rien d'autre qu'une mise en œuvre des méthodes read et write de l'interface Component. Ces méthodes se contentent de valider leurs arguments puis d'appeler les méthodes correspondantes du contrôleur de banque mémoire.

2.3 Classe BootRomController

La classe BootRomController du paquetage ch.epfl.gameboj.component.memory, publique et finale, représente le contrôleur de la mémoire morte de démarrage. Etant donné qu'elle modélise un composant connecté au bus, elle implémente l'interface Component.

Une particularité de la classe BootRomController est qu'elle contrôle l'accès à la cartouche, afin d'intercepter les lectures faites dans la plage 0 à FF16 tant et aussi longtemps que la mémoire de démarrage n'a pas été désactivée.

Pour ce faire, la cartouche (de type Cartridge) n'est pas connectée directement au bus du Game Boy. Au lieu de cela, le contrôleur de mémoire de démarrage (de type BootRomController) est placé entre le bus et la cartouche. Tant et aussi longtemps que la mémoire de démarrage n'a pas été désactivée par une écriture à l'adresse FF5016, le contrôleur intercepte toutes les lectures faites dans la plage 0 à FF16 et y répond lui-même, en fournissant l'octet de la mémoire de démarrage correspondant. Par contre, une fois que la mémoire de démarrage a été désactivée, le contrôleur devient « transparent » et transmet toutes les lectures à la cartouche.

La classe BootRomController définit un unique constructeur public :

  • BootRomController(Cartridge cartridge), qui construit un contrôleur de mémoire de démarrage auquel la cartouche donnée est attachée ; lève l'exception NullPointerException si cette cartouche est nulle.

En dehors de ce constructeur, la classe BootRomController ne fournit rien d'autre que des mises en œuvre des méthodes read et write de Component.

La méthode read intercepte les lectures dans la plage 0 à FF16 tant et aussi longtemps que la mémoire de démarrage n'a pas été désactivée, et y répond avec l'octet correspondant de la mémoire de démarrage. Toutes les autres lectures (sans exception aucune) sont transmises à la cartouche, en appelant sa méthode read à elle.

La méthode write détecte pour sa part les écritures à l'adresse FF5016 (pour laquelle la constante AddressMap.REG_BOOT_ROM_DISABLE existe) et désactive la mémoire de démarrage à la première d'entre elles, indépendemment de la valeur écrite. Notez que toutes les autres écritures, sans exception aucune, sont transmises à la cartouche, en appelant sa méthode write. Cela est nécessaire car certaines cartouches possèdent de la mémoire vive à certaines adresses, et ont de plus des contrôleurs de banque mémoire qui sont configurés au moyen d'écritures dans la plage 0 à 800016.

Pour faciliter votre travail, nous mettons à votre disposition une archive Zip contenant une interface nommée BootRom. Cette interface définit un tableau d'octets nommé DATA dont le contenu est celui de la mémoire de démarrage.

2.4 Classe Timer

La classe Timer du paquetage ch.epfl.gameboj.component, publique et finale, représente le minuteur du Game Boy. Etant donné qu'elle modélise un composant à la fois connecté au bus et piloté par l'horloge, elle implémente les interfaces Component et Clocked. Elle offre un unique constructeur public :

  • Timer(Cpu cpu), qui construit un minuteur associé au processeur donné, ou lève l'exception NullPointerException si celui-ci est nul.

Le processeur est passé au constructeur pour que le minuteur ait la possibilité de lever l'interruption TIMER lorsque cela est nécessaire.

En dehors de ce constructeur, la classe Timer n'offre aucune autre méthode publique que celles héritées de Component (read et write) et de Clocked (cycle). Les méthodes read et write donnent accès aux registres décrits plus haut, tandis que la méthode cycle met à jour le compteur principal. Ces trois méthodes doivent également mettre à jour au besoin le compteur secondaire. Des conseils de programmation sont donnés ci-dessous à ce sujet.

Notez que des constantes sont définies dans l'interface AddressMap que nous vous avons fournie pour les adresses des quatre registres du minuteur. Ces constantes sont :

  1. REG_DIV pour les 8 bits de poids fort du compteur principal,
  2. REG_TIMA pour le compteur secondaire (TIMA),
  3. REG_TMA pour la valeur de réinitialisation du registre secondaire,
  4. REG_TAC pour la configuration du minuteur.

2.4.1 Conseils de programmation

La principale difficulté rencontrée lors de l'écriture de la classe Timer est celle de la mise à jour du compteur secondaire. En effet, celui-ci doit être incrémenté selon la règle relativement complexe décrite plus haut.

Pour ce faire, nous vous suggérons de procéder ainsi :

  1. définissez une méthode privée nommée p.ex. state et retournant ce que nous avons appelé l'état du minuteur, c-à-d la conjonction logique du bit 2 du registre TAC et du bit du compteur principal désigné par les 2 bits de poids faible de ce même registre,
  2. définissez une méthode privée nommée p.ex. incIfChange prenant en argument une valeur booléenne représentant l'état précédent et incrémentant le compteur secondaire si et seulement si l'état passé en argument est vrai et l'état actuel (retourné par state) est faux.

Ensuite, chaque fois que vous écrivez du code modifiant soit le contenu du registre TAC, soit celui du compteur, procédez ainsi :

  1. obtenez l'état courant, appelons-le s0, et stockez-le dans une variable,
  2. effectuez le changement de la valeur,
  3. appelez incIfChange avec l'état s0.

De la sorte, le compteur secondaire sera géré correctement.

2.5 Classe GameBoy

Finalement, la classe GameBoy doit être augmentée afin d'ajouter au système la mémoire de démarrage, la cartouche et le minuteur. Cela implique d'apporter les modifications suivantes :

  1. changer le type de l'argument passé au constructeur pour qu'il soit désormais Cartridge,
  2. dans le constructeur, vérifier que la cartouche reçue n'est pas nulle, et lever NullPointerException si elle l'est,
  3. dans le constructeur, créer un contrôleur de mémoire de démarrage en lui passant la cartouche en argument, puis l'attacher au bus,
  4. dans le constructeur, créer un minuteur et l'attacher au bus,
  5. dans la boucle de la méthode runUntil, ajouter un appel à la méthode cycle du minuteur avant l'appel à celle du processeur.

Finalement, il vous faut ajouter une méthode nommée timer et donnant accès au minuteur.

2.6 Tests

Comme d'habitude, nous vous fournissons un fichier de vérification de signatures contenu dans une archive Zip à importer dans votre projet. Nous vous conseillons également d'importer la nouvelle version du fichier de vérification de signatures pour l'étape 5, qui corrige un petit problème.

2.6.1 Fichiers ROM

En plus du fichier de vérification de signatures, nous mettons à votre disposition une dernière archive Zip contenant 12 fichiers ROM, à importer dans votre projet. Ces fichiers ROM contiennent des programmes testant le comportement du processeur, et sont connus sous le nom de « tests de blargg », d'après le pseudonyme de leur auteur. Le fichier instr_timing.gb vérifie que les instructions prennent le bon nombre de cycles pour s'exécuter, tandis que les 11 autres testent que le résultat qu'elles produisent est correct.

Ces programmes de test affichent normalement leur résultat sur l'écran du Game Boy, que vous ne simulez pas encore. Heureusement, ce résultat est aussi envoyé sur le port série du Game Boy, caractère par caractère. Vous ne simulez pas non plus ce composant — et ne le simulerez pas dans le cadre de ce projet — mais il se trouve qu'il est très facile d'écrire un minuscule composant interceptant les caractères qui y sont écrits et les affichant à l'écran :

public final class DebugPrintComponent implements Component {
  @Override
  public int read(int address) {
    return NO_DATA;
  }

  @Override
  public void write(int address, int data) {
    if (address == 0xFF01)
      System.out.print((char)data);
  }
}

Ce composant ne fait rien d'autre qu'intercepter les écritures faites à l'adresse FF0116, interpréter les octets qu'on y écrit comme des caractères, et les afficher à l'écran.

Une fois ce composant défini, il reste à écrire un programme principal simulant le Game Boy complet. Une petite difficulté se pose alors : les tests de blargg dépendent de l'interruption VBLANK, levée par l'écran à la fin du dessin d'une image. Heureusement, il suffit de la lever périodiquement pour que tout fonctionne. C'est ce que fait le programme ci-dessous, qui la lève tous les 17 556 cycles car c'est le temps que met effectivement l'écran du Game Boy pour dessiner une image.

public final class DebugMain {
  public static void main(String[] args) throws IOException {
    File romFile = new File(args[0]);
    long cycles = Long.parseLong(args[1]);

    GameBoy gb = new GameBoy(Cartridge.ofFile(romFile));
    Component printer = new DebugPrintComponent();
    printer.attachTo(gb.bus());
    while (gb.cycles() < cycles) {
      long nextCycles = Math.min(gb.cycles() + 17556, cycles);
      gb.runUntil(nextCycles);
      gb.cpu().requestInterrupt(Cpu.Interrupt.VBLANK);
    }
  }
}

Une fois ce programme écrit, il est possible de l'exécuter en lui passant en premier argument le nom du fichier ROM à utiliser, et en second argument le nombre total de cycles à simuler. Pour ces fichiers ROM, vous pouvez utiliser systématiquement 30 000 000 (30 millions de cycle), ce qui suffit à exécuter tous les tests jusqu'à la fin. Notez que si vous ne savez pas comment exécuter un programme depuis Eclipse en lui passant des arguments, vous pouvez lire notre guide à ce sujet.

Si votre simulateur fonctionne, les tests devraient afficher leur nom sur la console Eclipse puis, au bout d'un temps plus ou moins long, le résultat du test. Par exemple, en exécutant le programme DebugMain en lui passant les arguments suivants :

01-special.gb 30000000

vous devriez voir apparaître le texte suivant dans la console Eclipse au bout de quelques secondes :

01-special


Passed

et de même pour les autres tests. Sur un ordinateur relativement récent, l'exécution d'un test devrait prendre environ 2 secondes1.

Si un test échoue, un message d'erreur est affiché. Par exemple, en introduisant volontairement une erreur dans la gestion de l'instruction DAA et en relançant le test ci-dessus, on obtient le message suivant :

01-special

BD9BF11C 
DAA

Failed #6

Si votre simulateur est capable d'exécuter tous les tests sans erreur, félicitations !

3 Résumé

Pour cette étape, vous devez :

  • écrire les classes MBC0, Cartridge, BootRomController et Timer selon les indications données plus haut,
  • augmenter la classe GameBoy afin d'ajouter les nouveaux composants au système,
  • documenter la totalité des entités publiques que vous avez définies,
  • rendre votre code au plus tard le 6 avril 2018 à 16h30, via le système de rendu.

Ce rendu est un rendu testé, auquel 18 points sont attribués, au prorata des tests unitaires passés avec succès.

Notez qu'en raison du Vendredi Saint, la date de ce rendu a été repoussée d'une semaine. En contrepartie, nous ne garantissons pas que le système de rendu soit fonctionnel durant le week-end de Pâques, c-à-d du 30 mars au 2 avril, même s'il devrait l'être.

N'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus. Souvenez-vous qu'aucun retard, aussi insignifiant soit-il, ne sera toléré !

Notes de bas de page

1

Notez que cela permet d'avoir une idée des performances de votre simulateur par rapport à un véritable Game Boy. Un Game Boy réel exécute environ 1 million de cycles par secondes, donc il lui faut environ 30 secondes pour en exécuter 30 millions. Si votre simulateur est capable de simuler ces cycles en 2 secondes, il est actuellement 15 fois plus rapide qu'un Game Boy réel. Cela permet de penser que, même une fois qu'il simulera l'écran du Game Boy, votre simulateur sera capable de fonctionner à vitesse réelle.