Unité arithmétique et logique
GameBoj – Étape 2

1 Préliminaires

Avant de commencer à travailler à cette seconde étape, lisez le guide Sauvegarder son travail. Il vous donnera des conseils importants concernant la sauvegarde de votre projet au cours du semestre.

2 Introduction

Le but principal de cette seconde étape est d'écrire une classe représentant une partie importante du processeur appelée l'unité arithmétique et logique. Cette unité est chargée d'effectuer des calculs, p.ex. des additions ou des soustractions, pour le compte du processeur.

Il faut noter que cette unité est intégrée au processeur, et ne constitue donc pas un composant individuel connecté au bus. Il est néanmoins intéressant de la modéliser comme une entité à part entière, d'une part car cela répartit le code simulant le processeur entre plusieurs classes, et d'autre part car une unité séparée est plus simple à tester.

2.1 Unité arithmétique et logique

Une unité arithmétique et logique ou UAL (arithmetic logic unit ou ALU en anglais) est une partie d'un processeur chargée d'effectuer des calculs sur des « entiers » de taille fixe.

Le processeur du Game Boy possède une UAL capable d'effectuer des calculs sur des « entiers » de 8 bits1. Parmi les opérations qu'elle offre figurent :

  • l'addition et la soustraction,
  • les opérations bit à bit usuelles (et, ou, ou exclusif, etc.),
  • le décalage et la rotation dans chacune des deux directions, mais d'une distance limitée à un seul bit,
  • le test et la modification d'un bit individuel d'un « entier » de 8 bits.

En plus de ces opérations sur des « entiers » de 8 bits, elle offre également l'addition sur les « entiers » de 16 bits.

Il faut noter que cette UAL ne permet pas d'effectuer des multiplications ou des divisions ! Cela implique que le processeur du Game Boy n'offre pas non plus d'instruction de multiplication ou de division, ce qui ne facilite pas la vie des programmeurs.

2.2 Fanions

En plus de produire un « entier » de taille fixe comme résultat, les différentes opérations de l'UAL produisent également un certain nombre de valeurs booléennes, nommées fanions ou parfois drapeaux (flags), qui donnent des informations sur le résultat. Ces fanions permettent de facilement déterminer si le résultat d'une opération a une caractéristique donnée — p.ex. s'il est nul —, simplement en consultant le fanion correspondant.

Les fanions sont généralement nommés au moyen d'une seule lettre et, dans le cas de l'UAL du Game Boy, sont au nombre de quatre : Z, N, H et C. Leur signification est la suivante :

  • Z (zero) est vrai ssi le résultat de l'opération vaut zéro,
  • N est vrai ssi l'opération effectuée était une soustraction,
  • H (half carry) est vrai ssi une retenue (resp. un emprunt) a été produite par l'addition (resp. la soustraction) des 4 bits de poids faible,
  • C (carry) est vrai ssi une retenue (resp. un emprunt) a été produite par l'addition (resp. la soustraction) de la totalité des 8 bits.

