Français

From jmips


BDBengali BGBulgarian CN EN ES ET FR DE IN IT JP PLPolish RO RURussian



Une Page
                        Francophone

Contents

Open-Source Simulateur MIPS en Java

Il s'agit d'un simulateur MIPS open-source facile à utiliser en Java. Vous devez déjà avoir téléchargé le logiciel; consultez le site web de jMIPS pour une copie récente et les dernières mises à jour. Vous receverez une copie de ce document avec le téléchargement dans le répertoire doc/html.
L'architecture d'un processeur MIPS simple

Apprendre à le connaître aide à l'apprentissage de l'architecture MIPS et également à se familiariser avec des notions plus générales des systèmes informatiques et des architectures.

Le processeur MIPS est disponible dans l’archive en cinq modèles différents, définis ici de 1 à 5. Le niveau de sophistication du modèle augmente proportionnellement avec le chiffre correspondant, ainsi:

  1. Processeur base sans pipeline (ces pages)
  2. Processeur avec pipeline sans optimisation
  3. Processeur optimisé avec pipeline
  4. Processeur optimisé avec pipeline et mémoire cache
  5. Processeur optimisé avec pipeline, mémoire cache et gestion des interruptions

Les pages ci-dessous vous mèneront à travers le processus de construction et d'utilisation du simulateur du processeur de base en détails, puis passera en considération les autres modèles. Les modèles ne diffèrent pas dans la façon dont ils sont construits ou utilisés - c'est seulement les entrailles qui sont différents et le résultat est qu'un modèle devient plus rapide qu'un autre dans le même contexte.

Vous voulez apprendre à travailler avec chaque modèle, à son tour, peut-être avec l'objectif à l'esprit d'améliorer le modèle pour le faire exécuter plus vite.

Le temps d'exécution de chaque instruction est l'une des sorties par défaut imprimées à partir du simulateur. Si vous souhaitez obtenir des statistiques telles que la durée nécessaire à l'exécution de chaque classe d’instructions dans des circonstances particulières, vous aurez à ajouter dans le code par vous-même. C'est un code libre. Le code est écrit clairement et expliqué dans les pages suivantes.

Retour en haut


Comment compiler et exécuter le simulateur de base MIPS

Si vous voulez ou devez compiler le code source Java pour accéder au code exécutable, suivez les étapes suivantes, selon les outils auxquels vous avez accès. Puisqu'il y a différents choix d'outils que les utilisateurs sont habitués d'utiliser sous différents systèmes d'exploitation, les sous-sections suivantes sont spécifiques à un système d'exploitation.

Retour en haut

Compiler avec Linux ou Unix

Si vous êtes sur Mac et utilisez un HFS ou HFS+ système de fichiers, allez dans les propriétés du système et activez la sensibilité à la casse.

Désarchivez le fichier zip ou décompressez le fichier tar (obtenez les sur le site web de jMIPS) avec

% unzip jMIPS-1.7.zip

ou

% tar xzvf jMIPS-1.7.tgz

respectivement.

Cherchez ensuite le répertoire src dans la hiérarchie des fichiers nouvellement décompressés et changez votre répertoire courant (utilisez cd).

Je préfère produire le code Java pour la machine virtuelle Java (JVM), avec

% javac CPU/Cpu1.java

dans le répertoire src et ensuite le fichier Cpu1.class peut être exécuté par un JVM sur n’importe quelle plateforme. Vous pouvez faire ainsi sous Linux avec

% java CPU/Cpu1 -q hello_mips

Par exemple

% java CPU/Cpu1 -q hello_mips32
Hello world
%

Retour en haut

Compiler avec Windows

Pour importer la source dans un Java NetBeans IDE, créez un nouveau NB projet (appelé "jMIPS", probablement), faites en sorte que les cases à cocher pour "Main Class", et autres, sont toutes non cochées.

Une fois que l’IDE a construit tous les répertoires et les fichiers de contrôle qu’il a besoin, copiez les fichiers *.java du répertoire src/CPU qui se trouvent dans l’archive du code source dans un nouveau sous-répertoire src/CPU dans le répertoire du projet jMIPS qui vient d’être créé par NetBeans. Utilisez la commande copie du système d'exploitation.

Le IDE détectera ce changement et élargira sa vue arborescence ‘Source Packages’ (le directoire jMIPS/src) pour inclure le package CPU et ses fichiers Java. Si vous préférez, vous pouvez systématiquement renommer les fichiers Java class pour chaque processeur, passant de Cpu1.java, Cpu2.java (et ainsi de suite) à WinCpu1.java, WinCpu2.java, par exemple. Vous aurez à renommer la classe déclarée à l'intérieur de chaque fichier pour qu'ils correspondent.

Retour en haut

Notes sur l'exécution d'un processeur

