Fonctionnement d'un ordinateur/L'unité de contrôle
Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, 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équence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l'unité de décodage d'instruction, une portion du processeur qui « décode » l'instruction.

Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des signaux de commande adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données.
| Micro-opération, encodage en binaire | |||
|---|---|---|---|
| Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |
Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des architectures actionnées par déplacement. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes.
Les séquenceurs câblés et microcodés
[modifier | modifier le wikicode]Pour 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
[modifier | modifier le wikicode]Si les instructions sont décodées par 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. La complexité du séquenceur dépend aussi de la complexité des instructions machine. 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, alors que les séquenceurs câblés sont préférés sur les processeurs RISC.
L'implémentation du séquenceur
[modifier | modifier le wikicode]Sur 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 : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur.

Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre 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, information qui est mémorisée dans un registre.

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 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). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction.
Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un compteur one-hot, ou encore un compteur en anneau. La raison est 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 encode 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.
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 le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit 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
[modifier | modifier le wikicode]Notons 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.
Les 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
[modifier | modifier le wikicode]Pour 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 par un circuit combinatoire. 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 parlerons 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 microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode.
Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur.
Un autre 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.
Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique.
En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut ê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é. Mais cela n'était pas l'intérêt principal, car 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é.
Le control store
[modifier | modifier le wikicode]La 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.
Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU.
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.
L'optimisation du microcode
[modifier | modifier le wikicode]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. On pourrait croire que les vides sont 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, ce 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 à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. 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. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. 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.
Les circuits d’exécution du microcode
[modifier | modifier le wikicode]Le 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
[modifier | modifier le wikicode]Un 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
[modifier | modifier le wikicode]Parfois, 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 corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient 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 leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. 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.
Il existe des processeurs dont le microcode est facilement programmable, accessible par le programmeur. 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, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite 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é a de nombreux défauts. 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 doivent réinitialiser le microcode pour ne pas exécuter des instructions 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, les processeurs de ce type sont très très rares.
La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le control store est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, 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. En réalité, le control store est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur.
Pour cela, le control store est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les 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. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. 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).
Les séquenceurs hybrides
[modifier | modifier le wikicode]Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. 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.
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. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué.
Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé.
Le séquenceur hybride du 8086
[modifier | modifier le wikicode]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. Le séquenceur de l'Intel 8086 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 instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode.
Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande.
Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur.
La gestion des branchements et instructions à prédicats
[modifier | modifier le wikicode]L'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
[modifier | modifier le wikicode]Les 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
[modifier | modifier le wikicode]Le 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
[modifier | modifier le wikicode]Voyons 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
[modifier | modifier le wikicode]Passons 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
[modifier | modifier le wikicode]Les 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.
