Portail informatique Année 2018 – 2019

PAAM – Programmation avancée des architectures multicoeurs

Ce Cours/TP a pour objectif de vous faire comprendre comment est conçu un processeur à partir de transistors. Il est probable que de terminer ce TP vous demande environ 2 à 3h de travail en hors-présentiel (exercices 8 et 9). Finir ces exercices n'est pas nécessaire pour la suite du cours, mais réussir à finir est relativement satisfaisant puisque vous aurez réussi à concevoir un processeur 8 bits complet relativement avancé. Dans ce cours, on suppose que nous sommes sur une île déserte et que nous devons reconstruire des ordinateurs à partir de zéro. Heureusement, nous avons un ami physicien sur l'île. Cet ami est capable d'assembler des circuits complexes construits à partir de transistors, l'élément de base de la plupart des circuits électroniques. Pour reconstruire un ordinateur, on considère des transistors de type MOS (Metal-Oxide-Semiconductor) car ce sont ceux aujourd'hui les plus couramment utilisés pour construire les micro-processeurs. Il existe deux types de transistors MOS : les transistors nMOS et les transistors pMOS. Dans les processeurs actuels, on trouve les deux types de transistors, on parle donc de technologie CMOS (Complementary Metal-Oxide-Semiconductor).
Transistors nMOS et cMOS.

Comme présenté sur la figure , un transistor de type MOS possède trois connexions :
  • la source, nommée S,
  • la grille, nommé G.
  • le drain, nommé D,

Un transistor MOS se comporte comme un interrupteur électrique. Pour un nMOS, lorsque la grille est alimentée, la source est connectée au drain, alors que pour un transistor pMOS, c'est l'inverse. Pour illustrer ce fonctionnement, si on imagine une rivière coulant de la source vers le drain, la grille agit comme une sorte de barrage empêchant l'eau de passer. Dans le cas d'un nMOS, lorsque la grille est alimentée, c'est comme si le barrage était ouvert : le courant passe de la source au drain. Dans le cas d'un pMOS, c'est l'inverse : lorsque la grille est alimentée, le barrage est fermé et le courant ne peut pas passer. Du transistor à la porte NAND Les transistors permettent de construire des portes logiques qui sont à la base de l'électronique. À cette question, nous étudions notre première porte logique : la porte NAND (pour Not AND). L'avantage de la porte NAND, c'est que c'est une porte logique universelle : elle permet de construire des portes logiques NOT, OR, AND ou XOR comme vous le verrez dans l'exercice suivant.
     
Porte NAND
ABOut
001
011
101
110
.aLa porte logique NAND (credit wikipedia). .bTable de vérité de la porte logique NAND.

La construction de la porte logique NAND à partir de portes nMOS et pMOS est donnée en figure .a, alors que la table de vérité de cette porte est donnée en figure .b. Dans la figure .a, Vdd est le courant haut qui correspond, par convention, à la valeur vrai. Vss est le courant bas qui correspond, par convention, à la valeur faux. Les deux transistors du haut (avec un petit rond sur la grille) sont des pMOS qui empêchent le courant de passer entre la source et le drain lorsque la grille est alimentée, alors que les deux transistors du bas (sans petit rond sur la grille) sont des nMOS qui laissent donc le courant passer entre la source et le drain lorsque la grille est alimentée.

Vérifiez que le circuit présenté en figure .a met bien en œuvre une porte NAND.
Lorsque A vaut 0 (faux), le transistor du bas de la figure associé à A empêche le courant de passer entre la source et le drain, donc Out n'est pas connecté à Vss (0). Ensuite, dans cette configuration, le transistor du haut associé à A laisse le courant passer entre Vdd (1) et Out : Out vaut donc 1. Exactement pour la même raison, si B vaut 0, Out vaut 1.

Enfin, lorsque A et B valent 1, les deux transistors du haut de la figure empêchent le courant de passer, ce qui déconnecte Out de Vdd (1). Dans ce cas, les deux transistors du bas de la figure laissent passer le courant de Vss (0) à Out : Out vaut donc 0.
Prise en main du simulateur de circuit Maintenant que vous avons notre première porte logique, nous pouvons l'utiliser pour construire les autres portes logiques. Avant de nous lancer dans cette mise en œuvre, nous prenons en main l'environnement de développement que nous utiliserons pour créer des circuits.

Dans un navigateur Web, connectez-vous au site https://circuitverse.org/simulator. Ce site permet de construire des circuits logiques de façon conviviale. De façon à pouvoir enregistrer vos travaux, il faut créer un compte. Pour créer un compte, cliquez sur Sign In. Ensuite, cliquez sur New user? Sign up. À partir de ce point, vous pouvez soit créer un nouveau compte, soit, si vous en avez un, utiliser votre compte google ou votre compte facebook pour vous authentifier.
Maintenant que vous avez un compte, vous pouvez entrer dans le simulator. Cliquez sur le menu Simulator qui se trouve en haut de la fenêtre. Vous pouvez commencer à configurer votre projet. Donnez lui un nom, par exemple CSC4536, en modifiant la partie Project Properties se trouvant en bas à gauche de la fenêtre. Avant d'aller plus loin, nous allons placer un premier élément dans notre circuit. Allez dans l'onglet Input se trouvant à gauche de la fenêtre et sélectionnez la première icône (lorsque vous passez dessus avec la souris, vous devriez voir Input). Déplacez votre souris sur l'espace de travail (la grille se trouvant à droite de la fenêtre), et cliquez quelque part. Félicitations, vous avez construit une entrée binaire ! Vous devriez constatez que, lorsque vous cliquez sur votre entrée binaire, vous pouvez la faire passer de 0 à 1 et inversement. Avant d'aller plus loin, nous allons vérifier que nous pouvons bien sauver notre travail. Dans le menu Project, cliquez sur Save Online, ce qui va créer une sauvegarde de votre projet hébergée sur le serveur (vous pouvez aussi enregistrer votre travail hors-ligne avec Save Offline). Pour vérifier que tout s'est bien passé, allez dans la configuration de votre compte représenté par votre nom d'utilisateur en haut à droite de la fenêtre, et cliquez sur My circuits. Vérifiez que vous retrouver bien votre projet et cliquez sur Launch pour relancer le simulateur. Avant de se lancer dans des conceptions plus intéressantes, nous allons juste apprendre à créer un circuit simple constitué d'une unique porte logique NAND. Vous trouverez cet élément dans l'onglet Gates à gauche de la fenêtre. Dans un premier temps, ajoutez une seconde entrée et une porte logique NAND. Vous devriez remarquer que, lorsque vous passez sur un élément dans l'onglet Gates, son nom s'affiche. Vous devriez aussi remarquer que, lorsque vous cliquez sur un élément placé dans l'espace de travail, vous pouvez le déplacer. Pour déplacer un élément, il faut cliquer à l'intérieur de l'élément et non sur les petits points verts se trouvant sur le bord et représentant les points de connexions. Nous allons maintenant apprendre à connecter les deux entrées à la porte logique. Pour connecter deux éléments, il faut créer un fil électrique entre ces éléments. Les points de connexions sont représentés par les points verts se trouvant sur le bord d'un élément. Lorsque vous cliquez sur un point de connexion et que vous déplacez votre souris, vous commencez à créer un fil. Connectez les entrées à la porte NAND. Pour finir, nous allons créer une sortie permettant de visualiser le résultat du calcul effectué par notre porte NAND. Dans l'onglet Output à gauche de l'écran, sélectionnez le premier élément (nommé Output). Cet élément, que nous appellerons une sortie binaire, permet d'afficher l'état d'un fil électrique (0 ou 1). Ajoutez une sortie binaire et connectez la sortie de la porte NAND à cette sortie. Vous devriez obtenir le circuit présenté en figure . Vérifiez, en modifiant les entrées, que votre circuit respecte bien la table de vérité de la porte NAND. Vous devriez remarquer qu'un fil alimenté (valeur 1) apparaît en vert clair alors qu'un fil non alimenté (valeur 0) apparaît en vert foncé.
Un circuit NAND.
Pour continuer cette prise en main de l'outil, nous allons apprendre à mettre un circuit en court circuit, c'est-à-dire que nous allons à la fois mettre les valeurs 0 et 1 sur un fil. Il ne faut bien sûr jamais mettre un circuit en court-circuit, cette question n'a que pour but de vous montrer ce qui se passe, ce qui va vous permettre d'identifier les erreurs dans vos circuits.

Pour réaliser ce court circuit, cliquer quelque part sur le fil reliant une des entrées à la porte NAND, ce qui permet de démarrer un second fil à partir de ce point. Construisiez un fil qui contourne entièrement la porte (par le bas si vous partez du bas ou par le haut sinon). Pour cela, il faut construire plusieurs fils. Vous devriez obtenir le résultat présenté en figure . Vous devriez voir des messages s'afficher en bas de la fenêtre vous indiquant qu'il y a un problème. Vous devriez aussi voir que le fil reliant la porte à la sortie est à moitié vert foncé et à moitié vert clair, et que le simulator vous a ajouté de petits rond aux points de connexions qui permettent de remonter à l'origine du problème (sur le haut de la figure dans le cas de la figure ).
Un circuit NAND en court-circuit.
Nous allons réparer notre circuit en détruisant le court-circuit. Pour cela, cliquez sur un fil et appuyez sur la touche backspace (retour arrière se trouvant en haut à droite de votre clavier), ce qui détruit un élément. Supprimez tous les fils réalisant le court-circuit. Pour finir notre prise en main, nous apprenons à déplacer plusieurs éléments. Pour cela, il suffit d'effectuer une sélection en appuyant sur la touche Shift (remarquez que si vous n'appuyez pas sur la touche Shift alors que vous cliquez sur une zone sans éléments avant de déplacer la souris, vous déplacez simplement la grille). Ensuite, vous pouvez, en cliquant sur l'un des éléments sélectionnés , déplacer l'ensemble de ces éléments. De la porte NAND aux autres portes logiques Le but de cet exercice est de construire les portes logiques manquantes à partir de la porte logique NAND. Les tables de vérités et les icônes associées aux portes que vous devez construire sont présentées en figure .
Porte NOT
aOut
01
10
Porte AND
abOut
000
010
100
111
Porte OR
abOut
000
011
101
111
Porte NOR
abOut
001
010
100
110
Porte XOR
abOut
000
011
101
110
Porte XNOR
abOut
001
010
100
111
Tables de vérités des principales portes.

