Bus, mémoires et bits
GameBoj – Étape 1

1 Préliminaires

Avant de commencer à travailler à cette première étape du projet, lisez les guides Travailler en groupe, Synchroniser son travail et Configurer Eclipse. Ils vous donneront des informations vous permettant de bien débuter.

2 Introduction

Le but de cette première étape est d'écrire :

  1. une classe permettant de connecter les composants du Game Boy entre eux,
  2. des classes représentant une première catégorie de tels composants, à savoir les mémoires (mortes et vives),
  3. des classes offrant des opérations de manipulation de bits, qui seront utiles tout au long du projet.

Comme toutes les descriptions d'étapes de ce projet, celle-ci commence par une introduction aux concepts nécessaires à sa réalisation, avant de présenter leur mise en œuvre en Java.

2.1 Bus

Comme tout ordinateur, le Game Boy est composé d'un certain nombre de composants électroniques, p.ex. le processeur, les mémoires, le clavier, l'écran, la cartouche, etc. Ces composants sont reliés les uns aux autres par un ensemble de connexions leur permettant d'échanger de l'information et que l'on nomme des bus.

Au niveau électronique, un bus est — schématiquement — composé d'un certain nombre de fils électriques reliant les composants entre eux. Lorsqu'un bus est composé d'un seul fil, on le qualifie de bus série, et quand il est composé de plusieurs fils, on le qualifie de bus parallèle. Le nombre de fils constituant un bus parallèle est nommée sa taille, et est généralement exprimée en bits étant donné que chaque fil permet la transmission d'un bit à la fois.

