Fonctionnement d'un ordinateur/L'unité de contrôle
Pour rappel, le chemin de données est rempli de composants à configurer d'une certaine manière pour exécuter une instruction. Pour configurer le chemin de données, il faut envoyer les signaux de commande adéquats sur l'entrée de sélection de l'unité de calcul, les entrées du banc de registres, ou les circuits du bus du processeur. Lorsque les circuits du chemin de données reçoivent ces signaux de commande, ils sont conçus pour effectuer une action précise et déterminée.
Mais l'instruction chargée depuis la mémoire ne précise pas les signaux de commande, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une série de signaux de commandes adéquats. C'est le rôle de l'unité de décodage d'instruction, une portion du séquenceur qui « décode » l'instruction. Le séquenceur traduit une instruction en suite de micro-opérations et émet les signaux de commande pour chaque micro-opération.
- Il existe des processeurs assez rares où chaque instruction machine précise directement les signaux de commande, sans avoir besoin d'une unité de décodage d'instruction. L'encodage de l'instruction en mémoire est alors très simple : il suffit de placer les signaux de commande les uns à la suite des autres, rien de plus. De telles architectures sont appelées des architectures actionnées par déplacement. Elles feront l'objet d'un chapitre dédié. En attendant, nous allons mettre ces architectures de côté pour le moment et nous concentrer sur des architectures plus courantes.
La traduction en question n'est pas simple, pour une raison assez importante : les instructions sont décomposées en plusieurs étapes, appelées micro-instructions, chacune configurant le chemin de donnée d'une manière bien précise. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Dans le cas le plus simple, chaque instruction correspond à une micro-opération et la traduction est alors triviale : le circuit est un simple circuit combinatoire. Mais dès que ce n'est pas le cas, le séquenceur devient un circuit séquentiel avec toute la complexité que cela implique.
Les séquenceurs câblés et microcodés
modifierPour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé.
Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes.
- La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques.
- La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit.
- La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (Programmable Logic Array).
Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir.
Les séquenceurs câblés
modifierSi les instructions sont décodées par un circuit à base d'un assemblage de portes logiques et de registres, on parle de séquenceur câblé. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides. Par contre, les séquenceurs câblés sont souvent utilisés sur les processeurs RISC, qui ont peu d'instructions, pour lesquels la complexité du séquenceur et le nombre de portes est assez faible et supportable. La complexité du séquenceur dépend de la complexité des instructions machine.
L'implémentation du séquenceur sans compteur
modifierSur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire.
C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente, ce qui fait qu'un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, la mémoire dans laquelle sont stockées les instructions doit être physiquement séparée de la mémoire dans laquelle on stocke les données, afin de pouvoir charger l'instruction tout en accédant aux données, le tout en un seul cycle d'horloge processeur.
L'implémentation du séquenceur avec un compteur
modifierSur la plupart des processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur est modifié, notamment par l'intégration d'un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, ce qui fait qu'il mémorise cette information dans un registre. En conséquence, les séquenceurs câblés sont des circuits séquentiels.
Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque cycle d'horloge, ou du moins à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM, par exemple). Il est réinitialisé quand l'instruction se termine, généralement quand le compteur a atteint le nombre de cycles adéquat pour éxecuter l'instruction.
Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. A la place, il s'agit souvent d'un compteur basé un registre à décalage, appelé un compteur one-hot, ou encore un compteur en anneau. Les raisons sont que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur doit coder peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit, à condition qu'on accepte d'ajouter quelques bascules.
En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible. Ce qui fait qu'on l'appelle le décodeur d'instruction. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate.
Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans l'accumulateur d'un processeur à accumulateur. Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le program counter.
- Le premier cycle copie le program counter dans le registre d’interfaçage pour les adresses.
- Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle.
- Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le program counter en parallèle.
- Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse.
- Le cinquième lit la donnée à lire depuis la mémoire.
- Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur.
Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur one hot facilite l'implémentation de la matrice de portes logiques.
La détermination de la fin d'une instruction
modifierNotons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le program counter pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur.
Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation.
L'implémentation des instructions de durée variable
modifierLes deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes.
Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur.
Les séquenceurs microcodés
modifierPour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les séquenceurs microcodés. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le control store, tandis que son contenu s'appelle le microcode.
- Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous aprlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide.
Les séquenceurs micro-codés sont plus simples à concevoir et cela simplifie beaucoup le travail des concepteurs de processeurs. L'usage du microcode a un autre avantage majeur : il permet d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie de sa simplicité de conception, un séquenceur microcodé a des inconvénients qui ne sont pas négligeables. Par exemple, ils utilisent plus de portes logiques que les séquenceurs câblés, vu qu'une ROM est un circuit gourmand en portes logique. De plus, un séquenceur micro-codé est plus lent qu'un séquenceur câblé, la raison principale étant qu'une mémoire ROM est bien plus lente qu'un circuit combinatoire fabriqué directement avec des portes logiques.
Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et complexe, avec beaucoup de modes d'adressages différents. Implémenter un grand nombre d'instruction avec un séquenceur câblé aurait été beaucoup trop compliqué, ce qui fait que les concepteurs de processeur préféraient utiliser un microcode. Entre une instruction émulée par une suite d'instructions machines, et la même instruction microcodée, les performances étaient généralement similaires. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, le microcode pouvant être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Pour résumer, les instructions microcodées n'étaient pas forcément plus rapides que leur équivalent logiciel, mais elles existent et cela suffisait sur les architectures CISC qui privilégiaient la taille du programme - la code size. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé.
Un exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes.
Le control store
modifierLa caractéristique principale du control store est sa capacité, qui est souvent assez petite. La capacité du control store dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du control store correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations.
- Avec le microcode horizontal, chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits !
- Avec un microcode vertical, les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le control store. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un control store plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur.
L'implémentation interne du control store ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le control store contenait 512 bytes/microinstructions de 21 bits chacune. Le control store n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes.
Le control store a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le control store contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un control store de 128 bytes, ce qui laisse 8 bytes vides. La position des vides dans le control store dépend de la solution utilisé. On pourrait croire que les vides sont généralement placés à la fin du control store, mais il est parfois préférable de disperser les vides dans le control store, afin de simplifier les circuits adossés au microcode, que nous allons voir dans ce qui suit.
Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le control store. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles.
La première optimisation de ce genre consiste à partager des bouts de microcode entre instructions machines, sur le même principe que les fonctions/sous-programmes/routines logicielles. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage.
La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du control store, dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. Une autre solution consiste à décoder certaines instructions simples sans passer par le microcode, ce qui donne les séquenceur hybrides dont nous parlerons dans la suite du chapitre.
Les circuits d’exécution du microcode
modifierLe processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici.
La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le control store, au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante.
La seconde méthode fait que le séquenceur contient un équivalent du program counter pour le microcode. On trouve ainsi un micro-séquenceur qui regroupe un registre d’adresse de micro-opération et un micro-compteur ordinal. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante.
Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille.
Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement.
Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante :
Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour.
Localiser la première microinstruction à exécuter dans le control store
modifierUn premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le control store. Pour cela, il y a plusieurs solutions.
La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le control store. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du control store, ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse.
L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le control store est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un control store utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents.
La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le control store. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un control store où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le control store, dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du control store, alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides.
La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le control store, elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son control store contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives.
L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-program counter de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits.
Cette solution a cependant pour défaut de laisser beaucoup de vides dans le control store. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du control store. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement.
Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du control store. Les deux premières méthodes remplissent le control store au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le control store. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du control store, car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc.
La mise à jour du microcode
modifierParfois, le processeur permet une mise à jour du control store, ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de faire des corrections de bugs ou de corriger des problèmes de sécurité assez tordus. Il est en effet fréquent que les processeurs soient sujets à des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans une documentation officielle assez imposante, preuve que ces bugs ne sont pas des exceptions d'une grande rareté. Si la plupart de ces bugs ne peuvent pas être corrigés, quelques bugs peuvent cependant se corriger avec des mises à jour du microcode interne au processeur. Les bugs en question peuvent être liés à des bugs dans le microcode lui-même, ou à des bugs situés ailleurs, mais qui peuvent être corrigés ou mitigés en bidouillant le microcode. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert.
Mais il existe des processeurs dont le microcode est facilement programmable, accessible par le programmeur, ce qui permet d'ajouter des instructions à la volée. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction. Cela permet de réduire le nombre d'instructions du programme, ce qui réduit la taille du code (la mémoire prise par le programme exécutable), mais facilite aussi la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un control store assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité pose des problèmes : si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui n'ont pas intérêt à utiliser des instructions microcodées, sous peine d'exécuter des instructions microcodées incorrectes. Les problèmes de compatibilité entre processeurs sont aussi légion (les programmes codés ainsi ne marchent que sur un seul processeur, pas les autres). Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Aussi, il n'est pas étonnant que les microcodes nus, facilement accessibles, sont très très rares. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastique intégrées au processeur (microcode fournit chiffré avec des clés connues seulement des fabricants de CPU, autres).
La mise à jour du microcode peut se faire de deux grandes manières, l'une étant permanente, l'autre étant temporaire. La mise à jour permanente du microcode signifie que le control store est une EEPROM ou une mémoire ROM reprogrammable. Mais c'est une solution rarement utilisée, car ces mémoires sont très difficiles à mettre en œuvre dans les processeurs. Le control store doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. Les mémoires mask ROM, non-reprogrammables, rentrent clairement dans ce cadre car elles sont fabriquées avec les mêmes transistors que le reste du processeur. Mais ce n'est pas le cas des autres mémoires ROM basées sur des transistors à grille flottante, totalement différents des transistors normaux. Il faut donc trouver une autre solution.
L'autre solution permet de mettre à jour le control store temporairement. Pour cela, le control store est implémenté avec deux mémoires. Une mémoire ROM qui contient le microcode originel, et une mémoire RAM. Pour simplifier ls explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. La micro-RAM est utilisée lors du décodage des instructions. L'idée est de permettre de modifier le microcode dans la micro-RAM avec un microcode corrigé. Ce dernier est chargé dans le processeur, dans la micro-RAM, peu après l'allumage du processeur. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Des mises à jour du BIOS peuvent contenir un microcode corrigé, capable de corriger des bugs ou des failles de sécurité.
Les séquenceurs hybrides
modifierLes séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés et mélangent les deux. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé.
L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un circuit de prédécodage. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un control store entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés.
Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. L'Intel 8086 ne contient pas deux séquenceurs séparés, mais est organisé comme suit : un control store de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une Group Decode ROM qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée Group Decode ROM est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instruction qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode.
La gestion des branchements et instructions à prédicats
modifierL'implémentation des branchements implique tout le séquenceur et l'unité de chargement. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le program counter quand un branchement est détecté. L'altération du program counter est le fait de l'unité de chargement. Elle a juste besoin qu'on lui précise à quelle adresse brancher, et quand un branchement a lieu. Quant au séquenceur, il doit gérer tout le reste.
L'implémentation des branchements conditionnels
modifierLes branchements inconditionnels sont les plus simples à gérer. Il suffit de détecter si une instruction est un branchement inconditionnel, et de déterminer où se trouve l'adresse de destination. Pour cela, on doit ajouter un circuit de détection des branchements, qui détecte si l'instruction exécutée est un branchement ou non. Il est situé dans le décodeur d'instruction. La détermination de l'adresse dépend du mode d'adressage et implique de configurer correctement le chemin de données. Il y a peu çà dire
Par contre, les branchements conditionnels demandent en plus de vérifier qu'une condition est respectée, ils demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels :
- Ceux qui doivent être précédés d'une instruction de test ou de comparaison.
- Ceux qui effectuent le test et le branchement en une seule instruction machine.
- Ceux où les branchements conditionnels sont émulés par une skip instruction, une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel.
Formellement, un branchement conditionnel demande de faire deux choses : calculer une condition, puis faire le branchement suivant le résultat de la condition. Dans ce qui suit, nous allons d'abord voir le cas où calcul de la condition et saut conditionnels sont réalisés tous deux par une seule instruction. Puis, nous verrons ensuite le cas où test et saut sont séparés dans deux instructions séparées. La raison est que le premier cas est le plus simple à implémenter. Le second cas demande d'ajouter des registres et quelques circuits, ce qui rend le tout plus compliqué.
Les circuits de saut conditionnel et de calcul de la condition
modifierLe calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données.
Premièrement, deux opérandes sont lus depuis les registres, puis sont envoyés à un circuit soustracteur qui soustrait les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les bits intermédiaires, et ils sont combinés pour calculer les différentes conditions.
En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égaux, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs.
Outre le calcul de la condition, un branchement conditionnel saute ou non à une certaine adresse. On sait déjà que le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du program counter et en mettant à 1 son entrée de réinitialisation. La seule difficulté est de décider s'il faut mettre à jour le program counter ou non.
Le program counter doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : elle est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant.
Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple.
Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit. Et c'est ce que nous allons voir dans la section suivante.
Le registre d'état ou les registres à prédicats et les circuits associés
modifierVoyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières.
- La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat.
- La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement.
Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change.
Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul.
L'autre cas est celui où les résultats des conditions sont mémorisés dans des registres à prédicats, connectés au séquenceur. Cela amène deux problèmes : l'instruction de test doit enregistrer le résultat dans le bon registre à prédicat, et il faut aussi lire le bon registre à prédicat suivant le branchement. Il faut donc gérer la sélection en lecture et en écriture. Rappelons que les registres à prédicats sont numérotés, ils ont un nom de registre dédié qui est fourni par le séquenceur. La sélection en lecture et écriture des registres à prédicat se base donc sur ces noms de registre. Pour la sélection en lecture, le choix du registre à prédicat voulu est réalisé par un multiplexeur, commandé par le séquenceur. Le multiplexeur est intégré à l'unité de chargement ou au séquenceur, peu importe. Pour l'enregistrement dans le bon registre à prédicat, le choix est réalisé en sortie de l'unité de calcul, généralement par un démultiplexeur.
L'implémentation des skip instructions
modifierPassons maintenant au cas des skip instruction, qui permettent d'émuler les branchements conditionnels par une instruction de test spéciale. Pour rappel, une skip instruction permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. . Dans ce cas, le program counter est incrémenté normalement si la condition n'est pas respectée, mais il est incrémenté deux fois si elle l'est. Les branchements inconditionnels s’exécutent normalement. Là encore, suivant la condition testée, on trouve un multiplexeur pour choisir le bon résultat de condition.
L'implémentation des instructions à prédicats
modifierLes instructions à prédicats sont des instructions qui s’exécutent seulement si une condition précise est remplie. Elles sont précédées d'une instruction de test qui met à jour le registre d'état ou un registre à prédicat. L'instruction à prédicat récupère alors le résultat de la condition, calculé par l'instruction de test précédente, et l'utilise pour savoir si elle doit se comporter comme un NOP ou si elle doit faire une opération. Leur implémentation est variable et deux grandes méthodes sont possibles. La première n’exécute pas l'instruction si la condition est invalide, l'autre l’exécute en avance mais n'enregistre pas son résultat dans les registres si la condition se révèle ultérieurement invalide.
La première méthode exécute l'opération, mais l'annule si la condition n'est pas respectée. Le calcul des conditions est fait en parallèle de l'autre opération et l'annulation se fait simplement en n'enregistrant pas le résultat de l’opération dans les registres. Le calcul de la condition s'effectue dans le séquenceur, mais le résultat est envoyé dans le chemin de données pour configurer un circuit qui autorise ou non l'enregistrement du résultat dans les registres. Un défaut de cette technique est que l'instruction est effectivement exécutée, ce qui fait que le processeur a consommé un peu d'énergie et a pris un peu de temps pour faire le calcul. L'autre conséquence est que l'instruction mobilise une unité de calcul ou de transfert entre registre, le banc de registres, etc. En soi, ce n'est pas un problème. Mais ça l'est sur les processeurs modernes, qui sont capables d’exécuter plusieurs instructions en parallèle, dans un ordre différent de celui imposé par le programmeur. Nous verrons ces techniques d’exécution en parallèle dans les derniers chapitres du cours. Toujours est-il que sur ces processeurs, une instruction à prédicats va mobiliser des ressources matérielles comme l'ALU ou le bus interne, pour éventuellement fournir un résultat inutile, alors qu'une autre instruction aura pu prendre sa place et calculer des données utiles.
La seconde méthode est la plus intuitive : elle consiste à lire le registre d'état/de prédicat, pour décider s'il faut faire ou non l'opération. Pour cela, le séquenceur lit le registre d'état/à prédicat, et génère les signaux de commande adaptés : il génère les signaux de commande d'un NOP si la condition n'est pas respectée, et il génère les signaux de commande pour l'opération voulue sinon. L’avantage de cette méthode est que l'instruction ne s’exécute pas si la condition n'est pas remplie. Le processeur ne gâche pas d'énergie pour rien, il peut immédiatement passer à l'instruction suivante si celle-ci est disponible, etc. De plus, sur les processeurs modernes capables d’exécuter plusieurs instructions en parallèle, on ne mobilise pas de ressources matérielles si la condition n'est pas remplie et celles-ci sont disponibles pour d'autres instructions.