La signification des options de ligne de commande est la suivante

  • Pas d’options: exécuter le simulateur sans aucune option (en particulier sans "-q"; c'est à dire sans " quiet ") va lister chaque instruction en code machine dès qu'elle est exécutée avec des informations de synchronisation par le côté.
% java CPU/Cpu1  hello_mips32
0:      0.000000007s:   0x80030080:     addiu $29, $29, -32
1:      0.000000012s:   0x80030084:     sw $31, 28($29) 
2:      0.000000019s:   0x80030088:     sw $30, 24($29) 
3:      0.000000024s:   0x8003008c:     addu $30, $29, $0
4:      0.000000030s:   0x80030090:     sw $28, 16($29)
...
218:    0.000001567s:   0x8003000c:     lui $3, -20480
219:    0.000001573s:   0x80030010:     ori $3, $3, 16
220:    0.000001580s:   0x80030014:     sb $3, 0($3)
%

Cette exécution a exécuté 220 instructions en 0.000001580 secondes de simulation (le taux de l’horloge est simulé sur 1GHz). C'était environ 5 tops d'horloge par l'exécution d'instruction.

  • -q: utilisant "-q" ("quiet") supprime la liste des instructions du code machine comme ils s’exécutent. En d'autres termes, avec -q vous ne voyez que ce que le programme en cours d'exécution devrait produire, et aucun bruit supplémentaire.
% java CPU/Cpu1 -q hello_mips32
Hello world
%
  • -d: information supplémentaire est sortie si "-d" ("debug") est donnée. Répéter "-d" augmente le bruit du débogage.
% java CPU/Cpu1 -d hello_mips32
text start at virtual addr 0x80030000 file offset 0x10000
text end   at virtual addr 0x800300e0 file offset 0x100e0
text entry at virtual addr 0x80030080 file offset 0x10080
read 224B at offset 65536 from file 'hello_mips32'
stack start at virtual addr 0xb0000000
stack end   at virtual addr 0xb0100000
0:      0.000000007s:   0x80030080:     addiu $29, $29, -32
1:      0.000000012s:   0x80030084:     sw $31, 28($29)
2:      0.000000019s:   0x80030088:     sw $30, 24($29)
...
%
  • -o: vous pouvez fixer certaines variables internes dans la simulation via "-o MEMORY_LATENCY=1000" (par example). Cherchez le code dans Cmdline.java et ajoutez vos propres paramètres d'options à la liste actuellement très courte!

S'il vous plaît modifiez le code source afin d'ajouter tout ce que vous voulez, mais ajoutez-vous vous-même à la liste des crédits dans le fichier, et envoyez votre code changé au projet ou publiez le vous-même ailleurs, comme vous voulez.

Retour en haut

Produire le code machine MIPS pour le processeur

Le programme “Hello world” produit sur le MIPS R3000 code machine est disponible dans l’archive avec le nom de fichier "hello_mips32" dans le sous-répertoire "misc/".

Le code source (langage C) de ce dernier se trouve dans le fichier « hello_mips32.c » qui est dans le répertoire « misc/ » de l’archive et son assembleur est « hello_mips32.s ». Le code source a été compilé pour produire le code machine avec la commande:

% gcc-static-o hello_mips32-Wl,-e, f hello_mips32.c

sur une machine MIPS réelle. Consultez le manuel pour la commande « gcc » (c.a.d., « man gcc ») pour savoir exactement la signification des options passer dans cette ligne de commande.

Pour les plateformes Unix sans MIPS, ceci devrait produire le même résultat en utilisant le compileur commun "mips-gcc" (vous devez pratiquement exécuter une commande "setup MIPS" dans votre environnement (ou session) shell pour établir le chemin de recherche de votre exécutable afin de déterminer les parties composantes du compilateur commun.

% mips-gcc-DMIPS-mips1-mabi = 32-c hello_mips32.c
% mips-ld-Ttext 0x80003000-ef-o hello_mips32 hello_mips32.o

Le modèle du processeur MIPS remplace parfaitment tous les fonctions d'une vraie machine MIPS pendant l'exécution du code machine de "Bonjour tout le monde" (Hello world).

% Java CPU/Cpu1-q hello_mips32
Hello world

Cependant, des codes machines plus compliquées impliquant des interruptions et des périphériques peuvent vaincre ce modèle.

Retour en haut

Entrer dans le code source

Voici quelques suggestions pour la façon d'entrer dans le code source, et de s'amuser en même temps.

  • Choisissez une partie du code Java source et commentez-le à votre satisfaction personnelle.

Il est assez bien commenté comme il est, mais il y aura sûrement des points où vous sentez qu'il a besoin de plus (ou moins, ou différents) de lignes commentaires.

Editez-le, ajoutez-vous aux crédits dans le premier bloc commentaire du fichier, et envoyez les modifications au projet.

Il s'agit généralement d'une bonne façon d'obtenir une connaîssance profonde du code. Plaignez-vous des parties mal écrites et difficiles à comprendre et corrigez-les.

Vous trouverez de nombreuses notes sur le code dans la section suivante.Vous aurez besoin de ces notes en regardant le code.Les notes se révèleront très utiles en termes de compréhension des caractéristiques à grande échelle, laissant les nuances à être expliquées par des commentaires dans le code source.

  • Ajouter des instructions bgezal et bltzal à l'ensemble d'instructions MIPS manipulées pour l'émulateur dans la grande boucle dans la seule méthode de la classe CPU1 du code Java.

Vérifiez d'abord sur le web ce que les instructions font et quel est leur format! Seulement à partir du nom, on peut dire qu'elles seraient un croisement entre une instruction de branchement et une instruction de saut-et-lien.

Supposons que bgezal est sensiblement la même chose que bgez, mais que dans le cas d'un test réussi, il fait la même chose qu'une instruction jal (mettez l'adresse de l'instruction suivante dans le registre d'adresse de retour, le registre ra). Cela est utile pour réaliser un appel de sousroutine conditionnelle. Vérifiez-le avec google!

Modifiez le code machine pour Hello world avec un éditeur binaire ("BVI" va bien sur Unix si vous êtes un utilisateur de vi; les utilisateurs d'Emacs savent déjà que emacs dispose d'un mode éditeur binaire) et remplacez le code machine de
bnez v0, foo
avec le code machine pour
bgezal v0, foo
.

Testez votre émulateur modifié sur le code machine modifié. Vous aurez besoin toujours d'un changement extra dans le programme afin de sauvegarder et de restaurer l'adresse dans le registre ra, mais vous verrez la nouvelle instruction de branchement fonctionner.

Retour en haut

Notes sur le code source du modèle d'un processeur MIPS simple

La régularité de l'ensemble des instructions MIPS rend très transparent le code Java de chaque modèle de processeur. Le code a également été écrit avec l'objectif d'être clair, sans être trop sophistiqué.

Conséquemment, vous pouvez vérifier ce que toute instruction MIPS fait en révisant les paragaphes correspondant dans le code source.

Le code Java de la classe CPU1 est impératif avec juste le minimum d'emballage orienté-objet. C'est une machine des états! Et c'est le cas aussi, dans la réalité. Tout ce dont vous avez besoin pour comprendre le modèle de processeur CPU1, vous le trouverez rapidement dans le code de la classe CPU1, ne le cherchez nulle part ailleurs.

Vous verrez qu'il y a juste un boucle while grande dans le code. Il utilise le cycle chercher-déchiffrer-(lecture des données)-exécuter-(écrire des données) générique de Von Neumann (voir la figure à droite), tel qu'il existe dans tous les designs de processeurs depuis les années 1940. Toutes les actions du modèle sont incorporés dans cet boucle longue, avec une partie contiguë courte dédiée à chaque sousclasse d'instruction MIPS. Ainsi, vous y trouverez les 10 ou 12 sections contiguës qui composent le corps du boucle.

Par exemple, la partie traitant des instructions de saut ressemble à ceci (la partie «chercher» (fetch) a déjà été faite au début de la boucle, afin de lire l'instruction suivante dans le registre IR). C'est majoritairement composé d'un bloc de commentaires:

 
     /*
      * Jump instructions
      *
      * format  J  : 000010 jjjjj jjjjj jjjjjjjjjjjjjjjj 
      *         JAL: 000011 jjjjj jjjjj jjjjjjjjjjjjjjjj
      *
      * action  of J, JAL:
      * PC <- (bigpageof(PC) << 28) | (jjj...jjj << 2);
      *
      * extra action of JAL:
      * RA <- PC
      */
   if (op == J | | op == JAL) {
       conf.jjjjjjjjjjjjjjjjjjjjjjjjjj = IR.jDST();              // decodage:  registre IR
       pc = PC.read();                                           // lire:      registre PC
                                                                 // executer:  (pas present)
      if (op == JAL)                                             // ecrire:    registres RA et PC
          register_unit.write (R_RA, pc);
      PC.write((bigpageof(pc) << 28) | (conf.jjjjjjjjjjjjjjjjjjjjjjjjjj << 2));
   }

Celles sont seulementes 6 lignes du code en totalité, sans compter les lignes de commentaires.

Pour résumer, le modèle du processeur codé dans la classe CPU1 incarne une conception abstraite du processseur Von Neumann sans les détails de très bas niveau et vous devriez avoir aucun problème de compréhension en lisant ce matériel de temps en temps (astuce: lorsque vous regardez une petite section d'un code source, le regarder avec une question en particulier pour résoudre. Par exemple, trouvez comment la fonction X est utilisée afin que vous puissiez l'utiliser vous-même, et ignorez tout le reste; répétez jusqu'à la fin).

La différence avec un processeur réel est que ce code ne peut faire qu'une seule chose à la fois.

Toutefois, l'exécution est programmée à l'aide des méthodes de la classe Clock (Horloge) et le compte final de ce que s'est passé ne se produit que lorsque le cycle du processeur a été completé. Donc, l'ordre dans le logiciel entre ces deux points temporales n'est pas particulièrement important. Tant que l'ordre est logique, le modèle fonctionne.

Par exemple, le code des sautes ci-dessus écrit le registre RA avec des données lues à partir de l'instruction dans le registre IR. Dans le processeur réel, ces deux choses se produisent simultanément, étant le résultat physiquement inévitable d'un champ de potentiel électrique transmis à travers d'un fil conducteur.

Dans le code ci-dessus, la lecture doit passer avant l'écriture, parce que la mise en œuvre en Java le nécessite! Nous ne pouvons pas écrire en Java "lire et écrire A à la suite dans B simultanément". Mais l'univers réel le fait aussi simple que ça en réalité! La comptabilité contenue dans les composants impliqués pour le code Java, cependant, va enregistrer le même instant simulé à la fois pour la lecture et l'écriture, et c'est tout ce que vous verrez à la fin du cycle.

Retour en haut

Organisation des classes

Les classes suivantes de niveau supérieur se trouvent dans le code source Java.

Seulement les cinq ou six premières contiennent un code méritant discussion, et la discussion suit le tableau ci-dessous:

Cpu1 (Contenant le point d'entrée principal) configure le processeur et exécute la simulation
CPU1 le fonctionnement du cycle fetch-decode-exécuter continu
RegistersUnit une émulation de l'unité de registres dans le processeur
ALU une émulation de l'unité arithmetique dans le processeur
Memory une émulation de l'unité de mémoire
IOBus une émulation d'un bus d'entrées/sorties
Clock une unité spéciale E/S qui accède à l'horloge du système
Register une émulation d'un registre unique, tel qu'il est incorporé dans RegistersUnit
SignExtend une émulation de l'unité d'extension de signe de 16-32 bits de dans le processeur
Console une émulation de l'unité E/S composé pour le clavier et l'écran
Clock une émulation horloge globalement unique, imitant le signal d'horloge dans le processeur
Utility une classe contenant des routines utiles - byteswapping, etc
Globals une classe contenant toutes les constantes globales, telles que la valeur numérique de l'opcode pour JAL
Cmdline cette classe fait la gestion des options du simulateur données dans la ligne de commande
OptionT une classe encapsulant la description des options de ligne de commande,
Elf une modélisation de la structure des fichiers exécutables (`ELF') sur disque, utilisés pour lire un fichier exécutable et de capturer le code machine du programme à l'intérieur
RegReturnT une classe modélisation d'une paire de valeurs obtenus de lire simultanément deux registres de l'unité de registres dans le processeur
AluReturnT une classe modélisation d'une paire de valeurs de sortie de l'ALU dans le processeur
DecodeReturnT une classe modélisation de l'ensemble d'attributs (codigo opération, indice du registre source, le décalage de branchement, etc) décodées à partir d'une instruction de code machine dans le processeur
InstructionRegister une classe contenant des méthodes pour le décodage d'une instruction de code machine dans le registre IR dans le processeur
Debug quelques méthodes que impriment detailles du debogage sur l'écran

Il s'agit d'un soit-disant 'modèle domaine'. Les classes dans le code source correspondent à des véritables composantes matérielles dans le processeur MIPS. Les méthodes correspondent aux véritables opérations physiques que les composants matérielles peuvent faire. Les opérations virtuelles, telles que celles qui peuvent être composées pour les combinaisons des opérations simples, ne sont jamais mises en œuvre dans le logiciel, peu importe si cela semble pratique, car ils n'ont aucune existence physique.

Diagramme UML de classes pour le code jMIPS

Le diagramme de classes à droite montre les dépendances.

Si vous souhaitez ajouter une fonction qui permet l'exécution pas-à-pas du modèle CPU1, vous avez besoin de faire pauser le cycle chercher-decoder-exécuter au début de la boucle while, et y attendre l'entrée d'utilisateur avant chaque nouveau cycle. Vous devriez attendre un 's' ("step") de l'utilisateur pour indiquer qu'il faut exécuter encore un cycle, un 'c' ("continuer") devrait indiquer la rentrée à exécution continue, un p ("print") et un numéro de registre ou de l'adresse mémoire doit montrer le registre ou contenu de la mémoire sur l'écran; un d ("display") devrait être comme l'impression, mais provoquant l'impression aussi à chaque pas ensuite.

Retour en haut

La routine principale dans Cpu1

Ceci est un précis rapide de la routine principale (main) dans Cpu1, la classe wrapper dont le travail consiste à interpreter les arguments de la ligne de commande Java::

public void main(String argv[]) {

    Cmdline.analyse(..., argv);                                // faire l'analyse de la ligne du commande

    Elf elf[] = new Elf[Cmdline.args.length];
    for (int j = 0; j < Cmdline.args.length; j++) {            // charger les fichiers executables `ELF'
        String filename = Cmdline.args[j];
        elf[j] = new Elf(filename);
    }
    byte stack[] = new byte[STACK_END - STACK_START];          // preparer la zone memoire du pile

    CPU1 cpu = new CPU1();                                     // construire le processeur
    {                                                          // passer la zone memoire du pile au manager memoire
        cpu.memory.addRegion(stack, STACK_START, STACK_END);
        for (int j = 0; j < elf.length; j++) {                 // passer les zone memoires aditionelles au manager
            cpu.memory.addRegion(elf[j].text, elf[j].text_start, elf[j].text_end);
        }
    }
    cpu.SP.write(STACK_END);                                   // changer son pointeur a indiquer en haut dans le pile
    cpu.PC.write(elf[0].text_entry);                           // changer le pointeur programme a indiquer le 1er instr
    cpu.run();                                                 // laisser courrir la simulation!
}

Vous voyez ici que l'analyse principale de la ligne de commande utilise la méthode éponyme de la classe Cmdline. L'analyse implique l'obtention d'une liste de fichiers exécutables dans lequels se trouve t-il le code machine à exécuter. Les fichiers exécutables sont structurés selon le format connu sous le nom `ELF'.

ELF est un standard multi-plateforme utilisé dans plusieurs systèmes d'exploitation actuellement en usage aujourd'hui et il est le format produit de les compilateurs gcc MIPS. La routine principale continue ensuite avec le chargement des fichiers ELF dans la mémoire dans le modèle du processeur CPU1.

Autrement dit, le code Java fait appel au constructeur Elf pour chaque nom de fichier donné dans la ligne de commande. Le constructeur analyse le contenu du fichier et extrait quelques informations telles que les points d'entrée, les diferents zones d'adresses virtuelles, etc, tous placés dans l'objet Elf de Java que resulte.

Puis le code construit la zone de la pile dans la mémoire du modèle, et indique à l'unité de mémoire son adresse avec un appel à la méthode addRegion. Il indique aussi à l'unité de mémoire les adresses des possiblement plusiers tronçons de code ramassés dans les fichiers exécutables ELF.

Il reste à définir la valeur initiale du registre SP (pointeur de pile), mettre la valeur initiale du registre PC (compteur de programme) égale à l'adresse du point d'entrée du programme - obtenu à partir du premier fichier dans la ligne de commande - et ensuite appeler la méthode run du nouvellement construit et préparé processseur de la classe CPU1.

Voilà c'est tout. Donc ce qui suit est tout ce que le programmeur a besoin de connaître de la classe Cpu1:

Cpu1
static void Main (args String []) point d'entrée; analyse les options de ligne de commande, construit CPU et laisser courrir

La section suivante vous montre ce que le mot run apliqué au modele processeur CPU1 veut dire.

Retour en haut

Le modèle du processeur CPU1

Ce qui suit est tout le programmeur peut voir ou a besoin de connaître dans la classe CPU1 que represente en Java le foncionnement du processeur. Il y a un constructeur (defaute), et une méthode.

CPU1
CPU1 () CPU1 constructeur
void run () cycle chercher/décoder/exécuter

Le code Java de la méthode run n'est pas conceptuellement difficile. C'est juste la boucle ci dessous:

public void run() {
    while (Clock.running()) {
         ... /* interieur du cycle Von Neumann chercher-décoder-exécuter */ ...
    }
}

Comment dit dans le commentaire, la boucle contient le code qui fait ce que le cycle chercher-décoder-exécuter doit faire:

 
// FETCH!

int pc  = PC.read();                                  // lire PC
int ir  = memory.read32be(pc);                        // lire l'instruction a l'adresse dans PC
IR.write(ir);                                         // ecrire l'instruction dans IR
pc      = feAlu.execute(ALU_ADDU, pc, 4).c;           // ajouter 4 au valeur du PC utilisant le composante adder
PC.write(pc);                                         // diriger le valeur incrementé du PC au PC

// DECODER!

byte op = InstructionRegister.OPCODE(ir);             // décoder l'opcode
DecodeReturnT conf = new DecodeReturnT();
{
    conf.op = op;                                     // diriger la sortie aux fils op,ir,pc
    conf.ir = ir;
    conf.pc = pc;                                     // NB: le valeur "PC original"+4 est caché dans conf.pc
}
switch(op) { 
  case 0:                                             // traiter les operations arithmetiques ("ALU ops")

    conf.sssss = InstructionRegister.aluopSRC1(ir);   // décoder le reste des champs de l'instruction 
    conf.ttttt = InstructionRegister.aluopSRC2(ir);
    conf.ddddd = InstructionRegister.aluopDST (ir);
    conf.func  = InstructionRegister.aluopOP  (ir);

    Clock.increment(DECODE_LATENCY);                  // anoncer le passage du temps a l'horloge

    // LIRE!                                          // lire le contenu des deux registres indiqués

    RegReturnT r = register_unit.read(conf.sssss, conf.ttttt);

    // EXECUTER!

    AluReturnT s = alu.execute(conf.func, r.a, r.b);  // faire l'operation arithmetique indiqué

    // ECRIRE!

    register_unit.write(conf.ddddd, s.c);             // diriger le resultat de l'operation au registre indiqué
    break;                                            // ESAC

La séquence chercher/décoder/(lecture)/executer/(écrire) est clairement visible dans le code.

Il y a un bloc conditionnel dédié à chaque sousclasse d'instruction (une par opcode distincte). Le fragment du code ci-dessus montre le bloc dédié au traitement des opérations ALU, ceux qu'ils ont tous l'opcode 0.

Les différentes opérations ALU se distinguent par des valeurs différentes du champ final (function code) dans l'instruction (les 6 derniers bits à la fin du mot bigEndian).

Tous les champs distincts de l'instruction sont éclatés, y compris le code de fonction, dans la section intitulée DECODER!, puis la fonctionnalité de l'instruction est mise en œuvre dans les sections courtes de code Java intitulées LIRE!, EXECUTER!, ECRIRE!

Retour en haut

Qu'est-ce que c'est l'acceptable et l'inacceptable dans le code d'un modèle du processeur?

Notez que le code dans la classe CPU1 utilise consciencieusement les sous-composants pour son travail.

Au lieu d'ajouter 4 à la valeur PC en utilisant la puissance du langage Java ("pc + = 4"), par exemple, le code FETCH lit le registre PC utilisant la méthode read de l'objet, puis il ajoute 4 utilisant la méthode execute de l'unite arithmetique feALU, puis il dirige le résultat vers le registre PC utilisant la méthode d'écriture de l'objet. C'est un détour longue où "pc + = 4" aurait pu faire le meme travail! Qu'est-ce que c'est que passe! Qu'est-ce qu'il y a qu'on permit ici et qu'est-ce que c'est qu'on ne permit pas?

Il est acceptable dans le code Java à étiquetter les valeurs de sortie d'un unité de et les utiliser comme les entrées aux autres unités. Nous l'avons fait avec "pc = ..." et "... = pc". Ce genre de chose fait la meme chose que fait le câblage que connecte les signaux réelles au travers des circuits du processeur dans un seul cycle d'horloge. C'est bien acceptable.

Faire quelque chose comme 'pc+=4' en Java serait un facon de tricher, car cela prend du temps et occupe des circuits dans l'univers réelle. Les objets Java que occupent le siege des composants matériels du processeur (ALU, unite des registres, etc) posedent une synchronisation intégrée avec l'horloge et les utiliser dans le code Java avance correctement l'horloge simulé. Tels objets doivrent etres utilise pour faire les calculs arithmetiques. Les calculs ont besoin d'un petit temps pour completer dans l'univers réelle, et les méthodes Java de tels objets avancent le temps simulé correctement. Tout OK.

Pour contre, utilisant code Java comme "pc + = 4" obtient magiquement l'effet désiré sans faire passer du temps simulé et sans occuper les unites simulés! Cela ne veut pas imiter l'univers réelle, c'est l'émulation de la magie. Pas OK.

Ainsi, toute l'arithmétique doit être fait en utilisant les objets Java représentant des composants matériels du processeur, et on n'est pas permit a utiliser les operations arithmétiques du langue Java.

Car le code de la section DECODE est ecrivé explicitement en ligne ici, ce que signifie qu'il ne fait pas passer du temps simulé, une mise à jour d'horloge explicite a été incorporé à la fin de la section DECODE afin de maintener l'honnête du simulateur.

Au contraire, la section FETCH n'a pas besoin de mettre à jour l'horloge avec un increment du temps supplémentaire parce que son temps d'execution est dominé pour l'effort a lire l'instruction du cache / la mémoire, et l'horloge est correctement mis à jour pour l'appel a la méthode d'accès a la mémoire dans cette section du code.

La règle pour ecriver le code Java d'un modele simulé dans ce genre est qu'on peut utiliser des variables Java pour garder les valeurs interresants, mais pas pour plus qu'un seul cycle de l'horloge simulée. Ils sont comme des valeurs des signaux transitoires. Toute valeur du signal que dure plus qu'un cycle simulé de l'horloge doit être gardé dans un registre.

C'est cette règle que a été observé en ecriver le code Java du modele CPU1.

Les valeurs transitoires pc et ir sont acheminés vers les registres PC et IR ou ils sont gardés pendant multiples cycles d'horloge.

Retour en haut

Les classes du composants CPU

Le reste de cette section décrit les classes Java qui représentent des composants au sein de la CPU. Il s'agit d'un document de référence! Vous lisez une section juste quand vous en avez besoin et non pas avant.

La classe de composant RegisterUnit

Cette composante constitue la 'mémoire petite et super rapide' à l'intérieur du processeur.

RegisterUnit
RegisterUnit() Constructeur du register unit
RegReturnT read (byte, byte) lire deux ints de deux registres numérotés
void write (byte, int) écrire un int à un registre numéroté
void write (byte, byte, int, int) écrire deux ints respectivement à deux registres numérotés

Les unités Register sont composées d'une collection r de 32 (en fait un peu plus afin d'incorporer PC et IR aussi) registres.

Il n'y a rien de surprenant dans les codes du méthode read et write. Lire et / ou écrire est tout ce qui peut être fait à des registres matériels. Le nombre d'arguments indique le nombre de registres qui sont lues et écrites en même temps (jusqu'à deux lectures et une écriture sont supportés, simultanément). Le type 'byte' pour les indices du registre comme arguments est juste pour que vous ne confondez pas les arguments qui sont les indices de 5 bits de registres avec les arguments qui sont les 32 bits contenu des registres. Oui, nous pourrions utiliser 'int' pour représenter les deux, mais alors vous ne pourriez pas voir qui a été à la recherche de la signature du méthode, et les erreurs de programmation en suivra.

private RegReturnT readAndWrite (byte x1, byte x2, bool w_e, int d1) {

        RegReturnT res = new RegReturnT ();
        Clock.increment (REGISTER_LATENCY);             // garder le chronométre à jour
        res.a = r[x1].read ();                          // lire deux registres
        res.b = r[x2].read ();
        if (w_e && x1 != 0)                             // si écrire est activée, écrire un registre
            r[x1].write (d1);
        return res;                                     // retourner les contenus (ancien) qui ont été lu du registre
}
public RegReturnT write (byte x1, int d1) {             // écrire un registre, retourner l'ancienne valeur 
        
        return readAndWrite (x1, 0, true, d1);
}
public RegReturnT read (byte x1, byte x2) {             // lire deux registres
 
        return readAndWrite (x1, x2, false, 0);
}

La seule chose à noter est la mise à jour automatique de l'horloge lors de l'utilisation de l'unité de registre. Cela garantit que l'utilisation de cette composante conduit à un honnête comptabilisation du temps dans la simulation. Lire et écrire se produisent simultanément. Quand on regarde le code du composant de la classe Register, nous allons voir que l'écrit prend effet lors du prochain cycle, de sorte qu'on peut faire de nombreuses distincte lectures et une écriture à un registre dans chaque cycle dans le logiciel, et ce sera comme si tout cela est arrivé à la fois dans le simulateur, à la fin du cycle.

Retour en haut

La classe ALU

La composante ALU fait l'arithmétique dans le CPU.

ALU
ALU () Constructeur ALU
AluReturnT execute (byte, int, int) exécuter une opération ALU donnée par opcode sur deux ints retournant le résultat de type int et un reste (carry) de type int

La classe ALU contient une importante quantitè de code, mais il est entièrement simple. Le code de la méthode execute se compose d'une instruction switch qui choisit un bloc court de code à exécuter sur le func (code de fonction) la valeur fournie comme argument. Le code combine les deux integers a et b d'une manière appropriée pour produire le résultat c et un reste (carry) ou l'indicateur zéro z:

public AluReturnT execute(byte func, int a, int b)
{
    AluReturnT res = new AluReturnT ();               // préparer à retourner deux mots de 32 bits
    switch (func) {
      case ALU_ADD:
        long r = a; r += b;                           // intérieurement l'addition signé de 64-bit 
        res.c = (int) (r & 0xffffffff);               // capturer le 32 bits du bas du résultat
        res.z = (int) ((r >> 32) & 0xffffffff);       // capturer le 32 bits d'en haut du résultat
        break;
      ... 
        // la traitement d'autres ops ALU 
    }
    Clock.increment(ALU_LATENCY);                     // noter la mise à jour automatique de l'horloge
    return res;
}

Le ci-dessus illustre uniquement le code de l'opération d'addition, mais il est parfaitement représentatif. Aucun code de l'ALU est plus compliqué que cela.

Retour en haut

Le composant du classe IOBus

Le IOBus fait partie d'une communication à trois voies entre le CPU, la mémoire et lui-même. Lorsque le CPU adresse certaines locations du mémoire, l'accès est redirigé vers le gestionnaire de mémoire qui fait partie du unité de mémoire (memory unit) au lieu de la IOBus.

Les méthodes read8 et write8 de la classe IOBus prennent une adresse de bus ( " 0 ", " 1 ", etc. ) au lieu d'une adresse mémoire. La redirection remplace une addresse bus (bus address) par une addresse memoire (memory address).

Il y a une méthode addIO qui enregistre une unité d’I/O (par exemple un objet de Console) sur le bus, ce qui lui donne une adresse de bus. Les adresses des bus sont attribués dans l'ordre d'inscription pour chaque unité d'I/O qui en veut, à tour de rôle. Lorsque le CPU et l'unité de mémoire sont misent en place, le bus est construit et une console unique est enregistré au bus aux deux adresses de bus 0 et 1 comme ceci:

IOBus iobus     = new IOBus();
Console console = new Console();
short stdin     = iobus.addIO(console);             // console  enregistré à l'addresse 0 du bus
short stdout    = iobus.addIO(console);             // console  enregistré à l'addresse 1 du bus

Adresse 0 du bus sera la liaison utilisé pour GETCHAR_ADDRESS dans l'unité de mémoire, representant à une lecture depuis la console, et l'adresse 1 du bus sera la liason pour PUTCHAR_ADDRESS, representant à une écrit sur la console.

L'interface complete aux méthodes du bus I/O est

IOBus
IOBus ( ) Constructeur
read8 (short busaddr) lisez un byte a travers le bus depuis a l'adresse du source en utilisant l'adresse du bus I/O
write8 (short busaddr, byte data) ecrire un byte sur le bus a l'adresse ciblé, retourner le chiffre écrit
addIO (IOUnit u) enregistrer une nouvelle unité I/O sur le bus pour lire et écrire, retourner l'adresse du bus

IOUnit est une classe abstraite ( "interface" ). Les objets du Console correspondent à cette interface. Pour répondre aux exigences de cette interface un objet doit seulement avoir que les méthodes read8 et write8. Ces méthodes ne prennent pas dutout une adresse car elles sont associées qu'à leur propre unité d'I/O :

IOUnit
byte read8 ( ) throws IOException lire un octet (envoyer une exception si échec)
int write8 (byte) érire un octet, retourner le nombre de byte écrits

Un IOUnit produit un octet sur une requête de lecture, et absorbe un octet sur une demande d'écriture. Les objets de Console en particulier ont ces méthodes. L'unité de mémoire qui établit le code ajoute une console au iobus et relie son adresse bus à une adresse mémoire via la liste de port. Le résultat c'est que l'objet du console est accessible sur le iobus via mémoire mappée adressage (memory mapped addressing) de l'unité de mémoire.

Pourquoi avons-nous pris la peine d’introduire un bus pour être un intermediare entre l'unité de mémoire et qu'est ce qu'il y a à ce moment d'une seule unité d'I/O, une console unique?

C'est vraiment pour qu'on puisse modéliser les retards introduite par le fait que le bus ne peut être utilisé que pour accéder à un bloc d'I/O à la fois, et seulement un octet à la fois peut se déplacer sur le bus (dans une seule direction à la fois), et le fait que dans la vie réel le bus d'I/O est aussi relativement lent - 33  MHz, par rapport au CPU de 1  GHz.

Le CPU peut exécuter 30 instructions dans le temps qu'il faut pour 1 octets de traverser le bus I/O.

Si l'unité de mémoire tente d'accéder au bus alors qu'elle est déjà en cours d'utilisation, il y aura difficulté. Assurez-vous que les écritures au bus d'I/O sont au moins 30ns d'intervalles sinon vous ferez face à ces difficultés et des choses étranges commenceront à se produire tels que des écrits au bus se voient rejetés.

Dans la pratique, ce n'est pas un grand problème. Quelle que soit le code machine que vous écrivez, c'est presque du jamais vu à produire une boucle suffisamment serrée pour faire deux écrit à l'I/O bus à moins de 30ns. La conception du CPU1 lutte pour delivrer 6 exécutions d'instructions machine durant ce moment. Ce n'est pas suffisant pour savegarder des registres, exécuter un saut à une sous-routine, faire quelque chose, revenir en arrière, restaurer les registres, augmenter un compteur de boucle, contrôler le saut du boucle, etc.

Il y a du code qui produit du bruit sur le flot/flux d'erreur du simulation si le bus d'I/O arrive a submergé et cela n'arrive pas en pratique. Mias cela pourrait ce produire. Pour s'amuser, essayer de la produire, en écrivant le code machine approprié (via l'assembleur MIPS, bien sûr). Vous aurez un flux de plaintes sur l'écran.

Des CPU réelles ont aussi leurs limites, et les véritables programmeurs leur écrivent. Vous pouvez écrire presque tout ce que vous voulez dans l'assembleur, et si le métal en dessous peut répondre comme vous en voulez ou pas est une différente question. Les véritables programmeurs suivent le principle de “ne le fait pas alors”.

Par exemple, si vous consultez le code des véritables assembleur vous trouverez que chaque opération de saut (jump instruction) est suivie par un NOP parce que le pipeline et le mécanisme de chercher en avance les instruction font qu'il soit innevitable que l'instruction apres un jump sera exécuté .


Les programmeurs avares pouvaient en théorie économiser l'espace d'un programme directement en suivant un saut (jump) avec une instruction mention non-trivial, mais ils ne le font pas, parce qu'ils savent ce qui va se passer. L'évolution a enseigné auz véritables programmeurs (ceux qui essaient d'en faire leur mieux pour obtenir leur prochain salaire) que la réalité est plus forte que la théorie. Les compilateurs ne le font pas, non plus, pour la même raison.

Retour en haut