Commencez par nettoyer votre espace de travail en supprimant le circuit créé à l'exercice précédent. Pour cela, sélectionnez tout votre circuit et utilisez backspace pour le supprimer (vous pouvez aussi aller dans le menu Project->Clear Project).

Ensuite, en considérant que vous n'avez que la porte logique NAND, mettez en œuvre les autres portes. Au fur et à mesure que vous construisez de nouvelles portes, vous pouvez bien sûr les utiliser pour construire les suivantes.

Nous vous conseillons de construire les portes dans cet ordre :
  • NOT Remarquez que NAND(A,A) = NOT(A).
  • AND Remarquez que NOT(NAND(A,B)) = AND(A,B).
  • OR Remarquez que OR(A,B) = NOT(AND(NOT(A),NOT(B))) = NAND(NOT(A),NOT(B)).
  • NOR Remarquez que NOR(A,B) = NOT(OR(A,B)).
  • XOR Remarquez que XOR(A,B) se comporte un peu comme OR(A,B), sauf quand A et B valent 1 : dans ce cas, on veut avoir 0 comme sortie. On peut donc facilement voir que XOR(A,B) = AND(A et B différent de 1, OR(A,B)). Ensuite une porte logique mettant en œuvre "A et B différent de 1 donne 1" est simplement la porte NAND. Donc, XOR(A,B) = AND(NAND(A,B),OR(A,B)).
  • XNOR Remarquez que XNOR(A,B) = NOT(XOR(A,B)).
Félicitations, vous avez maintenant construit les éléments de base permettant de concevoir des ordinateurs à partir de transistors !
À cette question, nous réalisons un circuit permettant d'additionner deux nombres encodés sur 8 bits. L'additionneur 1 bit Comme première étape, nous allons réaliser un circuit permettant d'additionner deux bits (un bit est une valeur binaire, pouvant donc prendre les valeurs 0 ou 1). Exactement comme on le fait en décimal, il peut il y avoir une retenue (carry en anglais), donc votre circuit doit produire un résultats sur deux bits :
  • 0 + 0 donne 0 et je retiens 0,
  • 0 + 1 donne 1 et je retiens 0,
  • 1 + 0 donne 1 et je retiens 0,
  • 1 + 1 donne 0 et je retiens 1 (comme 5 + 5 donne 0 et je retiens 1 en décimal),
abscout
0000
0110
1010
1101
Table de vérité d'un demi-additionneur.

La figure vous donne la table de vérité de l'additionneur. Les entrées a et b sont les nombres à additionner alors que s est la somme et cout la retenue.

Commencer par supprimez tous vos anciens circuits. À partir de cette étape, ne détruisez plus jamais aucun des circuits que nous aurons réalisés : il vont tous nous resservir à un moment où un autre.

Ensuite, réalisez le circuit permettant d'effectuer une addition binaire.

Vous pouvez remarquer que :
  • la somme sum réalise exactement un XOR entre a et b,
  • on a retenue si et seulement si a et b valent 1.
Voir la solution donnée à la question suivante.
De façon à additionner plusieurs bits, nous aurons besoin de réaliser plusieurs fois le circuit d'addition d'un unique bit. Notre logiciel de simulation permet de réutiliser des circuits qui ont déjà été mise en œuvre.

Pour rendre votre circuit réutilisable, commencez par donner un nom à vos entrées et à vos sorties. Pour cela, sélectionnez une des entrées. Dans la partie gauche de la fenêtre, vous devriez voir apparaître une zone Properties. À la ligne Label, donnez un nom à vos deux entrées (a et b). De la même façon, donnez des noms à vos deux sorties (s pour la somme et cout pour la retenue).

Ensuite, vous pouvez donner un nom à votre circuit. Pour cela, cliquez n'importe où sur la grille en dehors de tout élément. Ensuite, dans Project Properties, remplacez le nom actuel du circuit (ligne Circuit indiquant Main) en half-add. Le nom que vous voyez au dessus de la grille devrait changer de façon adéquate. Enfin, cliquez sur Edit layout en bas à gauche de la fenêtre, ce qui vous permet de modifier l'apparence de votre circuit quand vous le réutilisez. Nous vous conseillons de laisser les entrées à gauche et les sorties à droite. Nous vous conseillons aussi de mettre a en haut et b en dessous, et s en haut et cout en haut bas. Enfin, nous vous conseillons de laisser deux carreaux entre les entrées ou entre les sorties. Après avoir donné l'apparence qui vous plaît à votre circuit, appuyez sur Save pour revenir sur le schéma du circuit. Pensez à sauver votre travail (menu Project->Save Online).
Comme on le fait en décimal, quand on effectue un calcul sur plusieurs digits, il faut non seulement additionner les deux digits courants, mais il faut aussi additionner ce résultat avec la retenue de l'étape précédente. Nous réalisons donc ce circuit à cette étape. Créez un nouveau circuit (menu Circuit->New circuit) que vous nommerez 1-add (pour additionneur 1 bit).

Ajoutez 3 entrées nommées a, b et cin (retenue d'entrée provenant de l'étape précédente). Ajoutez aussi 2 sorties nommées s pour la somme et cout pour la retenue de sortie.

Enfin, utilisez des circuits half-add (menu Circuit->Insert SubCircuit) pour que s soit égal à la somme de a, b et cin, et que cout soit égal à la retenue de cette somme. Pour la somme, il suffit d'enchaîner deux half-add. Pour la retenue, il suffit que l'une des deux additions génère une retenue pour que le 1-add génère une retenue. Le cout du circuit 1-add est donc égal à un OU entre les sorties cout des deux half-add
Du bit à l'entier De façon à pouvoir créer un additionneur capable d'effectuer des additions sur des nombres encodés sur 8 bits, nous commençons par créer une entréee 8 bits. Créez un nouveau circuit nommé 8-add. Dans ce circuit, ajoutez une entrée. Si vous sélectionnez cette entrée, vous devriez voir apparaître ses propriétés. Donnez la valeur 8 à la propriété BitWidth de façon à ce que l'entrée soit une entrée avec 8 bits. Donnez le nom a à votre entrée. Si vous n'êtes pas à l'aise avec la représentation des nombres en binaire et en hexadécimal, cliquez sur l'aide.

On commence par étudier les nombres sur 4 bits. Ce sont des nombres écrits en base 2 qui possèdent 4 digits. La table de correspondance de la figure vous présente la correspondance entre les 16 premiers nombres binaires, décimales et hexadécimales. L'hexadécimal est simplement une représentation des nombres en base 16 dans laquelle les nombres décimaux de 10 à 15 sont représentés par les lettres de A à F. Dans la table, les nombres suffixés par b sont des nombres représentés en binaire, alors que les nombres préfixés par 0x sont des nombres représentés en hexadécimal.
 Binaire   Décimal   Hexadécimal 
0000b 0 0x0
0001b 1 0x1
0010b 2 0x2
0011b 3 0x3
0100b 4 0x4
0101b 5 0x5
0110b 6 0x6
0111b 7 0x7
1000b 8 0x8
1001b 9 0x9
1010b 10 0xA
1011b 11 0xB
1100b 12 0xC
1101b 13 0xD
1110b 14 0xE
1111b 15 0xF
Correspondance binaire/décimale/hexadécimale.

On voit qu'on peut représenter un nombre sur 8 bits avec deux digits hexadécimaux. Par exemple, 10010011b est le nombre 1001b * 1000b + 0011 = 0x9 * 0x10 + 0x3 = 0x93.
Le simulateur circuitverse permet d'afficher un nombre 4 bits facilement. Pour utiliser cette fonctionnalité, nous avons donc besoin de séparer notre nombre 8 bits en deux nombres sur 4 bits, l'un avec les 4 bits de poids fort, et l'autre avec les 4 bits de poids faibles. Dans l'onglet Misc, sélectionnez un Splitter (deuxième icône). Quand circuitverse vous demande la taille du nombre, mettez 8 puisque votre nombre fait 8 bits. Ensuite, il faut donnez la décomposition de notre nombre. Nous voulons deux groupes de 4 bits, donc saisissez 4 4, ce qui construit 2 sorties de 4 bits. Connectez le Splitter à a. Ensuite, dans l'onglet Output, sélectionnez HexDisplay (5ième icône) et placez deux HexDisplay à côté de votre Splitter. Connectez les 4 premiers bits du Splitter au second HexDisplay et les 4 derniers bits au premier HexDisplay. Vous devriez obtenir le circuit présenté en figure .
Décomposition et affichage d'un nombre 8 bits.
L'additionneur 8 bits Vous pouvez maintenant créer l'additionneur 8 bits. Pour cela, vous avez besoin des entrées suivantes :
  • a sur 8 bits que vous avez déjà créé à la question précédente,
  • b sur 8 bits que vous devez ajouter. Pensez à ajouter des HexDisplay et un Splitter pour voir la valeur de b,
  • cin sur 1 bit que vous devez ajouter. Cette entrée donne la retenue initiale, qui est bien sûr égale à 0 pour une addition, mais vous verrez dans les questions suivantes comment exploiter cette entrée pour faire des soustractions.