Les bus qui permettent aux composants d'un ordinateur de communiquer entre eux sont généralement des bus parallèle, et ceux du Game Boy n'échappent pas à cette règle. Comme nous le verrons plus loin, ses deux bus principaux ont respectivement une taille de 16 bits (pour le bus d'adresses) et de 8 bits (pour le bus de données).

2.1.1 Adressage

Etant donné que plusieurs composants sont généralement connectés entre eux dans un ordinateur, il est nécessaire de définir un protocole permettant à ces composants d'exprimer quelle donnée ils désirent s'échanger.

Par exemple, dans le cas du Game Boy, le processeur, la cartouche, différentes mémoires, le clavier, etc. sont connectés entre eux. Admettons que le processeur désire obtenir une donnée stockée sur la cartouche : comment peut-il exprimer le fait qu'il désire une donnée provenant de ce composant-là ? De plus, comment peut-il spécifier la donnée exacte qu'il désire, sachant qu'une cartouche contient plusieurs milliers d'octets ?

Ce problème est similaire à celui de l'acheminement de colis dans le monde réel. Lorsqu'une personne désire envoyer un colis à une autre personne, comment fait-elle pour spécifier le destinataire de celui-ci ? Elle utilise une adresse, qui permet au transporteur de savoir exactement où acheminer le colis.

Dans le monde électronique, une solution similaire a été choisie : on attribue une adresse — qui est un simple entier de taille fixe — à chaque donnée des différents composants connectés ensemble. Ensuite, on utilise deux bus séparés pour les connecter : un bus d'adresses (address bus), sur lequel sont transmises les adresses des données à échanger, et un bus de données (data bus), sur lequel sont transmises les données elle-mêmes.

Dans le cas du Game Boy, le bus d'adresses a une taille de 16 bits, tandis que le bus de données a une taille de 8 bits. Cela signifie que tous les composants du Game Boy sont reliés entre eux par au moins 24 fils électriques, 16 d'entre eux étant utilisés pour transmettre les adresses, les 8 autres l'étant pour transmettre les données.

Admettons maintenant que le processeur désire obtenir une donnée stockée sur la cartouche, comme décrit plus haut, p.ex. la première lettre du nom du jeu qu'elle contient. Comme nous le verrons ultérieurement, cette lettre est stockée sous la forme d'un octet, à l'adresse 308 (13416). Pour l'obtenir, le processeur transmet l'adresse 013416 sur le bus d'adresses. La cartouche, voyant que l'adresse lui appartient, obtient l'octet en question de sa mémoire interne — qui pourrait valoir 5416 si la cartouche était celle de Tetris, cet octet correspondant à la lettre T majuscule — et l'envoie sur le bus de données, d'où le processeur le récupère.

Le fait que le bus d'adresses fasse 16 bits implique qu'il existe 216 = 65 536 adresses différentes, allant de 0 à 65 535. Ces adresses sont généralement exprimées en hexadécimal (base 16), base dans laquelle elles vont de 000016 à FFFF16. Le fait que le bus de données fasse 8 bits signifie qu'il peut transporter un seul octet à la fois, dont la valeur est comprise entre 0016 et FF16. En combinant ces deux informations, on conclut qu'un total de 65 536 octets différents sont « adressables » sur le Game Boy, ce qui n'est pas beaucoup. Comme nous le verrons, différents mécanismes existent pour aller au-delà de cette limite, qui permettent à certaines cartouches de contenir jusqu'à 8 fois plus de données que le Game Boy ne peut en adresser directement.

2.1.2 Carte des adresses

Tout comme celui de la vie réelle, le système d'acheminement des données par un bus informatique implique que chaque donnée ait une adresse qui lui soit propre. Lors de la conception d'un système, il faut donc décider comment attribuer les différentes adresses disponibles aux composants. Cette information est souvent présentée sous la forme de ce que l'on nomme une carte des adresses ou carte mémoire (memory map), qui décrit l'attribution des différentes plages d'adresses.

Une version relativement grossière de la carte des adresses du Game Boy est présentée dans la table ci-dessous. Notez qu'un grand nombre d'éléments qui n'ont pas encore été décrits y sont mentionnés. Leur description sera faite au cours des étapes ultérieures, le but de cette table étant simplement de vous donner une idée de la manière dont les adresses sont réparties.

Plage Contenu
0000-00FF ROM de démarrage (désactivable)
0000-3FFF Banque ROM 0 (sur la cartouche)
4000-7FFF Banque ROM 1 à n (sur la cartouche)
8000-9FFF RAM vidéo
A000-BFFF RAM externe (sur la cartouche, optionnelle)
C000-DFFF RAM de travail
E000-FDFF même contenu que la plage C000-DDFF
FE00-FE9F RAM d'objets graphiques
FEA0-FEFF inutilisable
FF00-FF7F Registres divers
FF80-FFFE RAM haute
FFFF Registre d'activation des interruptions

En consultant les deux premières lignes de cette table, vous constaterez que la plage allant de 0 à FF16 est attribuée aussi bien à la ROM de démarrage qu'à la ROM de la cartouche, ce qui viole la règle exprimée plus haut. La raison en est que l'affectation de cette plage change au cours du temps : lors de l'allumage du Game Boy, elle est attribuée à la ROM de démarrage, mais lorsque le programme qu'elle contient termine son exécution, la plage est attribuée à la ROM de la cartouche. Les détails de ce mécanisme seront décrits dans une étape ultérieure.

2.2 Mémoire

Une mémoire est un composant électronique capable de stocker de l'information. Il en existe principalement deux sortes :

  • une mémoire morte (read-only memory ou ROM en anglais) a un contenu immuable ; il est donc possible de lire des données d'une telle mémoire, mais pas d'en écrire de nouvelles,
  • une mémoire vive (random-access memory ou RAM en anglais1) est une mémoire dont le contenu peut changer au cours du temps.

Une mémoire morte ressemble à un livre imprimé, en ce que son contenu est déterminé une fois pour toutes au moment de sa fabrication, tandis qu'une mémoire vive ressemble à un cahier dans lequel on écrit au crayon, en conservant ainsi toujours la possibilité de modifier le contenu.

La plupart des mémoires vives sont volatiles, ce qui signifie que leur contenu disparaît dès qu'elles ne sont plus alimentées en électricité. Il existe toutefois aussi des mémoires vives non volatiles, les plus connues étant probablement les mémoires Flash qui sont p.ex. utilisées pour stocker la plupart du contenu des téléphones portables, des tablettes et des ordinateurs équipés de « disques » dits SSD.

Dans le cas du Game Boy, toutes les mémoires vives de la console sont volatiles, mais certaines cartouches sont équipées de mémoire vive non volatile sur lesquelles des informations peuvent être stockées de manière permanente : meilleurs résultats obtenus par les joueurs, parties en cours, etc. Par faute de temps, nous ne simulerons pas ces mémoires non volatiles dans ce projet.

3 Mise en œuvre Java

Les concepts importants pour cette étape ayant été introduits, il est temps de décrire leur mise en œuvre en Java. Cette section énumère les classes et interfaces à écrire pour cette étape, et donne quelques conseils sur la manière de les programmer.

Attention : jusqu'à l'étape 6 du projet, vous devez suivre à la lettre les instructions qui vous sont données dans l'énoncé, et vous n'avez pas le droit d'apporter la moindre modification à l'interface publique des classes, interfaces et types énumérés décrits. Vous pouvez par contre définir les méthodes et attributs privés qui vous semblent judicieux.

Notez que toutes les classes et interfaces de ce projet appartiendront au paquetage ch.epfl.gameboj ou à l'un de ses sous-paquetages.

3.1 Représentation des valeurs de 8 et 16 bits

La totalité des données manipulées par le Game Boy font soit 8, soit 16 bits. La question se pose donc de savoir comment représenter de telles valeurs en Java.

Une solution naturelle serait d'utiliser le type byte pour les valeurs de 8 bits et le type short pour les valeurs de 16 bits, car ils se trouvent avoir la bonne taille.

Toutefois, et à quelques exceptions près — p.ex. pour les mémoires décrites plus bas — nous n'utiliserons pas ces types-là, mais plutôt le type int (32 bits). En fonction des cas, seuls les 8 ou 16 bits de poids faible d'une valeur de ce type seront utilisés, les autres bits valant 0.

Ce choix peut paraître suprenant mais il a au moins deux avantages :

  1. Les types byte et short sont signés, donc la moitié de leurs valeurs sont considérées comme négatives par Java, ce qui pose souvent des problèmes ; il est généralement beaucoup plus simple de considérer que l'on ne manipule que des valeurs positives. Toutefois, comme Java n'offre pas d'équivalents uniquement positifs des types byte et short, il est nécessaire d'utiliser un type de plus grande capacité, ici int, en se restreignant aux valeurs positives.
  2. Dans certains cas, avoir à disposition plus de bits que 8 ou 16 est utile, par exemple pour déterminer si une addition de deux valeurs 8 bits a provoqué une retenue. Nous verrons cela à l'étape suivante, par exemple.

Dès lors, et à quelques exceptions près, toutes les valeurs utilisées dans le projet pour représenter des valeur 8 ou 16 bits du Game Boy auront le type int. On fera néanmoins très attention à ce qu'elles ne sortent pas des plages autorisées, à savoir 0 à FF16 pour les valeurs 8 bits, et 0 à FFFF16 pour les valeurs 16 bits.

3.2 Interface Preconditions

Fréquemment, les méthodes d'un programme s'attendent à ce que leurs arguments satisfassent certaines conditions. Par exemple, une méthode déterminant la valeur maximale d'un tableau d'entiers peut s'attendre à ce que ce tableau ne soit pas vide.

De telles conditions sont souvent appelées préconditions car elles doivent être satisfaites avant l'appel d'une méthode : c'est à l'appelant de s'assurer qu'il n'appelle la méthode qu'avec des arguments valides.

En Java, la convention veut que chaque méthode vérifie ses préconditions et lève une exception — souvent IllegalArgumentException — si l'une d'entre elles n'est pas satisfaite.

Par exemple, une méthode max calculant la valeur maximale d'un tableau d'entiers pourrait commencer ainsi :

int max(int[] array) {
  if (! (array.length > 0))
    throw new IllegalArgumentException();
  // …
}

L'interface Preconditions du paquetage ch.epfl.gameboj a pour but de faciliter l'écriture de telles préconditions. En l'utilisant, la méthode ci-dessus pourrait être récrite ainsi :

import static ch.epfl.gameboj.Preconditions.checkArgument;
int max(int[] array) {
  checkArgument(array.length > 0);
  // …
}

Pour ce faire, l'interface Preconditions, du paquetage ch.epfl.gameboj, offre plusieurs méthodes publiques et statiques. La première d'entre elles, utilisée ci-dessus, est une méthode générale de vérification de précondition :

  • void checkArgument(boolean b), qui lève l'exception IllegalArgumentException si son argument est faux, et ne fait rien sinon.

En plus de cette méthode générale, Preconditions offre deux méthodes spécifiques à ce projet, qui permettent de vérifier qu'un entier de type int contient une valeur de 8 ou 16 bits :

  • int checkBits8(int v), qui retourne son argument si celui-ci est compris entre 0 et FF16 (inclus), ou lève l'exception IllegalArgumentException sinon,
  • int checkBits16(int v), qui retourne son argument si celui-ci est compris entre 0 et FFFF16 (inclus), ou lève l'exception IllegalArgumentException sinon.

Ces méthodes sont similaires à des méthodes fournies par la classe Objects de la bibliothèque Java et que nous utiliserons aussi dans ce projet : requireNonNull, checkFromIndexSize, checkFromToIndex et checkIndex.

3.3 Classe Rom

La classe Rom du paquetage ch.epfl.gameboj.component.memory, publique, finale et immuable, représente une mémoire morte. Elle offre un unique constructeur public :

  • Rom(byte[] data), qui construit une mémoire morte dont le contenu et la taille sont ceux du tableau d'octets donné en argument, ou lève NullPointerException si celui-ci est nul.

Attention : comme la classe Rom est immuable, son constructeur doit copier le tableau reçu en argument avant de le stocker, ce qui peut se faire au moyen de la méthode copyOf de la classe Arrays.

En plus de ce constructeur, la classe Rom offre les méthodes publiques suivantes :

  • int size(), qui retourne la taille, en octets, de la mémoire — qui n'est rien d'autre que la taille du tableau passé au constructeur,
  • int read(int index), qui retourne l'octet se trouvant à l'index donné, sous la forme d'une valeur comprise entre 0 et FF16, ou lève l'exception IndexOutOfBoundsException si l'index est invalide.

Attention : comme les valeurs de type byte sont signées en Java mais que la méthode read ne doit retourner que des valeurs positives, cette méthode ne peut pas se contenter de retourner « bêtement » les données du tableau passé au constructeur. Le plus simple pour garantir qu'elle retourne toujours des valeurs positives consiste à utiliser la méthode toUnsignedInt de la classe Byte.

3.4 Classe Ram

La classe Ram du paquetage ch.epfl.gameboj.component.memory, publique et finale, représente une mémoire vive. Elle est très similaire à la classe Rom, si ce n'est qu'elle offre une méthode supplémentaire permettant de modifier son contenu.

L'unique constructeur public offert par la classe Ram est le suivant :

  • Ram(int size), qui construit une nouvelle mémoire vive de taille donnée (en octets) ou lève IllegalArgumentException si celle-ci est strictement négative.

En plus de ce constructeur, la classe Ram offre les méthodes publiques suivantes :

  • int size(), qui retourne la taille, en octets, de la mémoire,
  • int read(int index), qui retourne l'octet se trouvant à l'index donné, sous la forme d'une valeur comprise entre 0 et FF16, ou lève l'exception IndexOutOfBoundsException si l'index est invalide.
  • void write(int index, int value), qui modifie le contenu de la mémoire à l'index donné pour qu'il soit égal à la valeur donnée ; lève l'exception IndexOutOfBoundsException si l'index est invalide, et l'exception IllegalArgumentException si la valeur n'est pas une valeur 8 bits.

La remarque concernant la valeur de retour de la méthode read de la classe Rom s'applique également à la valeur de retour de la méthode read de la classe Ram.

3.5 Interface Component

L'interface Component du paquetage ch.epfl.gameboj.component, publique, représente un composant du Game Boy connecté aux bus d'adresses et de données (processeur, clavier, etc.). Elle a donc pour but d'être implémentée par toutes les classes représentant un tel composant.

Cette interface définit un attribut public de type int, statique et final nommé NO_DATA et valant 10016. Cette valeur est celle qui sera retournée par la méthode read d'un composant — décrite plus bas — pour signaler le fait qu'il n'a aucune donnée à lire à l'adresse reçue. Etant donné que cette valeur ne constitue pas un octet valide — car elle est hors de la plage allant de 0 à FF16 — elle peut remplir ce rôle sans difficulté.

Les deux méthodes abstraites de l'interface Component sont :

  • int read(int address), qui retourne l'octet stocké à l'adresse donnée par le composant, ou NO_DATA si le composant ne possède aucune valeur à cette adresse ; lève l'exception IllegalArgumentException si l'adresse n'est pas une valeur 16 bits,
  • void write(int address, int data), qui stocke la valeur donnée à l'adresse donnée dans le composant, ou ne fait rien si le composant ne permet pas de stocker de valeur à cette adresse ; lève l'exception IllegalArgumentException si l'adresse n'est pas une valeur 16 bits ou si la donnée n'est pas une valeur 8 bits.

En plus de ces méthodes abstraites, l'interface Component offre également une méthode concrète (par défaut) :

  • void attachTo(Bus bus), qui attache le composant au bus donné, en appelant simplement la méthode attach de celui-ci avec le composant en argument (voir la section suivante).

3.6 Classe Bus

La classe Bus du paquetage ch.epfl.gameboj, publique et finale, représente — de manière très abstraite — les bus d'adresses et de données connectant les composants du Game Boy entre eux.

Cette classe contient un tableau dynamique des composants attachés aux bus, et permet de lire ou écrire des données qu'ils contiennent. Pour ce faire, la classe Bus offre les méthodes publiques suivantes :

  • void attach(Component component), qui attache le composant donné au bus, ou lève l'exception NullPointerException si le composant vaut null (la vérification peut se faire facilement au moyen de la méthode requireNonNull de Objects),
  • int read(int address), qui retourne la valeur stockée à l'adresse donnée si au moins un des composants attaché au bus possède une valeur à cette adresse, ou FF16 sinon (!) ; lève l'exception IllegalArgumentException si l'adresse n'est pas une valeur 16 bits,
  • void write(int address, int data), qui écrit la valeur à l'adresse donnée dans tous les composants connectés au bus ; lève l'exception IllegalArgumentException si l'adresse n'est pas une valeur 16 bits ou si la donnée n'est pas une valeur 8 bits.

La raison pour laquelle la méthode read retourne FF16 si aucun composant ne possède de valeur pour l'adresse donnée est que c'est effectivement le résultat obtenu sur un Game Boy, et certains programmes dépendent de ce comportement pourtant non documenté.

Notez que, pour des raisons d'efficacité, la méthode read doit arrêter de parcourir les composants dès que l'un d'entre eux retourne une valeur autre que NO_DATA.

3.7 Classe RamController

La classe RamController du paquetage ch.epfl.gameboj.component.memory, publique et finale, représente un composant contrôlant l'accès à une mémoire vive. Naturellement, elle implémente l'interface Component.

La classe RamController offre deux constructeurs publics :

  • RamController(Ram ram, int startAddress, int endAddress), qui construit un contrôleur pour la mémoire vive donnée, accessible entre l'adresse startAddress (inclue) et endAddress (exclue) ; lève l'exception NullPointerException si la mémoire donnée est nulle et l'exception IllegalArgumentException si l'une des deux adresses n'est pas une valeur 16 bits, ou si l'intervalle qu'elles décrivent a une taille négative ou supérieure à celle de la mémoire,
  • RamController(Ram ram, int startAddress), qui appelle le premier constructeur en lui passant une adresse de fin telle que la totalité de la mémoire vive soit accessible au travers du contrôleur.

En plus de ces constructeurs, la classe RamController offre les définitions des méthodes read et write de l'interface Component, qui lisent et écrivent les données dans la mémoire vive. Bien entendu, ces lectures et écriture ne se font que si l'adresse reçue est dans les bornes passées au constructeur.

3.8 Interface Bit

L'interface Bit du paquetage ch.epfl.gameboj.bits, publique, a pour but d'être implémentée par les type énumérés représentant un ensemble de bits. Elle possède une seule méthode abstraite :

  • int ordinal(), qui sera automatiquement fournie par le type énuméré, étant donné que tous ces types fournissent une telle méthode.

En se basant sur cette méthode abstraite, l'interface Bit définit deux méthodes par défaut :

  • int index(), qui retourne la même valeur que la méthode ordinal mais dont le nom est plus parlant,
  • int mask(), qui retourne le masque correspondant au bit, c-à-d une valeur dont seul le bit de même index que celui du récepteur vaut 1.

Notez que pour mettre en œuvre la méthode mask, vous pouvez attendre d'avoir écrit la méthode mask de la classe Bits décrite ci-dessous.

Le but de l'interface Bit n'étant pas forcément évident, voici un exemple de son utilisation. Admettons que l'on désire représenter un ensemble de jours de la semaine par une valeur de 7 bits, en faisant correspondre un bit à chaque jour. On pourrait pour cela définir un type énuméré implémentant l'interface Bit :

enum Day implements Bit {
  MON, TUE, WED, THU, FRI, SAT, SUN
};

La valeur MON (Monday) de ce type énuméré correspond au bit 0, la valeur TUE (Tuesday) au bit 1, etc. On peut alors représenter, par exemple, l'ensemble des jours du week-end au moyen d'une valeur dont seuls les bits 5 et 6 valent 1, qui s'obtient très facilement au moyen de la méthode mask :

int weekEnd = Day.SAT.mask() | Day.SUN.mask(); // 0b1100000

3.9 Classe Bits

La classe Bits du paquetage ch.epfl.gameboj.bits, publique et finale, a pour seul but de contenir des méthodes utilitaires statiques. Dès lors, elle est rendue non instanciable au moyen d'un constructeur par défaut privé :

public final class Bits {
  private Bits() {}
  // …
}

La classe Bits offre les méthodes publiques et statiques ci-dessous. Notez que certaines de ces méthodes (rotate, signExtend8, reverse8 et complement8) ne sont pas très faciles à mettre en œuvre, et des conseils de programmation sont donc donnés plus bas. Lisez-les avant de commencer à programmer !

  • int mask(int index), qui retourne un entier int dont seul le bit d'index donné vaut 1, ou lève IndexOutOfBoundsException si l'index est invalide, c-à-d s'il n'est pas compris entre 0 (inclus) et 32 (exclus) ; cette vérification peut se faire très facilement au moyen de la méthode checkIndex de Objects et de la constante SIZE de Integer,
  • boolean test(int bits, int index), qui retourne vrai ssi le bit d'index donné de bits vaut 1, ou lève IndexOutOfBoundsException si l'index est invalide,
  • boolean test(int bits, Bit bit), qui se comporte comme la méthode précédente mais obtient l'index à tester du bit donné,
  • int set(int bits, int index, boolean newValue), qui retourne une valeur dont tous les bits sont égaux à ceux de bits, sauf celui d'index donné, qui est égal à newValue (false correspondant à 0, true à vrai) ; lève IndexOutOfBoundsException si l'index est invalide,
  • int clip(int size, int bits), qui retourne une valeur dont les size bits de poids faible sont égaux à ceux de bits, les autres valant 0 ; lève IllegalArgumentException (!) si size n'est pas compris entre 0 (inclus) et 32 (inclus !),
  • int extract(int bits, int start, int size), qui retourne une valeur dont les size bits de poids faible sont égaux à ceux de bits allant de l'index start (inclus) à l'index start + size (exclus) ; lève IndexOutOfBoundsException si start et size ne désignent pas une plage de bits valide (cette vérification peut se faire très facilement au moyen de la méthode checkFromIndexSize de Objects),
  • int rotate(int size, int bits, int distance), qui retourne une valeur dont les size bits de poids faible sont ceux de bits mais auxquels une rotation de la distance donnée a été appliquée ; si la distance est positive, la rotation se fait vers la gauche, sinon elle se fait vers la droite ; lève IllegalArgumentException si size n'est pas compris entre 0 (exclus) et 32 (inclus), ou si la valeur donnée n'est pas une valeur de size bits,
  • int signExtend8(int b), qui « étend le signe » de la valeur 8 bits donnée, c-à-d copie le bit d'index 7 dans les bits d'index 8 à 31 de la valeur retournée ; lève IllegalArgumentException si la valeur donnée n'est pas une valeur de 8 bits,
  • int reverse8(int b), qui retourne une valeur égale à celle donnée, si ce n'est que les 8 bits de poids faible ont été renversés, c-à-d que les bits d'index 0 et 7 ont été échangés, de même que ceux d'index 1 et 6, 2 et 5, et 3 et 4 ; lève IllegalArgumentException si la valeur donnée n'est pas une valeur de 8 bits,
  • int complement8(int b), qui retourne une valeur égale à celle donnée, si ce n'est que les 8 bits de poids faible ont été inversés bit à bit, c-à-d que les 0 et les 1 ont été échangés ; lève IllegalArgumentException si la valeur donnée n'est pas une valeur de 8 bits,
  • int make16(int highB, int lowB), qui retourne une valeur 16 bits dont les 8 bits de poids forts sont les 8 bits de poids faible de highB, et dont les 8 bits de poids faible sont ceux de lowB ; lève IllegalArgumentException si l'une des deux valeurs données n'est pas une valeur de 8 bits.

3.9.1 Conseils de programmation

Certaines des méthodes décrites ci-dessus n'étant pas triviales à programmer, nous vous donnons ici quelques conseils pour faciliter votre travail. Notez toutefois que chacune des méthodes de la classe Bits s'écrit en 1 à 4 lignes de code, jamais plus. Dès lors, si vous commencez à écrire plus de code que cela, arrêtez-vous et réfléchissez à une solution plus simple !

  1. Rotation

    Pour écrire facilement la méthode rotate, il faut se souvenir qu'une rotation a deux propriétés importantes :

    1. une rotation à gauche de \(d\) bits d'une valeur de \(s\) bits peut se faire en combinant un décalage à gauche de \(d\) bits et un décalage à droite de \(s - d\) bits au moyen d'un « ou » bit à bit,
    2. une rotation de \(d ± k\times s\) bits avec \(k\in\mathbb{Z}\) est équivalente à une rotation de \(d\) bits2.

    La seconde propriété implique que n'importe quelle rotation est équivalente à une rotation à gauche de \(d\) bits avec \(d\in \{0, 1, \ldots, s - 1\}\) (ou à droite, mais de la distance complémentaire), où \(s\) est le nombre de bits de la valeur à tourner.

    Dès lors, pour facilement effectuer une rotation d'une valeur de \(s\) bits d'une distance \(d\) quelconque, on peut :

    1. ramener la distance \(d\) à l'intervalle allant de \(0\) (inclus) à \(s\) (exclus), ce qui se fait facilement en Java au moyen de la méthode floorMod de Math (l'opérateur % de Java ne convient pas ici, car il peut retourner une valeur négative),
    2. effectuer une rotation à gauche de cette distance réduite, en combinant des décalages comme décrit plus haut.

    Par exemple, admettons que l'on désire faire tourner la valeur de 4 bits abcd (chaque lettre représentant un bit) de -5 bits, c-à-d de cinq bits vers la droite. On commence par ramener la distance de rotation (-5) à l'intervalle allant de 0 à 3, ce qui donne 3. Ensuite on effectue une rotation à gauche de 3 bits, ce qui donne dabc, qui correspond bien au résultat attendu.

  2. Extension de signe

    Pour effectuer l'extension de signe dans la méthode signExtend8, la technique la plus simple consiste à laisser Java la faire en enchaînant deux conversions de type, comme l'illustre l'exemple ci-dessous :

    int x = 0xAA;     // x = 0b000…010101010 = 170 (32 bits)
    byte y = (byte)x; // y =      0b10101010 = -86 (8 bits)
    int z = (int)y;   // z = 0b111…110101010 = -86 (32 bits)
    

    Dans cet exemple, x est une valeur de type int (32 bits), considérée positive par Java car son bit de poids le plus fort vaut 0.

    Lorsque cette valeur est convertie en byte (8 bits) pour être stockée dans y, ses 8 bits de poids faible sont copiés tels quels. Cela a pour effet de changer l'interprétation par Java de ces bits : étant donné que le bit de poids le plus fort vaut maintenant 1, la valeur est considérée négative.

    Finalement, lorsque y est convertie en int pour être stockée dans z, cette conversion se fait de manière à conserver l'interprétation, donc le signe, de la valeur. Dès lors, le bit de poids le plus fort, valant ici 1, est copié dans les bits 8 à 31 de la valeur convertie.

  3. Renversement de bits

    La technique la plus simple pour mettre en œuvre la méthode reverse8 consiste à utiliser une table. Le tableau ci-dessous donne, pour chaque valeur de 8 bits, sa version renversée. Vous pouvez le copier dans votre programme pour faciliter la mise en œuvre de reverse8.

    new int[] {
      0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0,
      0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0,
      0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8,
      0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8,
      0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4,
      0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4,
      0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC,
      0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC,
      0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2,
      0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2,
      0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA,
      0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA,
      0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6,
      0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6,
      0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE,
      0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE,
      0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1,
      0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1,
      0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9,
      0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9,
      0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5,
      0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5,
      0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED,
      0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD,
      0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3,
      0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3,
      0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB,
      0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB,
      0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7,
      0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7,
      0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF,
      0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF,
    };
    
  4. Inversion bit à bit

    Pour inverser bit à bit une valeur de 8 bits dans la méthode complement8, la technique la plus simple consiste à faire un « ou exclusif » bit à bit avec une valeur constante judicieusement choisie.

