Simulation du microprocesseur GAPP en Smalltalk

 

 

Le G.A.P.P. (Geometric Arithmetic Parallel Processor) de N.C.R. est un Microprocesseur Parallèle à architecture systolique. Il a été conçu dans les années 1980 pour effectuer du traitement en temps réel.

 

Anatomie du GAPP

 

Il est composé d’une grille de 72 (6x12) processeurs élémentaires (PE). Son architecture est de type SIMD (Single-instruction multiple-data), c'est-à-dire que tout les processeurs élémentaires effectuent  le même code en même temps, mais sur des données différentes.

 

Chaque PE possède 4 registres (CM, NS, EW et C) de 1 bit, une unité arithmétique et logique et une mémoire locale de 128 bits. Chaque unité est reliée à ses quatre voisins immédiats nord, sud, est et ouest, pour former un maillage carré. Un bus d’adresse de 7 bits et un bus de contrôle de 13 bits sont communs à tout les PE.

 

Figure 1 : Chaque PE est relié à ses voisins immédiats au nord, au sud à l’est et à l’ouest par des lignes de communications bidirectionnelles N/S et E/W respectivement. Les lignes de contrôle de C0 à C12, l’horloge (CLK), la sortie globale (GO), et les lignes d’adresse (A0 – A6) sont les seules connections d’un processeur élémentaire interne vers le monde extérieur. Chaque processeur élémentaire comporte une unité arithmétique et logique à 1 bit, 4 registres et 128 bits de RAM.

 

 

 

Figure 2 : Diagramme de connexion de 4 éléments

 

La sortie globale (GO : global output) collecte le ou logique des valeurs de NS de l’ensemble des PE.

 

Le schéma suivant décrit la structure d’un processeur élémentaire (figure 3).

Figure 3 : Les registres CM (registre de communication) NS (nord-sud), EW (est-ouest) et C peuvent prendre les valeurs (0 ou 1) depuis différentes sources. Les lignes de contrôle déterminent quelles données sont transmises dans les registres. Par exemple, si les lignes C2, C3 et C4 valent respectivement 1, 0, et 1 c’est la valeur du registre C qui sera transmise dans le registre NS.

Si les contrôles C8, C9 et CA sont à 1, c’est un 1 qui sera chargé dans le registre C.

 

Cela mène à la structure d’une instruction pour le GAPP, composée d’une adresse sur 7 bits, et d’un code opératoire sur 13 bits divisé en 5 tranches indépendantes.

Les bits C0, C1 indiquent comment charger le registre CM, les trois suivants, C2, C3, C4 contrôlent la valeur de NS etc… La figure 4 donne l’ensemble des codes.

 

Figure 4 : jeu d’instructions

 

Pour le contrôle de la mémoire RAM, les bits Cb et Cc à 0 indiquent une lecture, les combinaisons 01, 10, et 11 indiquent que l’on écrit CM, C ou SM dans la mémoire.

 

SM est la somme délivrée par l’unité arithmétique et logique.

L’UAL n’a pas de ligne de contrôle parce qu’elle effectue toujours la même opération : à partir des trois entrées NS, EW et C elle délivre la somme SM, la retenue Cy (carry)  ou l’emprunt BW (borrow) selon la table suivante :

 

Figure 5

 