Ensuite, utilisez des Splitter 8 vers 1 1 1 1 1 1 1 1 pour retrouver les bits qui composent a et b. Ajoutez aussi 8 circuits 1-add et connectez les de façon adéquate pour créer un additionneur 8 bits.

Pour construire la sortie, ajoutez un Splitter 8 vers 1 1 1 1 1 1 1 1 et changez sa direction vers LEFT dans ses propriétés. Connectez les sorties des additionneurs 1 bits au Splitter, et connectez de l'autre côté du Splitter une sortie nommée s dans laquelle vous spécifierez que BitWidth est égal à 8. Ajoutez aussi des HexDisplay et un Splitter 8 vers 4 4 pour voir le résultat. Enfin, ajoutez une sortie cout à votre circuit.

Si gérer autant de fils vous semble trop difficile, n'hésitez pas à créer d'abord un circuit 4-add pour additionner des entiers 4 bits et à utiliser ce circuit pour construire le 8-add.

Pour les retenus, il faut les connecter en cascade. Le cin du premier 1-add reçoit la retenue d'entrée du circuit, et chaque cin suivant reçoit le cout de l'étape précédente.
Félicitations, vous venez de créer votre premier composant électronique complexe !
Dans un circuit, il arrive souvent d'avoir besoin de sélectionner une entrée parmi N en fonction d'une valeur. Ce mécanisme peut par exemple être utilisé pour sélectionner les registres qui participent à une instruction. Dans cet exercice, nous mettons un œuvre des multiplexeurs dont le rôle est justement de sélectionner une entrée parmi N. On commence par construire un multiplexeur permettant de sélectionner une entrée parmi deux et dans lequel chaque entrée est un bit. Créez un nouveau circuit nommé 2x1-mux. Ce circuit doit posséder deux entrées binaires nommées x0 et x1, et un sélecteur nommé sel. Le circuit doit aussi posséder une sortie binaire nommée out. Quand sel vaut 0, le circuit doit renvoyer x0 sur out, et quand sel vaut 1, le circuit doit renvoyer x1 sur out.

Le circuit renvoie 1 si et seulement si :
  • x0 vaut vrai et sel vaut 0,
  • ou x1 vaut vrai et sel vaut 1.
La formule logique du circuit est OR(AND(x0, NOT(sel)), AND(x1, sel)).
Sur le modèle de l'additionneur 8 bits, construisez un nouveau circuit nommé 2x8-mux prenant en entrée 2 valeurs sur 8 bits nommées x0 et x1, et un sélecteur sur un bit. Le circuit doit renvoyer x0 si sel vaut 0 et x1 sinon.
Sur le modèle de 2x8-mux, construisez un nouveau circuit nommé 2x3-mux prenant en entrée 2 valeurs sur 3 bits nommées x0 et x1, et un sélecteur sur un bit. Le circuit doit renvoyer x0 si sel vaut 0 et x1 sinon. Travailler sur des entiers encodés sur 3 bits peut vous sembler inattendu. Il faut comprendre qu'avec 3 bits, on peut encoder 23 = 8 valeurs. Comme à terme, notre processeur possédera 8 registres, nous aurons donc besoin de manipuler des numéros de registre encodés sur 3 bits. Le 2x3-mux permettra donc de sélectionner des numéros de registres.
À partir de 2x8-mux, construisez un circuit 8x8-mux permettant de sélectionner une entrée parmi 8, chacune des entrées faisant 8 bits.

De façon à pouvoir sélectionner une entrée parmi 8 = 23, le sélectionneur doit faire 3 bits.

Il faut utiliser un total de 7 2x8-mux. Les 4 premiers sont connectés directement aux entrées et le premier bit du sélecteur sert à sélectionner une des deux valeurs. Les deux suivants sont connectés aux quatre premiers 2x8-mux et le second bit du sélecteur permet de sélectionner une des deux valeurs. Le dernier est connectés aux deux 2x8-mux précédent et le dernier bit du sélecteur permet de sélectionner une des deux valeurs.
Dans cet exercice, nous concevons un circuit nommé ALU permettant de réaliser n'importe quel calcul arithmétique et logique. Vu le temps imparti, nous ne pourrons pas être exhaustifs, mais notre ALU va être capable d'effectuer des additions, des soustractions, des négations, les opérations logiques AND, OR et NOT, et des comparaisons. L'addition Commencez par créer un circuit nommé ALU. Ce circuit doit prendre en entrée :
  • Un entier a encodé sur 8 bits,
  • Un entier b encodé sur 8 bits,
  • Un entier func encodé sur 16 bits qui indiquera l'opération à effectuer avec a et b.

En sortie, ALU doit renvoyer uniquement le résultat de l'opération, c'est-à-dire un entier out encodé sur 8 bits.

Une ALU complète renverrai aussi un drapeau indiquant comment s'est déroulée l'opération, par exemple, est-ce qu'il y a eu un dépassement de capacité ou une division par zéro.
L'entrée func doit être analysée de la façon suivante :
  • Bits 0 à 5 (32 possibilités) : opération de base à effectuer. Dans notre cas, il s'agit des opérations d'addition (0), de ET binaire (1) ou de comparaison (2). On prévoit plus de bits que nécessaire pour pouvoir étendre notre ALU dans le futur.
  • Bits 6, 7 et 8 : indique si il faut inverser les bits de a, b et out.
  • Bit 9 : indique si il faut injecter artificiellement une retenue pour l'addition,
  • Bits 10 et 11 : indique l'opération de comparaison de base à effectuer (égal pour le bit 10, ou inférieur strictement pour le bit 11).
  • Bit 12 : indique si il faut inverser le résultat de l'opération de comparaison effectuée.
  • Bits 13 à 15 : réservés pour un usage futur.