3.10 Tests

Pour vous aider à démarrer ce projet, nous vous fournissons exceptionnellement une archive Zip contenant des tests unitaires JUnit pour cette étape.

Pour pouvoir utiliser ces tests, il vous faut tout d'abord les importer dans votre projet en suivant les indications d'importation d'archive Zip dans Eclipse, puis ajouter la bibliothèque JUnit à votre projet, en suivant les explications à ce sujet.

3.11 Documentation

Une fois les tests exécutés avec succès, il vous reste à documenter la totalité des entités publiques (classes, attributs et méthodes) définies dans cette étape, au moyen de commentaires Javadoc, comme décrit dans le guide consacré à ce sujet. Vous pouvez écrire ces commentaires en français ou en anglais, en fonction de votre préférence, mais vous ne devez utiliser qu'une seule langue pour tout le projet.

4 Résumé

Pour cette étape, vous devez :

  • configurer Eclipse selon les indications données dans le document consacré à ce sujet,
  • écrire les classes et interfaces Preconditions, Rom, Ram, Component, Bus, RamController, Bit et Bits selon les indications données plus haut,
  • vérifier que les tests que nous vous fournissons s'exécutent sans erreur, et dans le cas contraire, corriger votre code,
  • documenter la totalité des entités publiques que vous avez définies,
  • (optionnel mais fortement recommandé) rendre votre code au plus tard le 23 février 2018 à 16h30, via le système de rendu.

Ce premier rendu n'est pas noté, mais celui de la prochaine étape le sera. Dès lors, il est vivement conseillé de faire un rendu de test cette semaine afin de se familiariser avec la procédure à suivre.

Notes de bas de page

1

Notez que la terminologie anglaise est un peu malheureuse, car random-access memory signifie littéralement « mémoire à accès aléatoire », c-à-d une mémoire qui offre un accès immédiat à n'importe lequel de ses éléments, indépendemment de sa position. Or même s'il est vrai qu'une mémoire vive est à accès aléatoire, une mémoire morte l'est aussi. Dès lors, il est préférable de se souvenir des acronymes RAM et ROM sans trop réfléchir à leur signification exacte.

2

Notez que cette propriété est similaire à une propriété des rotations dans le plan, à savoir : une rotation d'un angle \(\alpha ± k\times 2\pi\) avec \(k\in\mathbb{Z}\) est équivalente à une rotation d'angle \(\alpha\). Dès lors, n'importe quelle rotation est équivalente à une rotation dans le sens horaire d'un angle \(\beta\) avec \(0\le\beta < 2\pi\) (ou anti-horaire, mais de l'angle complémentaire).