On peut l’utiliser pour effectuer des opérations logiques comme le ET logique (par exemple en plaçant 0 dans C, le CY prendra la valeur de NS.EW. De même si C=0, SM contiendra le XOR (ou exclusif) de NS et EW. Voir la figure 6.

 

Figure 6

 

Simulation de l’UAL

 

Commençons par la simulation de l’Ual. Pour représenter des Ual, nous créons une classe de ce nom, avec sa structure, puis nous définissons leurs comportements en écrivant des méthodes.

 

Le cadre ci-dessous donne la définition de la classe Ual. Les Ual possèderont 6 variables d’instance qui représenterons les trois entrées et les trois sorties.

 

Object subclass: #Ual

            instanceVariableNames: 'inputNs inputEw inputC outputSm outputCy outputBw '

            classVariableNames: ''

            poolDictionaries: ''

            category: 'VL-Gapp'!

 

Pour accéder à ces variables, nous définissons des méthodes d’accès. Les entrées doivent pouvoir être affectées, et les sorties doivent être lisibles.

 

!Ual methodsFor: 'accessing'!

 

inputC: c

            inputC := c!

 

inputEw: ew

            inputEw := ew!

 

inputNs: ns

            inputNs := ns!

 

outputBw

            ^outputBw!

 

outputCy

            ^outputCy!

 

outputSm

            ^outputSm! !

 

Ces méthodes sont regroupées dans un protocole nommé “accessing”.

Le signe := dénote l’affectation de variable. La variable à gauche du signe prend la valeur de l’expression située à droite du signe. Une affectation constitue une instruction Smalltalk.

 

Le signe ^ indique la valeur à retourner. Lors de la transmission du message « outputSM » à un objet de la classe Ual, la valeur retournée sera l’objet pointé par la variable d’instance « outputSM ». Notez que la méthode porte le même nom que la variable d’instance. C’est l’usage, lorsqu’on écrit de simple méthode d’accès de nommer la méthode avec le nom de la variable pour la lecture. Pour l’écriture, comme on a besoin d’un paramètre, celui-ci est introduit par le caractère « : ». Le deux-points doit être « collé » au nom, sans espace, en fait il fait partie du nom de la méthode.

 

Maintenant, le cœur du problème : à chaque cycle d’instruction, nous fournirons aux Ual leurs trois entrées, et nous leur demanderont de calculer les valeurs de sortie. Nous implémentons cela en écrivant une méthode nommée « executer ».

Nous avons trois variables binaires inputNs inputEw inputC, cela nous donne 8 combinaisons de valeurs possibles. La formule (4 * inputNs) + (2 * inputEw) + inputC donne un résultat unique pour chaque combinaison, de 0 à 7, comme si on lisait en binaire les trois premières colonnes de la table de la figure 5.

De même, si on concatène les trois bits de résultat SM, CY et BW et que l’on transcrit en base dix, on peut écrire une nouvelle table :

 

Entrées

Sorties

0

2

4

6

1

3

5

7

0

5

4

2

5

3

2

7

 

En réordonnant la première colonne cette table devient :

 

Entrées

Sorties

0

1

2

3

4

5

6

7

0

5

5

3

4

2

2

7

 

Pourquoi cette réorganisation ? Simplement parce que dans le langage Smalltalk, nous disposons de tableaux (objets appartenant à la classe Array) qui peuvent contenir des valeurs auxquelles on accède par un numéro. Pour construire un tableau d’entier correspondant à la seconde colonne de notre table, il suffit d’écrire : #(0 5 5 3 4 2 2 7).

Pour accéder à un élément de ce tableau, on lui envoie le message « at: » avec en argument le numéro de la case souhaité. 1 pour la première case, 2 pour la seconde, etc…

 

Au lieu d’utiliser la formule formule (4 * inputNs) + (2 * inputEw) + inputC qui fait appel à l’addition et à la multiplication, nous préférons utiliser le décalage et le ou logique (plus rapide).

Le décalage à gauche est calculé en Smalltalk par le message bitShift: avec un argument positif. Le décalage à droite avec bitShift: et un argument négatif.

Le ou logique avec bitOr: et le et logique  avec bitAnd:.

 

Le ou logique nous sert à combiner les trois bits d’entrée en un seul mot.

X+0 = X

X+1 = 1

Le et logique nous servira à extraire du résultat le bit qui nous intéresse en masquant les autres avec des zéros.

X.0 = 0

X.1 = X

 

!Ual methodsFor: 'execution'!

 

executer

            | k resultat |

            k := ((inputNs bitShift: 2)

                        bitOr: (inputEw bitShift: 1))

                        bitOr: inputC.

            resultat :=

                        #(0 5 5 3 4 2 2 7) at: k + 1.

            outputSm := (resultat bitShift: -2).

            outputCy := (resultat bitShift: -1) bitAnd: 1.

            outputBw := (resultat bitAnd: 1)! !

 

La variable temporaire K n’est pas vraiment nécessaire, mais elle permet d’isoler, pour la lecture, la valeur calculée en combinant les trois entrées. Ensuite, comme k varie de 0 a 7, on ajoute 1 pour accéder à l’élément du tableau dont les index varient de 1 a 8.

 

Un dernier protocole, le protocole d’initialisation est implémenté pour décrire comment une instance d’Ual doit être initialisée. La méthode « initialize » place un 0 dans toutes les variables.

Notez que l’ont peut affecter les six variables en une seule expression. Et notez aussi qu’il y a un bug, j’ai oublié d’affecter la variable inputC. Ce n’est pas catastrophique, car, lors de l’utilisation d’une Ual, ou commence toujours par affecter les entrées avant de lancer l’exécution.

 

!Ual methodsFor: 'initialize-release'!

 

initialize

            inputNs :=  inputEw  :=  outputSm  :=  outputCy  :=  outputBw := 0.! !

 

 

Pour créer une Ual, l’expression « Ual new » suffit.

Pour que le message new initialise la nouvelle Ual, on redéfinit la méthode new dans la métaclasse « Ual class » en invoquant la méthode « initialize ».

 

"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!

 

Ual class

            instanceVariableNames: ''!

 

 

!Ual class methodsFor: 'instance creation'!

 

new

            ^super new initialize! !

 

C’est cool. Je n’ai rien compris.

 

 

 

(à suivre…)

 

Sommaire de la suite :

La classe Gapp (Qui représente le Microprocesseur)

La classe Pe (Qui représente les Processeurs élémentaires)

La classe Ram (Qui représentera la mémoire locale des Pe).

La classe Instruction (qui permettra de programmer nos Microprocesseurs)

L’interface et la classe GappSim, pour visualiser et interagir avec notre simulation.