De façon à décomposer func, ajoutez un Splitter 16 vers 3 3 suivi de dix 1. Les 6 premiers bits vont être utilisés pour sélectionner l'opération effectuée en se servant de circuits 8x8-mux. Il faut donc décomposer ces 6 bits en 2 fois 3 bits puisque le sélecteur d'un 8x8-mux est encodé sur 3 bits. Comme nous n'avons que 3 opérations, nous nous servirons des 3 premiers bits, mais nous allons ignorer les 3 bits suivants réservés pour de futurs usages. Les 10 bits qui suivent (6 à 16) vont permettre de gérer les autres comportements du circuit ALU.
Ajoutez un 8-add et connectez y directement les entrées a et b et la sortie out. Ajoutez des HexDisplay avec des Splitter 8 vers 4 4 pour afficher les valeurs de a, b et out. Connectez aussi le bit 9 de func au cin de 8-add. Vérifiez que, quand ce bit 9 est à 0, votre circuit effectue bien une addition. La suite de l'exercice qui permet de compléter le circuit ALU est optionnel et prend environ 90mn (en plus des 2/3h de travail personnel annoncé en début d'exercice). Appuyez sur détails si vous voulez voir les questions.
Soustraction Supprimez la connexion entre le a d'entrée du circuit et celui du 8-add. Ensuite, ajoutez une porte NOT dans laquelle vous spécifierez qu'elle fait 8 bits (BitWidth). Connectez a à cette porte, puis ajouter un 2x8-mux pour sélectionner a ou NOT(a). Enfin, connectez la sortie du 2x8-mux au a du 8-add. Recommencez l'opération pour b et out. Connectez de façon adéquate les bits 6, 7 et 8 de func aux entrées sel des trois 2x8-mux. En activant les bits 6 et 8 de func, vérifiez que votre circuit effectue des soustractions. Expliquez ce phénomène. L'opération a - b revient à calculer a + (- b). Ensuite, on peut voir que - b consiste simplement en inverser les bits de b et ajouter 1. En effet, b + NOT(b) est égal à 11111111 par construction. Donc, b + NOT(b) + 1 = 0, ce qui revient à dire que -b = NOT(b) + 1. Opérations logiques Ajoutez un circuit 8x8-mux entre la sortie du 8-add et l'inverseur de bits. Connectez les bits 0 à 2 de func à ce multiplexeur. Maintenant, lorsque vous choisissez une autre valeur que 000 pour les trois premiers bits, vous devriez voir que votre circuit ne vous renvoie plus de résultat valide. Ajoutez une porte AND de taille 8 bits sous le 8-add. Connectez le 2x8-mux associé à l'entrée a et le 2x8-mux associé à l'entrée b à cette porte AND. Enfin, connectez la sortie de la porte AND à l'entrée x1 du 8x8-mux permettant de sélectionner le calcul effectué. Vérifiez que, quand vous mettez tous les bits de func à 0 sauf le bit 0, ALU effectue bien un AND entre a et b. Activez aussi les bits 6, 7 et 8 de func. Pour quelle raison ALU effectue maintenant un OR entre a et b ? Car OR(a, b) = NOT(AND(NOT(a), NOT(b))). Comparaison Nous allons maintenant créer le circuit qui permet de savoir si a est strictement plus petit que b. Techniquement, nous pouvons voir que a < b si et seulement si a - b < 0. Ensuite, une fois qu'on a compris pourquoi -b = NOT(b) + 1, on peut facilement en déduire que a - b < 0 si et seulement le bit 8 de a - b est égal à 1. Donc, on peut savoir si a < b en observant le bit 8 du calcul de a - b. Modifiez func de façon à effectuer une soustraction en activant les bits 7 et 9. Ensuite, ajoutez un Splitter 8 vers 1 1 1 1 1 1 1 1 à la sortie du 8-add (nous vous conseillons de placer ce Splitter assez loin du reste du circuit pour avoir un peu de place. Enfin, ajoutez une porte AND qui répond vrai si le bit 8 de la sortie du 8-add et le bit 11 de func sont à 1. Vous devriez constater que si vous activez les bits 7, 9 et 11 dans func et que si vous choisissez a < b, votre porte vous renvoie vrai. Nous mettons maintenant en place le circuit permettant de savoir si a est égal à b. Lorsque c'est le cas, a - b est égal à 0, ce qui signifie que chaque bit de a - b vaut 0. On peut donc ajouter un NOR à la sortie du Splitter que vous avez utilisez pour isoler le bit 8 de la soustraction, et y connecter les 8 bits (notez que vous pouvez demander à avoir 8 entrées à votre porte NOR en modifiant Input Size). Si a == b, comme tous les bits de a - b sont à zéro, le OR de ces bits renvoie 0, donc le NOR renvoie vrai, ce qui est exactement le résultat qu'on cherche. Ajoutez une porte AND derrière le NOR et connectez y la sortie du NOR et le bit 10 de func. Vous devriez pouvoir vérifier que si a est égal à b et que si vous activez les bits 7, 9 et 10 dans func, votre porte AND renvoie vrai. Ajoutez une porte OR qui renvoie vrai si a < b et le bit 11 de func est actif, ou si a == b et le bit 10 de func est actif. On peut, en activant simultanément les bits 7, 9, 10 et 11 de func savoir si a est inférieur ou égal à b. À cette étape, on peut donc savoir si a < b, a == b et a <= b. Maintenant, nous souhaitons pouvoir inverser le résultat de cette comparaison pour pouvoir traiter les cas où on souhaite savoir si a > b (bits 7, 9, 10, 11 et 12), a >= b (bits 7, 9, 11 et 12) et a != b (bits 7, 9, 10 et 12). Ajoutez un 2x1-mux permettant de sélectionner soit le résultat de la comparaison que vous avez déjà effectuée (la sortie du OR), soit l'inverse de ce résultat. Connectez le bits 12 de func à ce 2x1-mux. Vous devriez pouvoir maintenant effectuer toutes les comparaisons possibles. De façon à envoyer le résultat des comparaisons dans la sortie du circuit, il faut passer de 1 à 8 bits. Pour cela, ajoutez un Splitter 8 vers 1 7. Ensuite, ajoutez une entrée constante (onglet Input, entrée ConstantVal se trouvant en 5ième position). Double cliquez sur cette entrée pour mettre 0000000 (7 zéros), ce qui construit la constante 0 sur 7 bits. Connectez de façon adéquate la sortie du 2x1-mux et cette constante au Splitter afin de construire une valeur sur 8 bits, puis connectez cette valeur à l'entrée x2 du 8x8-mux permettant de sélectionner l'opération effectuée. Vérifiez que si vous activez les bits 1, 7 et 9, et que vous jouez avec les bits 10, 11 et 12, votre circuit est maintenant capable d'effectuer toutes les comparaisons. Pour aller plus loin Si vous voulez aller encore plus loin, vous pouvez ajouter un circuit permettant d'effectuer des décalages de bits en utilisant un Barrel-Shifter (voir l'explication sur la page de la Wikipedia) ou un circuit permettant d'effectuer un XOR entre a et b. Pour construire notre Barrel-Shifter, on s'intéresse d'abord au décalage à droite. On a techniquement 3 cas à traiter pour créer un jeu d'instruction assez large :
  • décalage binaire : on met des zéros à gauche à chaque fois qu'on décale d'un bit sur la droite,
  • décalage arithmétique : on réplique le bit de signe (celui de gauche) à chaque fois qu'on décale d'un bit sur la droite,
  • rotation : on fait tourner les bits, c'est-à-dire que le bit de droite passe à gauche.

Pour construire les circuits, on peut voir qu'il suffit de s'intéresser à un décalage de 7 bits maximum. Dans le cas d'un décalage de 8 bits ou plus :
  • Décalage binaire : on obtient forcément 0. Il suffit de traiter ce cas séparément.
  • Décalage arithmétique : (i) si on a un nombre positif, en décalant de 7 bits, on obtient 0, et si on continue à décaler, la valeur reste 0, (ii) si on a un nombre négatif, en décalant de 7 bits, on obtient 11111111, et si on continue à décaler, la valeur reste la même. On peut donc s'arrêter à 7.
  • Rotation : à chaque fois qu'on décale de 8 bits, on obtient le nombre d'origine. On peut donc uniquement considérer les 3 premiers bits du nombre donnant la nombre du décalages à effectuer.

Une fois ces observations faites, on s'intéresse donc à décaler de 0 à 7 bits et on traite les autres cas de façon spéciale. L'idée du Barrel-Shifter est de décomposer un décalage de n bits en un ensemble de décalage de k, où k est une puissance de 2. Par exemple décaler de 7 revient à décaler de 4, puis de 2, puis de 1. Ensuite, on peut écrire des circuits qui effectuent des décalages de 4, 2 et 1 bits (les circuits 8x4-shr, 8x2-shr et 8x1-shr). Ensuite, il suffit de composer ces décalages pour obtenir le circuit 8-shr. Dans tous ces circuits, si ar? vaut 1, il s'agit d'un décalage arithmétique, si rot? vaut 1, il s'agit d'une rotation, et si ar? et rot? valent 0, il s'agit d'un décalage logique.

Pour construire un décalage à gauche, il suffit de s'apercevoir qu'il suffit d'inverser les bits, d'effectuer le décalage à droite et de remettre les bits dans le bon sens. Pour cela, il suffit d'écrire un circuit reverse qui inverse le sens des bits (voir figures).
.a 8x4-shr – décalage de 4 bits vers la droite. .b 8x2-shr – décalage de 2 bits vers la droite. .c 8x1-shr – décalage de 1 bits vers la droite. .d 8-shr – décalage de n bits vers la droite. .e reverse – inversion des bits.
Et voici le résultat avec ALU et tous les circuits (sauf le décalage de bits). Dans l'état présenté, l'unité de calcul renvoie vrai si et seulement si a est strictement supérieur à b (bits 1, 7, 9, 10, 11, 12 activés).
Maintenant que nous sommes capables d'effectuer des calculs, il faut pouvoir stocker dans le processeur des variables internes. Pour cela, nous construisons des registres. Un registre est un circuit possédant principalement une entrée nommée in et une sortie nommée out. Selon son état, le registre est capable de copier l'entrée in sur la sortie out ou de mémoriser la sortie out indépendamment de l'état de l'entrée. Ce phénomène de mémorisation permet au registre de conserver un état dans le temps, et donc d'agir comme une mémoire. Le verrou SR NAND Dans un nouveau circuit nommé SR-nand, reproduisez le circuit constitué de deux portes NAND présenté en figure . Ce circuit s'appelle un verrou SR (pour Set et Reset) NAND (SR NAND latch en anglais).
Le verrou SR NAND.
Dans quels états sont Q et Q' quand S vaut 0 et R vaut 1 ? Et quand S vaut 1 et R vaut 0 ? On suppose que S = 0 et R = 1. Comme S = 0, Q = 1 puisque NAND(0, *) = 1. Comme Q = 1 et R = 1, Q' = 0.

On suppose que S = 1 et R = 0. Comme R = 0, Q' = 1 puisque NAND(*, 0) = 1. Comme Q' = 1 et S = 1, Q = 0.
On suppose que, initialement, S = 0 et R = 1. Dans quel état est le circuit si S passe à 1 ? Donnez exhaustivement l'ensemble des valeurs Q et Q' possibles en étudiant les différentes propagations possibles de l'électricité. Pour cela, étudiez le cas où la porte NAND du haut est la première à s'activer et le cas où la porte NAND du bas est la première à s'activer. Initialement, S = 0 et R = 1, donc Q = 1 et Q' = 0 (voir question précédente). On suppose que S passe à 1.

Si le NAND du haut s'active en premier, S = 1 et Q' = 0 laissent Q à 1. Comme Q = 1 et R = 1, Q' reste à 0. Le circuit est donc stabilisé sur la sortie Q = 1 et Q' = 0.

Si le NAND du bas s'active en premier, R = 1 et Q = 1 laissent Q' à 0. Comme Q' = 0 et S = 1, Q reste à 1. Le circuit est donc stabilisé sur la sortie Q = 1 et Q' = 0.

Au résultat, quel que soit la porte activée en premier ou la vitesse de propagation de l'électricité dans les différents circuits, après le passage de S à 1, Q reste à 1 et Q' reste à 0.
On suppose que, initialement, S = 1 et R = 0. Dans quel état est le circuit si R passe à 1 ? De la même façon étudiez les différentes possibilités de propagation de l'électricité. Initialement, S = 1 et R = 0, donc Q = 0 et Q' = 1 (voir question précédente). On suppose que R passe à 1.

Si le NAND du haut s'active en premier, S = 1 et Q' = 1 laissent Q à 0. Comme Q = 0 et R = 1, Q' reste à 1. Le circuit est donc stabilisé sur la sortie Q = 0 et Q' = 1.

Si le NAND du bas s'active en premier, R = 1 et Q = 0 laissent Q' à 1. Comme Q' = 1 et S = 1, Q reste à 0. Le circuit est donc stabilisé sur la sortie Q = 0 et Q' = 1.

Au résultat, quel que soit la porte activée en premier ou la vitesse de propagation de l'électricité dans les différents circuits, après le passage de S à 1, Q reste à 0 et Q' reste à 1.
Le bascule JK Comme vous avez du le constater, quand S = 1 et R = 1, on peut avoir Q = 1 et Q' = 0 si on vient de l'état S = 0 et R = 1, ou Q = 0 et Q' = 1 si on vient de l'état S = 1 et R = 0. Cette propriété est très forte car elle signifie qu'on peut "bloquer" la sortie du circuit à 1/0 ou 0/1 quand on passe S et R à 1. Pour exploiter cette propriété, on va un petit peu améliorer notre circuit. Dans un nouveau circuit nommé JK-flip-flop, reproduisez le circuit présenté en figure . Ce circuit s'appelle une bascule JK (JK flip-flop en anglais). Le nom JK semble venir du nom du concepteur de ce circuit (Jack Kirby).
La bascule JK (Circuit JK-flip-flop)
Mettez l'entrée E (Enable) à 1. Dans quels états sont Q et Q' si J = 1 et K = 0 ? Dans quels états sont Q et Q' si J = 0 et K = 1 ? Quand E = 1 et J = 1, l'entrée S de SR-nand est à 0. Quand E = 1 et K = 0, l'entrée R de SR-nand est à 1. Donc (voir questions précédentes), Q = 1 et Q' = 0. On remarque que la valeur de J est "recopiée" dans Q.

Quand E = 1 et J = 0, l'entrée S de SR-nand est à 1. Quand E = 1 et K = 0, l'entrée R de SR-nand est à 0. Donc (voir questions précédentes), Q = 0 et Q' = 1. On remarque que la valeur de J est aussi "recopiée" dans Q dans ce cas.
Mettez E = 1, J = 0 et K = 1. Passez E à 0. Que se passe-t-il si vous changez les valeurs de J ou de K ? Expliquez. De la même façon mettez E = 1, J = 1, K = 0. Passez E à 0. Que se passe-t-il si vous changez les valeurs de J ou de K ? Expliquez. Quand E vaut 0, les deux portes NAND envoient 1, ce qui bloque la sortie de SR-nand dans un son état courant. Si on part de J = 0 et K = 1, Q reste à 0. Si on part de J = 1 et K = 0, Q reste à 1. Le registre 1 bit Comme vous l'avez sûrement déjà compris, si K est l'opposé de J, alors, quand E vaut 1, l'entrée J est recopiée dans Q. E permet aussi de bloquer le circuit : quand E passe à 0, l'état de Q ne change plus, même quand J ou K change. En d'autres termes, l'état du JK-flip-flop "mémorise" l'ancienne valeur de J. On va exploiter cette propriété pour construire une première version du registre 1 bit. Créez un nouveau circuit que vous nommerez 1-reg. Ajoutez deux entrées à ce circuit :
  • in (1 bit) est l'entrée du registre,
  • clock (1 bit) permet de bloquer le circuit (l'explication du nom clock viendra plus tard).

Ajoutez aussi une sortie un bit nommée out. Enfin, créez un circuit qui (i) copie in dans out quand clock vaut 1 et (ii) laisse out inchangé quand clock vaut 0, même si in change.

Il suffit de connecter in à J, l'inverse de in à K et clock à E.
Ajoutez une entrée 1 bit nommée reinit à votre registre. Lorsque reinit vaut 0, le circuit doit fonctionner normalement (c'est-à-dire qu'il copie in dans out quand clock vaut 1 et conserve sa valeur quand clock vaut 0). En revanche, votre circuit doit propager la valeur de in vers out quand reinit vaut 1, et ceux, indépendamment de la valeur de clock.

Il suffit d'effectuer un OR entre clock et reinit et de connecter la sortie de ce OR au E du JK-flip-flop. En construisant le circuit, vous devriez vous rendre compte que reinit et clock ont des rôles symétriques, c'est-à-dire qu'on pourrait inverser les rôles de reinit et clock sans changer le comportement du circuit. C'est normal. Même si les deux entrées ont des rôles symétriques, elles ne serviront pas à la même chose dans la suite du TP : reinit va servir à réinitialiser le registre à son état initial alors que clock va servir à propager la valeur de in dans out.
Le registre 8 bits Sur le modèle de 8-add, construisez un circuit nommé 8-reg permettant de mémoriser une valeur sur 8 bits. Ce circuit doit avoir les entrées suivantes :
  • in sur 8 bits est l'état à l'entrée du registre,
  • reinit sur 1 bit permet de réinitialiser le registre,
  • clock sur 1 bit permet de propager la valeur de in vers out ou de mémoriser l'état courant.

Votre circuit doit avoir une unique sortie, out, sur 8 bits et donnant l'état actuel du registre.
Ajoutez à votre circuit une entrée v sur 8 bits. Cette entrée donne la valeur initiale du registre. Lorsque reinit vaut 1, cette valeur doit être directement propagée dans la registre et sur out. Lorsque reinit vaut 0, l'entrée in doit être propagée dans le registre et sur out uniquement quand clock vaut 1.

Pensez à utiliser un 2x8-mux pour sélectionner in ou v en fonction de reinit.
Ajoutez à votre circuit une entrée WE (Write Enable) qui, lorsqu'elle vaut faux, recopie out dans le registre quand clock passe à 1 au lieu de recopier in. Pour cela, ajoutez un 2x8-mux permettant de sélectionner soit in, soit out, et connectez la sortie de ce 2x8-mux dans le x0 de 2x8-mux qui permet actuellement de sélectionner in ou v. Avant d'aller plus loin, vérifiez que le comportement de votre registre est correct. Pour cela, mettez la valeur 10001000 dans v et la valeur 00000011 dans in, puis effectuez les tests suivants :
  • Mettez clock à 0 et WE à 1,
  • Passez reinit à 1 et vérifiez que out vaut v,
  • Passez reinit à 0 et vérifiez que out vaut toujours v,
  • Passez clock à 1 et vérifiez que out prend la valeur de in,
  • Passez WE à 0, puis modifiez la valeur de in. Vérifiez que out conserve sa valeur, même quand vous activez/désactivez clock.
Félicitations, même si nous serons amenés à modifier légèrement le registre 1 bit par la suite, vous avez construit un superbe registre 8 bits ! Vous avez maintenant la possibilité d'ajouter des variables internes à votre processeur !
Nous avons maintenant tous les circuits permettant de construire un calculateur simple. Dans cet exercice, nous construisons un compteur qui est incrémenté régulièrement, c'est-à-dire une sorte de montre ou de réveil. Le circuit que vous construisez à cette question pourra être détruit à la fin, mais il va vous permettre de comprendre comment utiliser correctement les circuits que nous avons déjà créés. Ce circuit va aussi nous permettre d'identifier un défaut dans notre conception du registre 1 bit, ce qui va nous permettre de le corriger. Dans un nouveau circuit que vous appellerez test, ajoutez un registre 8 bits ayant une valeur d'initialisation égale à 0. Pour créer des entrées constantes, il faut aller dans l'onglet Input et sélectionner un ConstantVal (5ième icone). Par défaut, la constante est sur 1 bit. Pour lui affecter une valeur 0 sur 8 bits, double-cliquez dessus et saisissez 00000000. Modifiez votre circuit de façon à ce que WE soit toujours à vrai, et à ce que vous puissiez modifier les valeurs reinit et clock avec des entrées. À la sortie de votre registre, ajoutez un circuit permettant d'ajouter la constante 1 à la valeur du registre et utilisez un HexDisplay pour afficher la valeur. Pensez aussi à connecter une valeur constante 0 au cin de l'additionneur. Vérifiez que quand vous passez reinit de 0 à 1 puis de 1 à 0, la sortie de l'additionneur est bien égal à 1. Nous allons enfin installer une première horloge dans notre processeur. Supprimez l'entrée qui permet de modifier clock manuellement. À la place, allez dans l'onglet Sequential Elements et ajoutez une horloge (Clock, 8ième élément) que vous connecterez à l'entrée clock du registre. Cette modification ne devrait pas changer l'affichage de votre circuit qui devrait rester à 1 (puisque votre circuit calcul toujours la somme entre la valeur du registre, qui vaut 0, et la constante 1).

Une horloge est un circuit spécial qui passe régulièrement de 0 à 1 et de 1 à 0. Quand on connecte une horloge à notre registre, la valeur de in du registre est propagée lorsque clock vaut 1 alors que la valeur précédente est conservée lorsque clock vaut 0. Il faut savoir que la fréquence à laquelle clock passe de 0 à 1 et revient de 1 à 0 s'appelle la fréquence du processeur. Aujourd'hui cette fréquence se situe entre 2GHz et 3GHz (2 à 3 milliards de périodes par seconde). C'est à cette cadence que le processeur propage les calculs dans les registres, et c'est donc une métrique relativement pertinente pour estimer la vitesse de calcul d'un processeur. Notre simulateur simule un processeur à 1Hz (demi-période d'horloge de 500ms). Vous pouvez modifier cette fréquence dans les propriétés du circuit (Clock Time).
On souhaite maintenant incrémenter notre registre à chaque période. Commencez par passer reinit à 1, ce qui va charger la valeur 0 dans le registre. Ensuite, connectez la sortie de l'additionneur à l'entrée du registre (in). Pour le moment, votre additionneur devrait toujours vous donner la valeur 1 puisque, à cause de reinit, le registre reste figé avec la valeur 0.

Maintenant, passez reinit à 0, ce qui devrait avoir comme effet de mettre à jour la valeur du registre à chaque période d'horloge. Pour quelle raison le simulateur génère-t-il un message d'erreur (Stack Overflow) et pour quelle raison la valeur du registre semble sauter instantanément à une valeur aléatoire ? Après avoir observé le phénomène, repassez reinit à 1 pour bloquer la valeur du registre à 0. Voir question suivante.
Vous êtes confrontez à un problème classique : votre circuit modifie l'entrée du calcul 8-add pendant le calcul lui-même. Techniquement, quand clock vaut 1, les informations se propagent dans votre circuit de la sortie du 8-add au registre, puis de nouveau à l'entrée du 8-add, ce qui modifie le calcul pendant qu'il est effectué. Comme vous n'avez aucune garantie que les résultats ont été propagés à la même vitesse par tous les chemins (rappelez vous qu'un signal électrique se propage à une vitesse donnée, à savoir une vitesse proche de la vitesse de la lumière), vous pouvez facilement obtenir des valeurs aléatoires : certains bits sont activés sur certains circuits et sont re-propagés avant d'autres. De façon à stabiliser le circuit, il faut donc empêcher les entrées de changer pendant le calcul.

Comme solution, nous vous proposons de modifier le registre 1-reg. Avant toute chose, il faut savoir qu'il ne faut pas que vous détruisiez les entrées ou les sorties de votre circuit actuel, sinon, vous serez obligé de refaire les connexions dans le circuit 8-reg. Donc, sans détruire les entrées et les sorties que vous avez actuellement, nous vous proposons d'ajouter une seconde bascule JK en contre-phase de la première. Techniquement, on vous demande de mettre en œuvre le circuit présenté en figure .
Un registre 1-bit maître-escalve.

Dans ce circuit, la bascule JK de gauche, appelée bascule maître, est en phase avec clock, alors que celle de droite, appelée bascule esclave, est en contre-phase. En effet, si on suppose que reinit vaut 0, l'entrée E du maître vaut 1 quand clock 1, alors que l'entrée E de l'esclave vaut 1 quand clock vaut 0.

Avec cette modification, les valeurs sont propagées de in vers out en deux étapes. Quand clock est égal à 0, on est en phase de calcul : le processeur effectue des calculs avec les valeurs out des registres, mais les résultats de ces calculs ne sont pas propagés dans les registres puisque les bascules maîtres empêchent la propagation des valeurs (le E du maître vaut 0 puisqu'il est connecté directement à clock).

Ensuite, lorsque clock passe à 1, on est en phase de propagation : on suppose que tous les calculs ont atteint les entrées in des registres à cet instant. Les maîtres mémorisent alors les valeurs précédemment calculées pour s'en servir comme d'entrées pour la prochaine phase de calcul. Pendant cette phase de propagation, le circuit est dans un état stable : les esclaves empêchent les valeurs de passer ce qui évite de modifier le résultat des calculs pendant que le maître enregistre les résultats.

Enfin, lorsque clock repasse à 0, l'esclave enregistre la valeur du calcul précédent en propageant la valeur déjà stockée par le maître. Cette propagation a pour effet de modifier la valeur du registre et sa sortie, ce qui amorce donc une nouvelle phase de calcul avec de nouvelles valeurs (notez que, comme précédemment expliqué, le maître devient bloquant, ce qui garantie que, pendant ce nouveau calcul, les registres ne sont plus modifiés). Dans le circuit que nous vous proposons, reinit réinitialise instantanément la valeur des registres puisque reinit est connecté en phase à la fois avec les maîtres et avec les esclaves. Ce comportement est souhaité puisque lors d'une réinitialisation, on s'attend à ce que tous les registres reprennent instantanément leurs valeurs initiales, et ceux sans attendre une phase. Notez que la fréquence maximale d'un processeur est limitée par la distance que le signal électrique doit parcourir en suivant le plus long chemin de calcul entre les sorties et les entrées des registres. En effet, si la fréquence devient trop grande par rapport à cette distance, il devient alors possible que certains résultats atteignent les entrées mais pas d'autres, ce qui mettrait le processeur dans un état non prédictible.

Vu cette limite, on voit donc qu'à chaque fois qu'on arrive à multiplier par deux le nombre de transistors par cm2, on diminue par deux la distance à parcourir pour effectuer un calcul, ce qui permet d'augmenter la fréquence du processeur d'autant. Cette augmentation automatique de la fréquence avec la densité en terme de transistors est restée vraie jusque vers 2005 : tous les 18 mois, on divisait grosso-modo par deux la taille des transistors, ce qui permettait de multiplier la densité et donc la fréquence des processeurs par deux (loi de Moore).

Aujourd'hui, la loi de Moore reste correcte car on arrive toujours à diminuer la taille des transistors régulièrement. Toutefois, cette diminution de la taille de gravure ne se traduit plus par une augmentation automatique de la fréquence pour une toute autre raison : l'énergie consommée par un circuit augmente avec une loi en carré de la fréquence. Au delà de 3GHz à 4GHz, l'énergie consommée par un ordinateur deviendrait colossale (sans parler des problèmes de dissipation de la chaleur par effet Joule).
Dans le circuit test passez reinit à 0, ce qui devrait démarrer le calcul. Vérifiez que votre registre est bien incrémenté à chaque période d'horloge (soit chaque seconde si vous avez gardé la demi-période par défaut).
Féclitations ! Vous savez réaliser de petits calculateurs !
Nous avons maintenant créé pratiquement tous les circuits de base permettant de construire un processeur complet. Avant de se lancer dans cette mise en œuvre complète, il faut comprendre comment notre processeur va fonctionner.

Pour commencer, notre processeur a une mémoire de 16 cellules numérotées de 0 à 15. Une cellule mémoire contient un octet (une valeur sur 8 bits), et nous réutiliserons les registres 8 bits pour mettre en œuvre les cellules.

Ensuite, notre processeur possède 8 registres internes numérotés de 0 à 7. Chaque registre fait 8 bits, notre processeur est donc un processeur 8 bits. Pour bien comprendre la différence entre les registres internes et la mémoire, il faut savoir que la mémoire est un composant séparé du processeur et que notre processeur ne peut que :
  • effectuer des calculs sur les registres internes,
  • lire une cellule mémoire et stocker sa valeur dans un registre interne (instruction LD),
  • écrire la valeur d'un registre interne dans une cellule mémoire (instruction ST).

Trois des registres du processeur ont un rôle particulier qui sera expliqué dans la suite. Les cinq autres sont libres et permettent d'effectuer des calculs.

Le premier registre particulier est le 7ième registre qui se nomme pc (Program Counter). Ce registre indique le numéro de cellule mémoire qui contient la prochaine instruction à exécuter. Notre processeur offre deux types d'instructions.
  • Forme triplet : op out, i0, i1
  • Forme accumulateur : op out, val

Où :
  • op est un numéro d'instruction, par exemple, la valeur 2 correspond à l'addition,
  • out est le numéro de registre modifié par l'instruction (appelé registre de sortie de l'instruction),
  • i0 est le premier numéro de registre d'entrée de l'instruction,
  • i1 est le second numéro de registre d'entrée de l'instruction,
  • val est une valeur directe sur 8 bits.

Quand l'instruction est sous la forme triplet, elle effectue un calcul avec les registres i0 et i1, puis stocke le résultat dans le registre out. Par exemple, l'instruction 2 3 1 2 sous la forme triplet est une addition (puisque op vaut 2) qui ajoute la valeur du registre numéro 1 avec la valeur du registre numéro 2 puis stocke le résultat dans le registre numéro 3.

Quand l'instruction est sous forme accumulateur, elle effectue un calcul avec un registre accumulateur (le registre out) et la valeur directe encodée sur 8 bits. Par exemple, l'instruction 2 3 1 sous la forme accumulateur est une addition (puisque op vaut 2) qui ajoute 1 au registre numéro 3.

Pour encoder une instruction, nous avons besoin de 16 bits :
  • Premier octet 
    • op est encodé sur 4 bits (bits 0 à 3), ce qui permet de définir jusqu'à 32 instructions. Dans notre processeur, nous ne définirons que 3 instructions de base, n'hésitez pas à en ajouter en fonction de vos envies après avoir terminé l'exercice,
    • Le bit 4, appelé accu? indique si l'instruction est sous la forme accumulateur ou triplet,
    • Les bits 5 à 7 donnent le numéro du registre de sortie (avec 3 bits, on peut encoder 8 valeurs),
  • Deuxième octet :
    • Dans le cas d'une instruction sous la forme accumulateur, cet octet contient val,
    • Sinon, cet octet se décompose de la façon suivante :
      • Les bits 0 à 2 donnent i0,
      • Les bits 3 à 5 donnent i1,
      • Les bits 6 à 7 ne sont pas utilisés.
En sachant que l'instruction 0 est un LD, 1 est un ST et 2 est un ADD, voici quelques exemples d'instructions : Octet 1 : 000 1 0010 => op = ADD, accu? = 1, out = 0 Octet 2 : 1111 1111 => val = 15 => ajoute 15 au registre numéro 0 (out) Octet 1 : 010 0 0010 => op = ADD, accu? = 0, out = 2 Octet 2 : 00 001 100 => i0 = 4, i1 = 1 => ajoute la valeur du registre numéro 4 (i0) à celle du registre numéro 1 (i1), puis stocke le résultat dans le registre numéro 2 (out) Octet 1 : 100 0 0000 => op = LD, accu? = 0, out = 4 Octet 2 : 00 000 010 => i0 = 2, i1 = 0 => charge la cellule mémoire dont le numéro est stocké dans le registre numéro 2 (i0), puis stocke cette valeur dans le registre numéro 4 (out) (LD n'utilise pas i1) Octet 1 : 100 0 0001 => op = ST, accu? = 0, out = 4 Octet 2 : 00 000 010 => i0 = 2, i1 = 0 => écrit le contenu du registre numéro 0 (i1) dans la cellule mémoire ayant pour numéro la valeur du registre numéro 0 (i0) (ST n'utilise pas out puisque cette instruction ne modifie aucun registre du processeur, uniquement des cellules mémoires)

De façon à stocker les deux octets d'une instruction dans le processeur, nous utilisons le 5ième et le 6ième registre que nous nommons insnl (insn low) et insnh (insn high).

Finalement la boucle de notre processeur devient :
  • charge la cellule mémoire indiquée dans pc dans le registre insnl,
  • ajoute 1 à pc,
  • charge la cellule mémoire indiquée dans pc dans le registre insnh,
  • ajoute 1 à pc,
  • exécute l'instruction définie par les 8+8 bits de insnl et insnh.
Il n'y a pas de question dans cet exercice.
Dans cet exercice, nous concevons les circuits qui permettent de gérer les registres et la mémoire de notre processeur. D'un point de vu technique, nous mettons en œuvre les registres du processeur et la mémoire avec des registres. Bien que le circuit de base utilisé pour construire les registres et la mémoire soit le même, il existe trois différences fondamentales entre ces objets :
  • La mémoire se trouve en dehors du processeur, ce qui signifie que la fréquence de la mémoire n'est pas forcément la même que celle du processeur. Dans notre exercice, de façon à simplifier, nous utiliserons la même horloge dans les deux cas. Mais dans un processeur réel, il faut mettre en place des mécanismes de synchronisation lors des accès à la mémoire, ce qui ralentit l'accès à la mémoire par rapport à un accès registre.
  • Alors qu'à chaque cycle processeur, il est possible de lire et écrire simultanément plusieurs registre, le circuit d'une mémoire est prévu pour permettre au plus une unique lecture et une unique écriture pendant un cycle horloge (horloge de la mémoire).
  • À cause de problèmes d'encombrement, il n'est pas possible de mettre une grande quantité de registres dans un processeur (au mieux quelques dizaines ou centaines de kilo-octets stockés sous la forme de registres dans un cœur d'un processeur). En revanche, une mémoire ne contient que peu de circuits pour effectuer des calculs et il est possible de mettre plusieurs barrettes mémoires sur une carte mère. Il est donc beaucoup plus facile de construire de grandes quantités de mémoire (jusqu'à plusieurs tera-octets aujourd'hui dans une seule machine).

Comme les registres du processeurs et la mémoire sont construits au départ avec des registres, nous allons commencer par réaliser un assemblage de 8 registres, nommé 8x8-regs (8 registres de 8 bits) qui nous servira de base pour construire les registres du processeur et la mémoire dans la suite de l'exercice.

Le 8x8-regs doit pouvoir être utilisé pour construire les registres du processeur. Quand on analyse notre jeu d'instruction, on voit que :
  • Une instruction peut lire jusqu'à 2 registres pendant un cycle. C'est le cas des instructions sous la forme triplet,
  • Une instruction peut écrire au plus un registre pendant un cycle. C'est le cas d'une instruction de calcul qui utilise l'ALU, comme l'instruction ADD (qu'elle soit sous la forme triplet ou non), mais aussi de l'instruction LD qui permet de lire une cellule mémoire et de stocker sa valeur dans un registre.
  • Une instruction peut ne pas écrire dans un registre, comme l'instruction ST qui ne fait que écrire la valeur d'un registre dans une cellule mémoire. Il faut donc être capable de désactiver les écritures sur le 8x8-regs au besoin.
Pour sélectionner jusqu'à deux registres d'entrées d'une instruction dans le 8x8-regs, nous disposons déjà du circuit 8x8-mux. En revanche, pour sélectionner le registre écrit par une instruction, il nous manque un circuit. En effet, il faut être capable d'activer l'entrée WE de l'un des huit registres du 8x8-regs en fonction d'un numéro de registre encodé sur 3 bits (3 bits suffisent pour énumérer 8 = 23 valeurs).

À cette question, on vous demande donc de mettre en place un démultiplexeur. Un démultpléxeur possède une entrée nommée sel sur N bits et 2N sorties numérotés de x0 à xn-1. Si l'entrée vaut K, alors le démultiplexeur passe la sortie xK à vrai et laisse les autres à faux. Un démultiplexeur a aussi une seconde entrée nommée enable qui, lorsqu'elle vaut 0, désactive le multiplexeur. Dans ce cas, le démultiplexeur n'active aucune sortie.

Mettez en œuvre le circuit 8-demux dans lequel N=3 (c'est-à-dire 2N = 8). Chaque sortie xi doit être connecté à une porte AND possédant 4 entrées. La première des quatre entrées est directement connectée à enable de façon à pouvoir déconnecter le démultiplexeur. Chacune des trois autres entrées est associée à un des bits de sel, soit directement, soit à son inverse. Par exemple, pour la sortie x5, il suffit de voir que 5 = 101b, donc, il faut connecter l'entrée 1 de sa porte AND au bit 0 de sel, l'entrée 2 à l'inverse du bit 1, et l'entrée 3 au bit 2.
Nous pouvons maintenant construire le 8x8-regs qui assemble 8 registres. On souhaite avoir les entrées suivantes :
  • v0 à v7 sont les valeurs initiales des registres (8 bits),
  • si0 et si1 sont les numéros des deux registre lus par l'instruction courante (3 bits),
  • sout est le numéro de registre écrit par l'instruction (3 bits), vout la valeur qu'on souhaite écrire (8 bits) et WE indique si les écritures sont activées (1 bit),
  • reinit permet de réinitialiser les huit registres aux valeurs initiales (v0 à v7) (1 bit),
  • clock est l'horloge (1 bit).

En sortie, le circuit doit renvoyer :
  • r0 à r7 sont les valeurs courantes des 8 registres,
  • vi0 et vi1 sont les valeurs des registres si0 et si1,

Les connexions entre les 8 registres, les vi et les ri est naturelle. Pour sélectionner vi0 et vi1 à partir de si0 et si1, il faut utiliser des 8x8-mux. Pour sélectionner le registre sout, il faut utiliser un 8-demux que vous connecterez aux entrées WE des huit registres. Il faut aussi connecter le WE du circuit au enable du 8-demux. Le vout d'entrée du circuit, lui, doit être connecté à chaque in des huit registres. Enfin, le reinit et le clock d'entrée du circuit doit être connecté à chacun des reinit et clock des 8 registres.
On construit maintenant le circuit REGS qui contient les registres du processeur. Construire ce circuit à partir de 8x8-regs est très simple : il suffit d'initialiser chacun des registres à la valeur 0 puis de renommer r5 en insnl, r6 en insnh et r7 en pc. Le autres entrées ou sorties doivent conserver le même nom. Pendant que vous mettez en œuvre REGS, profitez-en pour vérifier que votre mise en œuvre de 8x8-regs est correcte. En effet, identifier un éventuel bug dans 8x8-regs plus tard sera beaucoup plus complexe.
Nous pouvons maintenant mettre en œuvre le circuit MEM. Ce circuit assemble 2 circuits 8x8-regs pour simuler une mémoire de 16 octets, ce qui sera largement suffisant pour faire des tests. MEM prend en entrée :
  • address sur 8 bits : le numéro de cellule mémoire (registre) accédé. Même si on n'a que 16 octets de mémoire, on prévoit 8 bits pour address puisque notre processeur est un 8 bits,
  • data sur 8 bits : la valeur écrite dans l'adresse si l'instruction en cours est une écriture mémoire (instruction ST),
  • WE sur 1 bit : indique si l'instruction en cours est une écriture,
  • reinit sur 1 bit : permet de réinitialiser la mémoire à sa valeur initiale,
  • clock sur 1 bit : l'horloge.

En sortie, MEM renvoie la valeur se trouvant à l'adresse address.

Initialement, nous vous proposons de placer un programme directement dans la mémoire. Ce programme assez simple incrémente la 15ième cellule mémoire. Si on suppose que la cellule 15 contient la valeur d'une variable x initialisée à 128 (0x80 en hexadécimal), le code exécuté par notre programme serait à peu près celui là en Java : do { x = x + 1; } while(true);

À bas niveau, voici les valeurs initiales que doit contenir la mémoire pour exécuter ce programme : Octet 0 : 000 1 0010 Octet 1 : 00001111 => ajoute 15 à r0 (r0 valant initialement 0, r0 vaut 15 en sortie, c'est-à-dire le numéro de la cellule contenant la variable x) Octet 2 : 001 0 0000 Octet 3 : 00 000 000 => charge la cellule se trouvant en r0 dans r1 (charge la 15ième cellule car r0 = 15) Octet 4 : 001 1 0010 Octet 5 : 00000001 => ajoute 1 à r1 Octet 6 : 000 0 0001 Octet 7 : 00 001 000 => écrit r1 dans la cellule r0 (adresse de x) Octet 8 : 111 1 0010 Octet 9 : 11111000 => ajoute 248 à pc (comme pc = 10 avant d'exécuter l'instruction, pc + 248 = 258 modulo 256 = 2 => saute à l'octet 2, ce qui permet de boucler) Octets 10 à 14 : 00000000 Octet 15 : valeur initiale de x, mettez 01110000 (0x80 en hexadécimal) Le circuit MEM présenté en solution ajoute aussi une sortie qui permet d'observer la cellule 0xf (laquelle contient la variable incrémentée par le programme) de façon à pourvoir débugger.
Circuit MEM
Nous pouvons enfin attaquer la phase finale de conception du processeur. À cette question, nous créons le circuit nommé MICROCODE qui génère les instructions à exécuter. Comme précédemment expliqué, la boucle principale de notre processeur consiste en lire la cellule se trouvant en pc et la stocker dans le registre insnl, ajouter 1 à pc, lire la cellule se trouvant en pc et la stocker dans le registre insnh, ajouter 1 à pc, exécuter insnl/insnh et recommencer.

Plutôt que de créer des cas spéciaux d'additions ou de lecture pour effectuer ces opérations, nous allons générer manuellement les 4 instructions qui précédent l'exécution de insnl/insnh avant de générer l'instruction encodée dans insnl/insnh. En interne, de façon à simuler ces instructions, nous allons utiliser un registre 8 bits nommé mini-pc. La valeur de ce registre indique quelle instruction il faut générer : l'une des quatre instructions qui permet de lire insnl/insnh ou l'instruction insnl/insnh.

Techniquement, ce circuit prend en entrée :
  • insnl et insnh sont les valeurs se trouvant dans les registres insnl et insnh. Après avoir exécuté le chargement de ces registres, le circuit va simplement renvoyer ces valeurs,
  • reinit et clock pour piloter le registre mini-pc.

De façon à sélectionner l'instruction à exécuter en fonction de mini-pc, il faut utiliser deux 8x8-mux (l'un pour la partie 8 premiers bits de l'instruction et l'autre pour la 8 bits suivants) pilotés par les 3 bits de poids faible de mini-pc. Ces 8x8-mux doivent générer les sorties suivantes pour le circuit : mini-pc = 0 : insnl = 101 0 0000 insnh = 00 000 111 => ld insnl, pc mini-pc = 1 : insnl = 111 1 0010 insnh = 00000001 => add pc, 1 mini-pc = 2 : insnl = 110 0 0000 insnh = 00 000 111 => ld insnh, pc mini-pc = 3 : insnl = 111 1 0010 insnh = 00000001 => add pc, 1 mini-pc = 4 : insnl = insnl donné en entrée du circuit insnh = insnh donné en entrée du circuit Enfin, à chaque cycle, votre circuit doit incrémenter mini-pc, et lorsqu'il vaut 5, le faire revenir à 0 (en utilisant reinit par exemple). Notez que si mini-pc vaut 5, c'est que les bits 0, 1 et 2 valent respectivement 1, 0, 1.
Une fois que notre circuit MICROCODE a généré une instruction à exécuter, il faut la décoder, c'est-à-dire générer toutes les informations qui vont permettre de piloter les autres circuits.

Créez un nouveau circuit nommé DECODER qui prend en entrée :
  • insnl : la partie basse de l'instruction à analyser,
  • insnh : la partie haute de l'instruction à analyser,
Ce circuit doit générer les sorties suivantes :
  • func sur 16 bits. Il s'agit de la fonction que doit exécuter le circuit ALU. Comme notre jeu d'instruction ne contient qu'une addition, il suffit de générer la valeur 0 sur 16 bits à partir d'une constante.
  • wm? est vrai quand l'instruction écrit en mémoire, c'est-à-dire quand l'instruction exécutée est un ST (valeur 1). Pour identifier ce cas, il faut isoler les bits qui donnent l'instruction, c'est-à-dire les 4 premiers de insnl, puis utiliser un 8-demux pour savoir si l'instruction est l'instruction 1 (ST). Un seul 8-demux piloté par les 3 premiers bits du numéro d'instruction est nécessaire puisque nous n'utilisons pas le 4ième bit du numéro d'instruction.
  • wr? est vrai lorsque l'instruction écrit dans un registre, c'est-à-dire si l'instruction n'est pas ST.
  • alu? indique quelle valeur doit être utilisée pour écrire dans un registre quand wr? est vrai : la valeur renvoyée par le circuit MEM quand alu? vaut 0 ou la valeur renvoyée par le circuit ALU quand alu? vaut 1. Techniquement, seule l'instruction LD utilise la sortie de MEM pour l'écrire dans un registre. La valeur alu? est donc égal à l'inverse de la sortie x0 du 8-demux que vous avez utilisé pour identifier l'instruction ST.
  • cst? indique si la seconde opérande d'entrée de l'instruction est une constante. Cette valeur est donc vraie si et seulement si accu? (4ième bit de insnl) est vrai. En effet, dans ce cas, l'instruction écrite sous la forme op rout, val effectue un calcul à partir de rout et de la constante val au lieu d'effectuer un calcul à partir de deux registres.
  • sout donne le registre écrit par l'instruction (bit 5, 6 et 7 de insnl).
  • si0 donne le premier registre d'entrée de l'instruction, c'est-à-dire l'adresse pour les instructions LD et ST, et la première opérande pour ALU. Dans le cas d'une instruction au format triplet (c'est-à-dire quand le bit 4 de insnl vaut 0), il faut renvoyer les bits 0, 1 et 2 de insnh. Dans le cas d'une instruction au format accumulateur, il faut renvoyer sout.
  • si1 donne le second registre d'entrée de l'instruction, c'est-à-dire la donnée à écrire pour ST ou la seconde opérande pour ADD. Il faut donc renvoyer les bits 3, 4 et 5 de insnh.
  • vi1 donne la valeur constante qui doit être utilisé en remplacement de si1 dans le cas où cst? vaut 1. Il faut renvoyer insnh.
Vous pouvez enfin mettre en œuvre votre processeur. Vous avez besoin de deux entrées : reinit et clock. Tout le reste sera généré par les autres circuits. Vous avez besoin des circuits suivants :
  • Un circuit MICROCODE pour générer les instructions,
  • Un circuit DECODER connecté à MICROCODE pour interpréter les instructions,
  • Un circuit REGS qui contient les registres. Ce circuit prend en entrée les si0, si1, sout et wr? généré par DECODER (l'entrée vout de ce circuit sera donnée par un autre circuit).
  • Un circuit 2x8-mux permettant de sélectionner la deuxième opérande de l'instruction. Quand cst? vaut 0, cette opérande est donnée par le vi1 de REGS (instruction au format triplet) et par le vi1 de DECODER sinon (format accumulateur).
  • Un circuit ALU qui utilise le vi0 de REGS comme première valeur, la sortie du 2x8-mux précédent comme seconde valeur, et qui exécute la fonction func générée par DECODER.
  • Un circuit MEM qui utilise le vi0 comme adresse, la sortie du 2x8-mux précédent comme data et le wm? de DECODER comme entrée WE.
  • Un circuit 2x8-mux qui prend en entrée la sorties de MEM en x0, la sortie de ALU pour x1, et la sortie alu? de DECODER comme sélecteur. Pour terminer le processeur, il suffit de connecter la sortie de ce circuit au vout de REGS puisque, si wr? est vrai, c'est la sortie de ce circuit qu'on veut enregistrer dans le registre de sortie.
Ce circuit étant légèrement complexe, surtout parce que des bugs de mise en œuvre provenant d'autres circuits vont apparaître, nous vous conseillons vivement d'utiliser des HexDisplay pour afficher l'état des registres, la valeur v0/adresse utilisée en entrée de ALU/MEM, la valeur v1/data utilisée en entrée de ALU/MEM et la valeur vout envoyée dans REGS. Vous pouvez aussi afficher la case mémoire 15 pour voir l'état de cette case mémoire évoluée au cours du temps (il faut alors ajouter une sortie à MEM pour pouvoir connaître cette valeur). Au démarrage, cette valeur devrait prendre la valeur 0x80 et être incrémentée régulièrement pas le programme. N'hésitez pas aussi à afficher l'état du minipc de MICROCODE en ajoutant une sortie à ce circuit. Bon courage !
Si vous êtes arrivé au bout de cet exercice, bravo ! Vous savez construire un processeur de bout en bout ! Depuis la fin des années 70, on n'écrit plus de processeur directement avec des portes logiques, sauf pour comprendre sa mise en œuvre comme dans cet exercice. En effet, dans notre exercice, nous avons fait l'hypothèse que MEM avait la même fréquence que le reste du processeur, et que ALU était capable d'effectuer un calcul en moins d'un demi-cycle. Dans des machines réelles, ce n'est pas le cas. Il faut donc synchroniser des circuits qui effectuent des opérations à différentes vitesses avec la même horloge, voir des circuits qui ont des horloges différentes quand on communique entre la mémoire et le processeur (on parle de circuit multi-clock domains). Comme gérer ces synchronisations à la main devient rapidement très difficile, on utilise aujourd'hui des langages plus expressifs comme VHDL ou Verilog.