Les notions de retenue et d'emprunt sont décrites plus en détail à la section suivante. Le fonctionnement des fanions peut néanmoins être illustré au moyen de l'exemple suivant : admettons que l'on demande à l'UAL du Game Boy de calculer la somme des deux « entiers » de 8 bits valant 9116 et 8316. Elle produit :

  • un résultat de 8 bits valant 1416 (qui est différent du résultat mathématique, 11416, car celui-ci n'est pas représentable avec 8 bits),
  • un fanion Z faux, car le résultat n'est pas égal à 0,
  • un fanion N faux, car l'opération n'était pas une soustraction,
  • un fanion H faux, car l'addition des 4 bits de poids faible (116 et 316) n'a pas produit de retenue — en d'autres termes, son résultat (416) est représentable avec 4 bits uniquement,
  • un fanion C vrai, car l'addition des 8 bits a produit une retenue — en d'autres termes, son résultat (11416) n'est pas représentable avec 8 bits et a été tronqué en 1416.

Il faut noter que certains fanions n'ont pas de sens clair pour chacune des opérations de l'UAL. Par exemple, la notion de retenue/emprunt n'a de sens que pour l'addition et la soustraction, et dès lors la signification des fanions H et C n'est claire que pour ces deux opérations. La valeur d'un fanion à la suite d'une opération pour lequel il n'a pas de sens évident a donc été choisie plus ou moins arbitrairement par les concepteurs du processeur du Game Boy, et ne répond généralement à aucune logique.

2.3 Retenues et emprunts

La signification des deux fanions de retenue (H et C) n'est pas la même pour les additions que pour les soustractions. Quelques explications à leur sujet s'imposent donc.

2.3.1 Addition

En binaire (base 2), une addition de deux nombres peut se faire selon la même technique qu'en base 10, consistant à additioner deux à deux chacun les chiffres des deux nombres, de droite à gauche, en propageant les éventuelles retenues.

Par exemple, pour additionner les nombres 112 (3) et 1, on procède ainsi :

  1. les deux bits de poids faible (1 et 1) sont additionnés : le résultat vaut 0, mais une retenue est produite,
  2. les deux bits suivants (1 et 0) ainsi que la retenue précédente (1) sont additionés : le résultat vaut à nouveau 0, et la retenue 1,
  3. les deux bits suivants (0 et 0) ainsi que la retenue précédente (1) sont additionés : le résultat vaut 1, et aucune retenue n'est produite.

Le résultat est donc 1002, soit 4, comme attendu.

Plus généralement, admettons que l'on additionne deux valeurs de n bits nommées x et y. Les différents bits de ces deux valeurs peuvent être numérotés de 0 (bit de poids le plus faible) à n - 1 (bit de poids le plus fort). De la même manière, on peut numéroter les bits de retenue c (pour carry) de 0 à n. Cette convention est illustrée par la table ci-dessous :

Index de bit n n - 1 2 1 0
Retenue c c[n] c[n-1] c[2] c[1] c[0]
x   x[n-1] x[2] x[1] x[0]
y   y[n-1] y[2] y[1] y[0]

Il est très important de comprendre qu'avec cette convention, le bit d'index i de la retenue est produit par l'addition des bits d'index i - 1 de x, y et c ! Dès lors, c[0] vaut toujours 0, et la retenue contient un total de n + 1 bits, soit un bit de plus que les deux opérandes.

Une fois les différents bits de la retenue numérotés de la sorte, il est trivial d'expliquer la signification des fanions H et C lors d'une addition dans l'UAL du Game Boy : H est simplement c[4], tandis que C est c[8].

2.3.2 Soustraction

Tout comme l'addition, la soustraction peut s'effectuer en base 2 exactement comme en base 10 : bit à bit, de droite à gauche, en effectuant éventuellement des emprunts à gauche.

Par exemple, pour calculer 1002 (4) - 1, on procède ainsi :

  1. les deux bits de poids faible (0 et 1) sont soustraits : comme la valeur à soustraire est plus grande que la valeur dont on soustrait, on emprunte 1 à gauche, et le bit de poids faible du résultat est : 102 (emprunt) + 0 - 1 = 1,
  2. les deux bits suivants (0 et 0) sont soustraits, de même que l'emprunt effectué précédemment, qui doit lui aussi être soustrait ; là aussi, un emprunt doit être effectué, et le prochain bit vaut donc 102 (emprunt) + 0 - 1 (emprunté ci-dessus) - 0 = 1,
  3. les deux bits suivants (1 et 0) sont soustraits, de même que l'emprunt précédent ; le dernier bit vaut donc 1 - 1 (emprunté précédemment) - 0 = 0.

Le résultat est donc 0112, soit 3, comme attendu.

Dans le cas général d'une soustraction de deux valeurs de n bits, il est une fois encore possible de numéroter les bits des opérandes et de l'emprunt. On obtient une table similaire à celle ci-dessus, si ce n'est que l'emprunt est nommé b, comme borrow :

Index de bit n n - 1 2 1 0
Emprunt b b[n] b[n-1] b[2] b[1] b[0]
x   x[n-1] x[2] x[1] x[0]
y   y[n-1] y[2] y[1] y[0]

Là aussi, il est important de comprendre que le bit d'index i de l'emprunt est produit par la soustraction des bits d'index i - 1 de x, y et b ! Dès lors, b[0] vaut toujours 0. Par contre, conceptuellement en tout cas, l'emprunt peut avoir un nombre infini de bits, car lorsque x < y, b[i] = 1 pour tout in. (Pour vous en rendre compte, vous pouvez essayer de calculer 0 - 1 avec la technique sus-mentionnée). Cela n'a toutefois pas d'impact sur la discussion qui suit.

Comme pour l'addition, étant donné la numérotation des bits de l'emprunt décrite ci-dessus, il devient trivial d'expliquer la signification des bits H et C dans le cas d'une soustraction : H est simplement b[4], tandis que C est b[8].

2.4 Ajout avec retenue, soustraction avec emprunt

Dans la section précédente, nous avons affirmé que dans le cas d'une addition, le premier bit de la retenue — c[0] — valait toujours 0. De même, dans le cas d'une soustraction, le premier bit de l'emprunt — b[0] — valait toujours 0.

Dans certaines situations, il est pourtant intéressant de pouvoir spécifier, lors d'une addition, la valeur de c[0] et, lors d'une soustraction, la valeur de b[0].

Par exemple, admettons que l'on désire effecuter une soustraction de deux valeurs 16 bits alors qu'on ne dispose que d'une UAL capable d'additioner des valeurs 8 bits. Intuitivement, cela peut se faire en enchaînant deux additions de 8 bits : la première pour les 8 bits de poids faible, la seconde pour les 8 bits de poids fort. Toutefois, il faut bien prendre garde à une chose : si la première addition produit une retenue, celle-ci doit être utilisée comme retenue initiale de la seconde addition ! En d'autres termes, la valeur de c[8] de la première addition doit être utilisée comme valeur de c[0] de la seconde addition.

Pour cette raison, l'UAL du Game Boy offre la possibilité de spécifier la valeur de c[0] (resp. b[0]) lors d'une addition (resp. une soustraction). On appelle ces variantes de l'addition et de la soustraction : addition avec retenue (add with carry) et soustraction avec emprunt (subtract with borrow).

2.5 Retenues des addition 16 bits

L'UAL du Game Boy peut également effectuer des additions de valeurs 16 bits, ce qu'elle fait en chaînant deux additions de 8 bits comme décrit ci-dessus.

Lorsqu'une telle addition est effectuée, la question se pose de savoir ce que valent les fanions de retenue, H et C. En gros, deux possibilités sensées existent :

  1. ces fanions sont ceux de la première addition de 8 bits, celle qui somme les 8 bits de poids faible de chacune des deux valeurs,
  2. ces fanions sont ceux de la seconde addition de 8 bits, celle qui somme les 8 bits de poids fort de chacune des deux valeurs et l'éventuelle retenue produite par la première addition.

Visiblement, les concepteurs du processeur du Game Boy n'ont pas réussi à se décider pour l'une de ces deux possibilités, et on retrouve donc les deux dans différentes situations. Pour cette raison, notre UAL simulée offre deux variantes de l'addition 16 bits, qui produisent la même valeur 16 bits mais des fanions H et C calculés différemment.

2.6 Rotation à travers la retenue

Comme d'autres UAL, celle du Game Boy offre une opération dite de rotation à travers la retenue (rotate through carry).

Conceptuellement, cette opération consiste à obtenir une valeur de 9 bits en combinant les 8 bits de la valeur et le bit de retenue, qui devient le bit de poids le plus fort de la valeur combinée. Cette valeur de 9 bits subit une rotation, et les 9 bits du résultat sont à nouveau séparés en le bit de poids le plus fort, qui devient le fanion de retenue du résultat, et les 8 bits de poids faible, qui constituent la valeur du résultat.

Par exemple, admettons que l'on désire effectuer une rotation d'un bit vers la gauche de la valeur 8 bits stuvwxyz (chaque lettre représente un bit) et du fanion C. On procède comme décrit plus haut en :

  1. formant la valeur 9 bits combinée, Cstuvwxyz,
  2. tournant cette valeur d'un bit vers la gauche, ce qui donne stuvwxyzC,
  3. séparant cette valeur en son bit de poids le plus fort, s, qui devient la retenue du résultat, et ses 8 bits de poids faible, tuvwxyzC, qui deviennent la valeur du résultat.

2.7 Valeurs décimales codées en binaire

Les ordinateurs représentent en interne les nombres en base 2, tandis que les humains ont l'habitude de travailler en base 10.

Cette différence de base pose parfois problème, par exemple lorsqu'on désire afficher, en base 10, un nombre stocké dans l'ordinateur. En effet, cet affichage implique de déterminer la représentation en base 10 d'un nombre dont on ne connaît que la représentation en base 2.

La solution la plus naturelle pour passer d'une représentation à l'autre consiste à calculer le reste de la division du nombre par 10, ce qui donne le chiffre de poids faible. Le chiffre suivant (depuis la droite) peut ensuite être obtenu en appliquant la même technique au nombre divisé entièrement par 10, et ainsi de suite.

Cette solution fonctionne, mais sa mise en œuvre est problématique (et coûteuse) sur un système comme le Game Boy dont l'UAL n'offre pas d'opération de division.

Pour cette raison, et d'autres, certains nombres sont parfois stockés dans une représentation particulière nommée le décimal codé binaire, abrégé DCB (binary coded decimal, BCD). L'idée est de représenter chaque chiffre décimal par un groupe de 4 bits, selon la correspondance (évidente) suivante :

Chiffre Représentation
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001

Par exemple, 73 est représenté en DCB par un groupe de huit bits, les quatre de poids fort valant 0111 (7), les quatre de poids faible valant 0011 (3), soit 01110011. Notez que cette représentation n'a rien à voir avec la représentation de 73 en binaire, qui vaut 01001001.

Bien entendu, le décimal codé binaire gaspille beaucoup de place, car il n'utilise que 10 des 16 valeurs représentables au moyen d'un groupe de 4 bits. Ainsi, en DCB, un groupe de 8 bits ne permet que de représenter les entiers allant de 0 à 99, alors qu'en binaire, ces mêmes 8 bits permettent de représenter les entiers allant de 0 à 255. L'intérêt du DCB est que la conversion en base 10 est triviale car il y a une correspondance directe entre un groupe de 4 bits et un chiffre décimal.

Pour faciliter la manipulation de nombres décimaux codés binaire, l'UAL du Game Boy permet d'ajuster le résultat d'une opération arithmétique (addition ou soustraction) effectuée sur deux nombres DCB afin que le résultat soit lui aussi au format DCB.

Par exemple, admettons que l'on désire calculer la somme de 35 et de 38, représentés en DCB, et obtenir un résultat qui soit lui aussi en DCB. En DCB, 35 est représenté par les 8 bits valant 0011 0101, tandis que 38 l'est par les 8 bits valant 0011 1000. En calculant « bêtement » la somme de ces deux valeurs 8 bits, on obtient 0110 1101. Ces 8 bits ne constituent pas une valeur DCB valide, car les 4 bits de poids faible valent 1101, ce qui ne correspond pas à un chiffre décimal. Dans ce cas particulier, il suffit d'ajouter 6 au résultat pour obtenir 0111 0011, qui est bien la représentation DCB de 73, la somme de 35 et de 38.

De manière générale, lorsqu'on effectue une addition ou une soustraction « normale » (c-à-d sans savoir qu'on manipule des valeurs DCB) sur deux valeurs DCB, le résultat obtenu n'est pas forcément une valeur DCB valide. Pour la rendre valide, il faut soit ajouter (dans le cas d'une addition), soit soustraire (dans le cas d'une soustraction) 6 à l'un ou l'autre, ou les deux, groupes de 4 bits. Ainsi, dans l'exemple ci-dessus, il a été nécessaire d'ajouter 6 au groupe de bits de poids faible2.

La manière dont on détermine le sous-ensemble des deux groupes de 4 bits nécessitant une correction n'est pas totalement triviale. Dès lors, nous vous la décrivons sous la forme de l'algorithme d'ajustement ci-dessous, donné en pseudo-code. Cet algorithme prend quatre paramètres : la valeur 8 bits V à ajuster et les fanions N, H et C décrits plus haut. Cinq valeurs sont produites en résultat par la dernière ligne, sous la forme d'un quintuplet dont les composantes sont données entre parenthèses et sont, dans l'ordre : la valeur ajustée (Va) et les 4 fanions Z, N, H et C.

fixL := H ∨ (¬N ∧ V[3:0] > 9)
fixH := C ∨ (¬N ∧ V > 99₁₆)
fix := 60₁₆ × fixH + 06₁₆ × fixL
Va := V - fix si N, V + fix sinon
(Va, Va = 0, N, faux, fixH)

On fait ci-dessus l'hypothèse que les fanions sont utilisables comme des entiers valant 0 si faux, 1 si vrai. D'autre part, les symboles usuels sont utilisés pour la conjonction (), disjonction () et négation (¬) logiques. Finalement, V[3:0] dénote l'entier représenté par les 4 bits de poids faible de V.

Pour illustrer le fonctionnement de cet algorithme on peut l'appliquer à l'exemple donné plus haut, l'addition des valeurs DCB 0011 0101 (3516) et 0011 1000 (3816). Dans ce cas, V vaut 0110 1101 (6D16), le fanion N est faux car l'opération effectuée était une addition et pas une soustraction, le fanion H est faux car l'addition des 4 bits de poids faible (0101 et 1000) n'a pas produit de retenue, et le fanion C est faux car l'addition des deux valeurs 8 bits n'a pas produit de retenue non plus.

Les paramètres de l'algorithme étant connus, on peut l'exécuter et on obtient alors :

fixL := faux ∨ (¬faux ∧ D₁₆ > 9₁₆) = vrai
fixH := faux ∨ (¬faux ∧ 6D₁₆ > 99₁₆) = faux
fix := 60₁₆ × 0 + 06₁₆ × 1 = 06₁₆
Va := 6D₁₆ + 06₁₆ = 73₁₆
(73₁₆, faux, faux, faux, faux)

En d'autres termes, la valeur ajustée vaut 7316 (comme attendu) et les fanions Z, N, H et C sont tous faux.

3 Mise en œuvre Java

3.1 Encodage des résultats

Comme cela a été expliqué plus haut, les opérations de l'UAL produisent non seulement un « entier » 8 (ou 16) bits, mais aussi quatre fanions, soit cinq valeurs au total.

Malheureusement, une méthode Java ne peut retourner qu'une seule valeur en résultat. La solution généralement adoptée lorsqu'on désire écrire une méthode retournant plus d'une valeur consiste à définir une classe dont les instances peuvent contenir ces valeurs, et à retourner ensuite une instance de cette classe.

Cette solution serait bien entendu utilisable ici, mais le coût de la création d'un nouvel objet à chaque appel d'une méthode de l'UAL serait prohibitif étant donné la simplicité des opérations effectuées par ces méthodes (additions, etc.).

Dans ce cas particulier, il existe toutefois une solution plus simple et nettement moins coûteuse : l'empaquetage des valeurs dans un seul « entier » Java de type int. Etant donné que les méthodes de l'UAL retournent une valeur de 8 (ou 16) bits plus 4 fanions de 1 bit chacun, seul 12 (ou 20) bits sont nécessaires pour représenter ces 5 valeurs, et il est donc tout à fait possible de les empaqueter dans un « entier » Java de type int.

Les différentes méthodes de l'UAL retournent donc un entier int dont les 32 bits ont le contenu suivant :

Bit(s) Contenu
31 à 16 (ou 24) toujours 0
15 (ou 23) à 8 résultat 8 (ou 16) bits
7 fanion Z
6 fanion N
5 fanion H
4 fanion C
3 à 0 toujours 0

Par exemple, lorsqu'on demande à l'UAL de calculer la somme des deux entiers 9116 et 8316 déjà utilisés à la §2.2, elle retourne un « entier » int valant 0000141016. Cet entier contient, dans ses différents bits et selon la convention décrite plus haut, à la fois le résultat (1416) et les fanions (00012).

Il peut paraître étrange de stocker les fanions dans les bits 4 à 7 et de laisser les bits 3 à 0 inutilisés. Ce choix a été fait simplement car, comme nous le verrons à l'étape suivante, le processeur stocke les fanions sous la forme d'une valeur de 8 bits qui correspond directement aux 8 bits de poids faible d'une valeur encodée comme décrit ci-dessus.

3.2 Calcul des bits de retenue et emprunt

L'UAL du Game Boy effectue les opérations d'addition et de soustraction comme décrit plus haut, en procédant bit à bit, de droite à gauche. Ce faisant, elle détermine individuellement chacun des bits de retenue (dans le cas d'une addition) et d'emprunt (dans le cas d'une soustraction) et n'a donc aucune peine à connaître la valeur des fanions H et C.

Pour simuler cette UAL en Java, il serait bien entendu possible d'effectuer également les additions et soustractions bit à bit, mais cela serait à la fois pénible à écrire et lent à exécuter. Il est beaucoup plus simple et efficace d'utiliser les opérations d'addition et de soustraction offertes par Java. La question se pose néanmoins de savoir comment déterminer la valeur des fanions H et C.

Dans le cas d'une addition, la réponse est simple : une addition sur n bits produit une retenue si et seulement si le résultat est plus grand que la plus grande valeur représentable au moyen de n bits. Dès lors, en Java, on peut déterminer la valeur du fanion C suite à l'addition de deux valeurs 8 bits stockés dans les variables x et y ainsi :

boolean c = (x + y) > 0xFF;

Notez qu'il est fondamental que l'addition soit faite sur des entiers comportant au moins n + 1 bits pour que le fanion C soit calculé correctement ! C'est une des raisons pour lesquelles nous avons décidé de représenter les valeurs 8 et 16 bits au moyen du type Java int (32 bits) dans ce projet.

Dans le cas d'une soustraction, la réponse n'est pas beaucoup plus compliquée : une soustraction sur n bits produit un emprunt si et seulement si la seconde opérande de la soustraction est strictement plus grande que la première. Dès lors, en Java, on peut déterminer la valeur du fanion C suite à la soustraction de deux valeurs 8 bits stockées dans les variables x et y ainsi :

boolean c = x < y;

3.3 Classe Alu

La classe Alu du paquetage ch.epfl.gameboj.component.cpu, publique, finale et non instanciable, contient principalement des méthodes statiques permettant d'effectuer des opérations sur des valeurs 8 ou 16 bits et d'obtenir à la fois le résultat et la valeur des fanions.

3.3.1 Fanions

Les fanions eux-mêmes sont définis sous la forme d'un type énuméré nommé Flag et défini à l'intérieur de la classe Alu. Etant donné que les valeurs de ce type énuméré représentent des bits, il implémente l'interface Bit écrite à l'étape précédente.

Pour simplifier différentes parties du code, les fanions sont représentés comme une valeur de 8 bits dont les 4 bits de poids faible sont toujours 0. Dès lors, le type énuméré Flag contient 8 éléments au lieu des 4 correspondant aux fanions, et les 4 premiers de ces éléments portent un nom indiquant le fait qu'ils sont inutilisés (p.ex. UNUSED_0 à UNUSED_3).

Comme nous l'avons vu, la plupart des méthodes de la classe Alu retournent 5 valeurs — le résultat lui-même et les 4 fanions — empaquetées dans un entier int. Dans la description qui suit, les fanions retournés par une méthode sont spécifiés de manière compacte en donnant, dans l'ordre ZNHC, la valeur des différents fanions, représentée par une seule lettre. Ces lettres peuvent être :

  • la lettre correspondant au fanions (Z, N, H ou C) si ce fanion a une valeur qui dépend du résultat, ou
  • 0 si ce fanion vaut toujours 0 (faux),
  • 1 si ce fanion vaut toujours 1 (vrai).

Par exemple, dans la description de la méthode and ci-dessous, il est mentionné que les fanions qu'elle retourne valent Z010, ce qui signifie que :

  • le fanion Z dépend du résultat et vaut 1 ssi celui-ci est nul,
  • le fanion N est toujours faux (0),
  • le fanion H est toujours vrai (1),
  • le fanion C est toujours faux.

3.3.2 Direction de rotation

En plus de l'énumération correspondant aux fanions, la classe Alu définit une seconde énumération publique nommée RotDir et possédant deux valeurs nommées LEFT et RIGHT. Les valeurs de cette énumération sont utilisées pour spécifier aux méthodes de rotation de bits décrites plus bas dans quelle direction la rotation doit être effectuée.

3.3.3 Méthodes

La classe Alu offre les méthodes publiques (et statiques) ci-dessous. Pour faciliter leur compréhension, des exemples numériques d'utilisation de ces méthodes sont donnés plus bas à la §3.3.5.

Afin de ne pas alourdir la description qui suit, la validation de la plupart des arguments n'est pas spécifiée explicitement. Toutefois, sachez que chaque fois qu'une méthode est décrite comme acceptant une valeur de 8 ou 16 bits, celle-ci doit être validée au moyen de la méthode correspondante de l'interface Preconditions.

Cela dit, les méthodes de la classe Alu sont :

  • int maskZNHC(boolean z, boolean n, boolean h, boolean c), qui retourne une valeur dont les bits correspondant aux différents fanions valent 1 ssi l'argument correspondant est vrai,
  • int unpackValue(int valueFlags), qui retourne la valeur contenue dans le paquet valeur/fanion donné,
  • int unpackFlags(int valueFlags), qui retourne les fanions contenus dans le paquet valeur/fanion donné,
  • int add(int l, int r, boolean c0), qui retourne la somme des deux valeurs 8 bits données et du bit de retenue initial c0 (voir §2.4), et les fanions Z0HC,
  • int add(int l, int r), identique à la méthode précédente mais avec false comme dernier argument,
  • int add16L(int l, int r), qui retourne la somme des deux valeurs 16 bits données et les fanions 00HC, où H et C sont les fanions correspondant à l'addition des 8 bits de poids faible,
  • int add16H(int l, int r), identique à add16L, si ce n'est que les fanions H et C correspondent à l'addition des 8 bits de poids fort,
  • int sub(int l, int r, boolean b0), qui retourne la différence des valeurs de 8 bits données et du bit d'emprunt initial b0 (voir §2.4), et les fanions Z1HC,
  • int sub(int l, int r), identique à la méthode précédente, mais avec false comme dernier argument,
  • int bcdAdjust(int v, boolean n, boolean h, boolean c), qui ajuste la valeur 8 bits donnée en argument afin qu'elle soit au format DCB, selon l'algorithme décrit à la §2.7,
  • int and(int l, int r), qui retourne le « et » bit à bit des deux valeurs 8 bits données et les fanions Z010,
  • int or(int l, int r), qui retourne le « ou inclusif » bit à bit des deux valeurs 8 bits données et les fanions Z000,
  • int xor(int l, int r), qui retourne le « ou exclusif » bit à bit des deux valeurs 8 bits données et les fanions Z000,
  • int shiftLeft(int v), qui retourne la valeur 8 bits donnée décalée à gauche d'un bit, et les fanions Z00C où le fanion C contient le bit éjecté par le décalage (c-à-d que C est vrai ssi le bit en question valait 1),
  • int shiftRightA(int v), qui retourne la valeur 8 bits donnée décalée à droite d'un bit, de manière arithmétique, et les fanions Z00CC contient le bit éjecté par le décalage,
  • int shiftRightL(int v), qui retourne la valeur 8 bits donnée décalée à droite d'un bit, de manière logique, et les fanions Z00CC contient le bit éjecté par le décalage,
  • int rotate(RotDir d, int v), qui retourne la rotation de la valeur 8 bits donnée, d'une distance de un bit dans la direction donnée, et les fanions Z00CC contient le bit qui est passé d'une extrémité à l'autre lors de la rotation,
  • int rotate(RotDir d, int v, boolean c), qui retourne la rotation à travers la retenue, dans la direction donnée, de la combinaison de la valeur 8 bits et du fanion de retenue donnés, ainsi que les fanions Z00C ; comme expliqué à la §2.6, cette opération consiste à construire une valeur 9 bits à partir de la retenue et de la valeur 8 bits, la faire tourner dans la direction donnée, puis retourner les 8 bits de poids faible comme résultat, et le bit de poids le plus fort comme nouvelle retenue (fanion C),
  • int swap(int v), qui retourne la valeur obtenue en échangeant les 4 bits de poids faible et de poids fort de la valeur 8 bits donnée, et les fanions Z000,
  • int testBit(int v, int bitIndex), qui retourne la valeur 0 et les fanions Z010Z est vrai ssi le bit d'index donné de la valeur 8 bits donnée vaut 0 ; en plus de la validation de la valeur 8 bits reçue, cette méthode valide l'index reçu et lève IndexOutOfBoundsException s'il n'est pas compris entre 0 et 7.

3.3.4 Conseils de programmation

Etant donné que la plupart des méthodes retournant un « paquet » contenant une valeur 8/16 bits et les fanions, il est conseillé de définir une méthode privée permettant de facilement créer un tel paquet. Cette méthode pourrait avoir le profil suivant :

int packValueZNHC(int v,
		  boolean z,
		  boolean n,
		  boolean h,
		  boolean c) { /* … */ }

D'autres méthodes privées pourraient également être utiles. N'hésitez pas à en définir lorsque vous constatez que vous écrivez plusieurs fois le même code !

3.3.5 Exemples

Pour faciliter la compréhension des différentes méthodes décrites ci-dessus, quelques exemples d'utilisation sont donnés ici.

Toutes les méthodes de calcul de la classe Alu retournent à la fois un résultat 8 ou 16 bits, et la valeur des 4 fanions, le tout empaqueté dans un entier de 32 bits. La valeur d'un tel paquet peut être extraite au moyen de la méthode unpackValue, les fanions au moyen de la méthode unpackFlags. La table ci-dessous illustre le fonctionnement de ces deux méthodes, ainsi que de la méthode maskZNHC. Notez que les valeurs utilisées dans cette table se rapportent à la ligne signalée par un obèle (†) dans la table plus bas.

Appel Résultat
unpackValue(0xFF70) 0xFF
unpackFlags(0xFF70) 0x70
maskZNHC(false, true, true, true) 0x70

La table ci-dessous donne quelques exemples d'utilisation des méthodes de calcul 8 bits. Pour faciliter la lecture, le résultat est donné dans deux colonnes : la première donne la valeur, obtenue par l'application de la méthode unpackValue au résultat de l'appel ; la seconde donne les fanions, obtenus par l'application de la méthode unpackFlags à ce même résultat. Les fanions sont donnés à la fois sous forme numérique et sous forme textuelle, dans l'ordre ZNHC, où une lettre indique que le fanion correspondant est vrai, et un tiret indique qu'il est faux.

  Appel Valeur Fanions
  add(0x10, 0x15) 0x25 0x00 (----)
  add(0x08, 0x08) 0x10 0x20 (--H-)
  add(0x80, 0x7F, true) 0x00 0xB0 (Z-HC)
  sub(0x10, 0x10) 0x00 0xC0 (ZN--)
  sub(0x10, 0x80) 0x90 0x50 (-N-C)
sub(0x01, 0x01, true) 0xFF 0x70 (-NHC)
  bcdAdjust(0x6D, false, false, false) 0x73 0x00 (----)
  bcdAdjust(0x0F, true, true, false) 0x09 0x40 (-N--)
  and(0x53, 0xA7) 0x03 0x20 (--H-)
  or(0x53, 0xA7) 0xF7 0x00 (----)
  xor(0x53, 0xA7) 0xF4 0x00 (----)
  shiftLeft(0x80) 0x00 0x90 (Z--C)
  shiftRightL(0x80) 0x40 0x00 (----)
  shiftRightA(0x80) 0xC0 0x00 (----)
  rotate(LEFT, 0x80) 0x01 0x10 (---C)
  rotate(LEFT, 0x80, false) 0x00 0x90 (Z--C)
  rotate(LEFT, 0x00, true) 0x01 0x00 (----)

Finalement, la table ci-dessous illustre la différence entre les deux méthodes d'addition 16 bits.

Appel Valeur Fanions
add16L(0x11FF, 0x0001) 0x1200 0x30 (--HC)
add16H(0x11FF, 0x0001) 0x1200 0x00 (----)

3.4 Classe GameBoy

En plus de la classe Alu, une seconde classe nommée GameBoy est à commencer pour cette étape. Comme son nom l'indique, cette classe modélise un Game Boy et a pour but d'instancier les différents composants de la console et de les attacher à un bus commun.

Cette classe sera augmentée au cours des étapes, mais pour l'instant elle contient déjà :

  • un bus, auquel tous les composants sont connectés,
  • une mémoire vive, appelée la mémoire de travail (work RAM) d'une taille de 8192 octets et accessible depuis l'adresse C00016,
  • une copie des 7680 premiers octets de cette mémoire de travail, accessible depuis l'adresse E00016.

Ce que l'on entend par « copie » est que les mêmes données sont accessibles au moyen de deux adresses différentes. Ainsi, aussi bien l'adresse C00016 que l'adresse E00016 désignent le premier octet de la mémoire de travail, et une valeur écrite à l'une ou l'autre de ces adresses peut être relue par la suite à travers l'autre adresse.

La mémoire de travail et sa copie sont mentionnées dans la carte des adresses du Game Boy, présentée à l'étape précédente. Pour faciliter votre travail, nous mettons à votre disposition une archive Zip à importer dans votre projet et contenant une interface nommée AddressMap décrivant cette carte des adresses. Les constantes utiles pour cette étape sont :

  • WORK_RAM_START, WORK_RAM_END et WORK_RAM_SIZE qui caractérisent la mémoire de travail,
  • ECHO_RAM_START, ECHO_RAM_END et ECHO_RAM_SIZE qui caractérisent sa copie partielle (généralement désignée par echo en anglais).

Il faut noter que dans la documentation officielle du Game Boy, la zone echo est décrite comme inutilisable. En pratique, elle se comporte comme décrit ci-dessus, et comme certains programmes font usage de ce comportement non documenté, nous devons le simuler.

La version initiale de la classe GameBoy est très simple et ne comporte qu'un constructeur et une méthode d'accès. Le constructeur a le profil suivant :

  • GameBoy(Object cartridge), qui construit un Game Boy en créant les composants nécessaires (actuellement, seule la mémoire de travail et sa copie) et en les attachant à un bus commun ; l'argument cartridge est à ignorer totalement dans la version actuelle, mais sera utilisé plus tard pour représenter la cartouche attachée au Game Boy ; son type sera alors changé.

L'unique méthode publique de la version actuelle de la classe GameBoy est un accesseur :

  • Bus bus(), qui retourne le bus du Game Boy.

A ce bus doivent être attachés les deux contrôleurs pour la mémoire de travail, qui y donnent accès via les deux plages d'adresses susmentionnées.

3.5 Tests

À partir de cette étape, nous ne vous fournissons plus de tests unitaires. Il vous faut donc les écrire vous-même si vous désirez en avoir, ce qui est fortement conseillé.

Si nous ne vous fournissons pas de tests JUnit, nous vous fournissons néanmoins une archive Zip à importer dans votre projet et contenant un fichier nommé SignatureChecks_2.java. La classe qu'il contient fait référence à la totalité des classes, interfaces et méthodes de cette étape, ce qui vous permet de vérifier que leurs noms et types sont corrects. Cela est capital, car la moindre faute à ce niveau empêcherait l'exécution de nos tests unitaires.

Attention : après avoir importé le contenu de cette archive, n'oubliez surtout pas d'ajouter le répertoire sigcheck qu'elle contient au Java Build Path de votre projet, comme expliqué aux points 5 à 7 de notre guide d'importation. Si vous oubliez de faire cette manipulation, Eclipse ne prendra pas en compte le fichier de vérification de signature et ne vous signalera donc pas les erreurs qu'il contient, le rendant totalement inutile.

Nous vous fournirons de tels fichiers pour toutes les étapes jusqu'à la mi-semestre, et il vous faudra penser à vérifier systématiquement qu'Eclipse ne signale aucune erreur à leur sujet. Faute de cela, votre rendu se verra refusé par notre système.

4 Résumé

Pour cette étape, vous devez :

  • écrire les classes Alu et GameBoy selon les indications données plus haut,
  • documenter la totalité des entités publiques que vous avez définies,
  • rendre votre code au plus tard le 2 mars 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 que la documentation de votre code ne sera pas évaluée avant le rendu intermédiaire. Dès lors, si vous êtes en retard, ne vous en préoccupez pas pour l'instant.

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

Au niveau électronique, l'UAL du Game Boy manipule en réalité des « entiers » de 4 bits seulement, mais elle est présentée au niveau logiciel comme une UAL de 8 bits. Chaque opération sur des données de 8 bits est donc décomposée en deux opérations sur des données de 4 bits. Par exemple, une addition de deux « entiers » de 8 bits est effectuée en chaînant une première addition des 4 bits de poids faible et une seconde addition des 4 bits de poids fort et de l'éventuelle retenue produite par la première addition.

2

Intuitivement, la raison pour laquelle la correction à apporter vaut 6 est que le DCB n'utilise pas 6 des 16 valeurs représentables avec 4 bits. Il faut donc leur « sauter par dessus » en ajoutant ou soustrayant 6, selon l'opération effectuée.