Fonctionnement d'un ordinateur/Le chemin de données
Comme vu précédemment, le chemin de donnée est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité d communication avec la mémoire, et le ou les bus qui permettent à tout ce petit monde de communiquer.
Les unités de calculModifier
Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée unité arithmétique et logique. Certains préfèrent l’appellation anglaise arithmetic and logic unit, ou ALU.
L'interface d'une unité de calculModifier
Étudions rapidement l'interface de l'ALU, à savoir ses entrées et sortie. Niveau entrée, il faut bien sûr fournir les opérandes à l'unité de calcul. Outre les opérandes, il faut spécifier l'instruction à effectuer à notre unité de calcul : il faut bien prévenir notre unité de calcul qu'on veut faire une addition et pas une multiplication. Pour cela, l'unité de calcul possède une entrée pour la configurer : l'entrée de sélection de l'instruction. Sur cette entrée, on va mettre un numéro qui précise l'instruction à effectuer. Pour chaque unité de calcul, il existe une correspondance entre ce numéro et l'instruction à exécuter. Généralement, l'opcode de l'instruction est envoyé sur cette entrée, du moins sur les processeurs où l'encodage des instructions est "simple".
Niveau sortie, on en a une pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état : le registre d'état est obligatoirement relié à certaines sorties de l’unité de calcul.
L'intérieur d'une unité de calculModifier
Les unités de calcul les plus simples contiennent un circuit différent pour chaque opération possible. L’entrée de sélection sert uniquement à sélectionner le bon circuit.
Pour effectuer cette sélection, on peut utiliser des multiplexeurs commandés par l'entrée de sélection.
D'autres envoient les opérandes à tous les circuits en même temps, et activent ou désactivent chaque sous-circuit suivant les besoins. Chaque circuit possède ainsi une entrée de commande, dont la valeur est déduite par un circuit combinatoire à partir de l'entrée de sélection d'instruction de l'ALU (généralement un décodeur).
Nous avions déjà vu un exemple d'unité de calcul configurable dans le chapitre sur les circuits de calcul. Vous devez vous souvenir du circuit additionneur-soustracteur, capable d'effectuer soit des additions, soit des soustractions. Et bien on peut l'utiliser comme unité de calcul. Cela donne une unité de calcul très simple, qui ne peut faire que deux opérations, mais c'est malgré tout une unité de calcul. Or, ce circuit est configurable :l'envoi d'un bit permet de choisir quelle opération faire, sans pour autant configurer un multiplexeur. C'est donc un exemple d'unité de calcul configurable.
Le bit-slicingModifier
Sur certains processeurs assez anciens, de l'époque des premiers microprocesseurs 8 et 16 bits, l'ALU est elle-même découpée en plusieurs ALU qui traitent chacune un petit nombre de bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul plus grosses à partir d’unités de calcul plus élémentaires s'appelle en jargon technique du bit slicing.
Cette technique est utilisée pour des ALU capables de gérer les opérations bit à bit, l'addition, la soustraction, mais guère plus. Il n'y a pas, à ma connaissance, d'ALU en bit-slicing capable d'effectuer une multiplication ou une division. La raison est qu'il n'est pas facile d'implémenter une multiplication entre deux nombres de 16 bits avec deux multiplieurs de 4 bits (idem pour la division). Alors que c'est plus simple pour l'addition et la soustraction : il suffit de transmettre la retenue d'une ALU à la suivante. Bien sûr, les performances seront alors nettement moindre qu'avec des additionneurs modernes, à anticipation de retenue, mais ce n'était pas un problème pour l'époque.
Nous allons d'ailleurs voir une ALU de ce type dans ce qui suit.
Un exemple d'unité de calcul : le 74181Modifier
Afin d'illustrer ce qui a été dit plus haut, nous allons étudier un exemple d'unité de calcul. Nous avons choisit l'unité de calcul 74181, très souvent utilisée dans les autres cours d'architecture des ordinateurs pour son aspect pédagogique indéniable. Il s'agit d'une unité de calcul commercialisée dans les années 60, à une époque où le microprocesseur n'existait pas. Les processeurs de l'époque étaient conçus à partir de pièces détachées assemblées et connectées les unes aux autres. Les pièces détachées en question étaient des des boitiers qui contenaient des registres, l'unité de calcul, des compteurs, des PLA, qu'on assemblait sur une carte électronique pour faire le processeur. L'unité 74181 était une des toutes premières unités de calcul fournie dans un boitier, là où il fallait auparavant fabriquer une ALU à partir de circuits plus simples comme des additionneurs ou des circuits d’opérations bit à bit.
Le 74181 était une unité de calcul de 4 bits, ce qui veut dire qu'il était capable de faire des opérations arithmétiques sur des nombres entiers codés sur 4 bits. Il prenait en entrée deux nombres de 4 bits, et fournissait un résultat de 4 bits. Il était possible de faire du bit-slicing, à savoir de combiner plusieurs 74181 afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Le 74181 était spécifiquement conçu pour, du moins pour l'addition. Pour cela, le 74181 gérait un bit de retenue en entrée et fournissait une sortie pour la retenu du résultat.
Pour comprendre ce qu'est ce bit de retenue, rappelons qu'une addition en binaire s'effectue comme en décimal : on additionne les bits colonne par colonne pour obtenir le bit de résultat, et il arrive qu'une retenue soit propagée à la colonne suivante. Si on combine deux 74181 de 4 bits, le premier va calculer l'addition des 4 bits de poids faible, alors que le second calcule l'addition des 4 bits de poids fort. Mais il faut propager la retenue de l'addition des 4 bits de poids faible au second 74181. Pour cela, le 74181 fournit cette retenue sur la sortie de retenue, l'autre réceptionne celle-ci sur son entrée de retenue. La retenue passée en entrée est automatiquement prise en compte lors d'une addition par le 74181. La gestion de la retenue est une fonctionnalité appréciable, surtout pour l'époque, qui est en plus très simple à implémenter. Comme nous l'avons vu dans le chapitre dédié aux circuits de calculs, ajouter une entrée de retenue ne coute rien et est très simple à implémenter en à peine quelques portes logiques.
Les opérations de base du 74181 comprennent l'addition et 16 opérations dites bit à bit. Il peut fonctionner selon deux modes. Dans le premier mode, il effectue une opération bit à bit seule. Dans le second mode, il effectue une opération bit à bit entre les deux nombres d'entrée A et B, additionne le nombre A au résultat, et additionne la retenue d'entrée. Pour résumer, il effectue une opération bit à bit et une addition facultative. Le fait de faire une opération bit à bit avant l'addition permet d'émuler une soustraction. Rappelons qu'une soustraction entre deux nombres A et B s'obtient en inversant les bits de B, en additionnant et en ajoutant 1. Or, il existe une opération bit à bit qui inverse tous les bits d'un nombre et celle-ci est supportée par le 74181. Ajouter 1 se fait en envoyant une retenue égale à 1 sur l'entrée de retenue. En tout, le 74181 était capable de réaliser 32 opérations différentes : les 16 opérations bit à bit seules (le maximum d'opérations de ce type possibles entre deux bits), et 16 autres opérations obtenues en combinant les 16 opérations bit à bit avec une addition.
L'entrée de sélection de l'instruction fait 5 bits, ce qui colle parfaitement avec les 32 instructions possibles. Les 5 bits en question sont séparés en deux : un groupe de 4 bits qui précise l'opération bit à bit, et un bit isolé qui indique s'il faut faire l'addition ou non. L'opération bit à bit à effectuer, est précisée par 4 bits d'entrée notés s0, s1, s2 et s3. L'activation de l'addition se fait par un bit d'entrée, le bit M, qui précise s'il faut faire ou non l'addition.
Le schéma du circuit est reproduit ci-dessous. Un oeil entrainé peut voir du premier coup d’œil que l'additionneur utilisé est un additionneur à anticipation de retenue modifié. Pour comprendre en quoi il est modifié, faisons quelques rappels. Vous savez qu'un additionneur à anticipation de retenue est composée de trois sous-circuits : une qui calcule les signaux P et G pour chaque chaque colonne (qui indiquent si l'addition des deux bits Génère ou Propage une retenue), un circuit qui calcule la retenue finale pour chaque colonne, puis un circuit qui additionne les opérandes avec les retenues calculées par les deux couches précédentes. Ici, le circuit qui calcule les signaux P et G est fusionné avec le circuit de calcul des opérations bit à bit. Il s'agit de la première couche dans le schéma ci-dessous. La seconde couche est composé du reste de l'additionneur, à savoir du circuit d'addition et de génération des retenues finales.
L'unité de calcul 74181 n'était pas fabriquée avec des transistors CMOS. À l'époque, ce type de transistor n'était pas utilisé : ils n'étaient pas assez performants comparés aux transistors dit TTL. Aujourd'hui, la situation s'est inversée et les transistors TTL sont tombés en désuétude. Le 74181 comprend 75 portes logiques. Ce nombre est à relativiser, car l’implémentation utilisait des optimisations qui fusionnaient plusieurs portes entre elles en utilisant des montages à transistor TTL assez subtils. On pouvait par exemple implémenter une porte AND-OR-NOT, identique à une porte ET suivie d'une porte NOR, avec un seul montage.
Pour ceux qui veulent en savoir plus sur cette unité de calcul et n'ont pas peur de lire une analyse des transistors TTL de la puce, voici deux articles très intéressant sur cette ALU :
- Inside the vintage 74181 ALU chip: how it works and why it's so strange
- Inside the 74181 ALU chip: die photos and reverse engineering
Les unités de calcul spécialiséesModifier
Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Cette séparation est très utile pour certaines opérations compliquées à insérer dans une unité de calcul normale.
Presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la floating-point unit, aussi appelée FPU. Néanmoins, ce regroupement des circuits pour nombres flottants n'est pas aussi strict qu'on pourrait le croire. Dans certains cas, les circuits capables d'effectuer les divisions flottantes sont séparés des autres circuits (c'est le cas dans la majorité des PC modernes) : tout dépend de l'architecture interne du processeur utilisé. Autrefois, ces FPU n'étaient pas incorporés dans le processeur, mais étaient regroupés dans un processeur séparé du processeur principal de la machine, appelé le coprocesseur arithmétique. Un emplacement dans la carte mère était réservé au coprocesseur. Ils étaient très chers et relativement peu utilisés, ce qui fait que seules certaines applications assez rares étaient capables d'en tirer profit : des logiciels de conception assistée par ordinateur, par exemple.
De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, des instructions de test et des branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. les registres à prédicats sont situés juste en sortie de cette unité de calcul.
Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc.
Les anciens processeurs avaient un circuit incrémenteur séparé de l'unité de calcul. C'est le cas sur l'Intel 8085, le Z-80, et bien d'autres processeurs 8 bits. L'incrémenteur peut très traiter des nombres plus grands que l'ALU. Par exemple, c'est le cas sur le Z-80, où l'incrémenteur peut manipuler des nombres de 16 bits, alors que l'ALU ne peut gérer que des nombres de 8 bits. D'autres processeurs ont la même chose. Une des raisons à cela est que l'incrémenteur est utilisé pour manipuler des entiers spécifiques, comme des adresses ou des indices de boucles. Incrémenter des adresses est très utile, que ce soit pour manipuler des tableaux, le pointeur de pile, voire le program counter. Et si l'incrémenteur est utilisé pour des adresses, elles n'ont pas forcément la même taille que les entiers, du moins sur certaines anciennes architectures.
Les registresModifier
Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthode pour sélectionner ou préciser un registre. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat. Pour cela, la quasi-totalité des processeurs actuels nomment les registres, le nom du registre permettant de l'identifier. Mais d'autres registres ne sont pas nommés et sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.
Les registres nommés et numérotésModifier
Les registres qui sont numérotés avec un nom de registre sont rassemblés dans une RAM, dont chaque byte est un registre. Celle-ci porte le nom de banc de registres (register file).Sur la plupart des architectures modernes, le banc de registres est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture.
Sur les processeurs simples, anciens, le banc de registre n'a qu'un seul port, qui sert à la fois pour la lecture et l'écriture. La raison est que ces processeurs sont des architectures à accumulateur, où on lit une opérande depuis les registres, l'autre opérande étant dans l'accumulateur, le résultat étant lui aussi envoyé à l'accumulateur. L'accumulateur fait partie de ces registres qui ne sont pas dans le banc de registres, ce qui fait qu'il n'y a donc qu'un seul accès au banc de registre par opération. D'où le fait que le banc de registre n'ait qu'un seul port.
Mais sur les architectures LOAD-STORE où à registres, le banc de registres est souvent une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que instructions ont alors besoin de lire deux données et d'enregistrer leur résultat dans des registres. Le tout se marie bien avec un banc de registre à trois port : deux de lecture (pour les deux opérandes), un d'écriture (pour le résultat). Mais l'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.
Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :
Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :
Les bancs de registres séparésModifier
Intuitivement, on s'attend à ce que tous les registres du processeur soient mis dans un seul banc de registre, afin de simplifier la conception du processeur. Mais il est des situations où ce n'est pas une bonne idée et où l'utilisation de plusieurs bancs de registres est plus simple et intuitive. C'est le cas sur les architectures dont les registres sont spécialisés.
L'exemple type est celui où on a des registres qui ne peuvent contenir qu'un type bien défini de donnée, avec par exemple des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registre pour chaque type de registre. Beaucoup d'architectures ont un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre généraux. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs. Il y a la même chose sur les processeurs où les adresses sont séparées des nombres entiers : les deux n'ont pas la même taille, ce qui rend l'usage de deux bancs de registres adéquat.
L'usage d'un banc de registre spécialisé est aussi une bonne idée sur les processeurs où la pile d'appel est mémorisée dans les registres. Nous avons vu il y a quelques chapitres que certains processeurs ne placent pas la pile d'appel en mémoire RAM, mais dans les registres. Ou alors, ils disposent d'une copie du sommet de la pile d'appel, afin d'améliorer les performances. Dans les deux cas, les registres de la pile d'appel sont placés dans un ban de registres séparés des registres de données. Un bon exemple est celui de l'Intel 4004, qui gère une pile de trois niveaux, directement dans ses registres, dans un banc de registres séparé. Chose étonnante, ce banc de registres contient aussi le program counter, mais c'est là un détail saugrenu et non une règle.
Enfin, un dernier cas un peu particulier est celui des registres à prédicats sur les architectures modernes. Pour rappel, ceux-ci sont des registres de 1 bit qui mémorisent les résultat des comparaisons et instructions de test. Ils sont généralement placés à part, dans un banc de registres séparé. Et non seulement ils ont leur propre banc de registres, mais celui-ci est relié à une unité de calcul spécialisée dans les conditions/instructions de test. L'unité de calcul peut écrire dans ce banc de registres à volonté, le séquenceur lit ces registres à chaque cycle. Parfois, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux, par exemple. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats, ce qui n'est possible qu'avec cette voie en lecture.
Mais rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux non-spécialisés. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés. Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.
Les bancs de registres unifiésModifier
Certains processeurs utilisent un seul gros banc de registres dit unifié, même avec des registres spécialisés. Tous les registres, qu'ils soient entier, flottant, pour les adresses ou autres, sont alors placés dans un seul banc de registre. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être le même : le séquenceur doit ajouter des bits au nom de registre pour former l'adresse finale.
L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.
Les bancs de registres dupliquésModifier
Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. Sur certains processeurs il s'agit d'une technique qui va de pair avec l'usage d'optimisations particulières, comme le pipeline, l’exécution dans le désordre, le renommage de registres, etc. Nous ne reparlerons dans les chapitres de la fin du cours, brièvement, dans le chapitre sur la gestion des exceptions sur les processeurs pipelinés. Mais dans d'autres cas, le banc de registre est dupliqué en deux exemplaires qui contiennent exactement les mêmes données, pour des raisons d'optimisation.
Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. La raison à cela est que ces processeurs disposent de plusieurs unités de calcul, capables de fonctionner en parallèle. Il est possible de lancer plusieurs calculs en même temps, un par ALU. Avec un seul banc de registre, il devrait être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage serait tout simplement ignoble. Aussi, pour simplifier le processeur, le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'unité de calcul. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifié, que ce soit au niveau du câblage, que de la conception des bancs de registres. Plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Dupliquer un banc de registre est donc un compromis entre le nombre de ports du banc de registres (et donc la performance du processeur), la simplicité de conception et la chaleur dégagée par le processeur.
Les registres adressés implicitement : le registre d'état, l'accumulateur, ...Modifier
De nombreux registres n'ont ni nom de registre, ni adresse, et sont sélectionnés implicitement par certaines instructions : program counter, registre d'état, etc. Sur la plupart des processeurs, ils ne sont pas rassemblés avec les autres registres, et sont en-dehors du banc de registre. Ils sont souvent intégrés dans des circuits spécialisés ou mis à part des autres registres. Ils sont souvent reliés au chemin de données directement. Ce n'est toutefois pas systématique : on peut placer ces registres dans un banc de registre, mais c'est rarement utilisé. Dans ce cas, on doit jouer un peu sur les noms de registre avant de les envoyer sur les entrées d'adresse du banc de registre. Rien de bien méchant. Les deux solutions sont viables, comme nous le verrons plus bas.
Le cas de l'accumulateurModifier
Il existe quelques cas de registres qui ne sont jamais mis dans le banc de registre. Mais ces cas sont très rares. Même des registres aussi importants que le program counter peuvent se mettre dans le banc de registre, mais nous verrons cela dans le chapitre suivant, où une section entière sera dédiée à ça.
On peut citer le cas du registre accumulateur, présent sur les architectures à accumulateur, qui est un simple registre relié à l'unité de calcul de cette façon. Nous verrons plus bas que le registre d'état est aussi dans ce cas.
Le cas du registre d'étatModifier
Le registre d'état fait aussi partie de ces rares registres qui font bande à part et ne sont pas placés dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/flags provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est donc connectée à l'unité de calcul, mais sa sortie est reliée au séquenceur ou au bus interne au processeur. Sa sortie est reliée au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non, et donc s'il faut faire ou non le branchement. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.
S'il est techniquement possible de mettre le registre d'état dans le banc de registre, ce serait se compliquer la vie pour rien. Cela n'aurait aucun avantage et complexifierait l’implémentation des instructions arithmétiques, qui devraient alors modifier faire deux écritures dans le banc de registre : un pour le registre de destination, et une autre pour le registre d'état. On pourrait implémenter cela en utilisant un banc de registre à deux ports, mais ce serait gâcher beaucoup de circuits pour pas grand chose, le second port n'ayant pas l'occasion de servir pour d'autres instructions, sauf éventuellement pour certaines instructions de multiplication. On pourrait aussi penser faire les deux écritures l'une après l'autre, mais cela demanderait de rajouter un registre en plus, ce qu'on cherche à éviter.
Le cas du pointeur de pileModifier
Il n'est pas rare que certains processeurs aient des registres spécialisés pour le pointeur de pile et le frame pointer. Il est possible de mettre ces registres à part, en -dehors du banc de registre, afin de les séparer des nombres entiers. Il faut dire que ces registres contiennent une adresse, pas un entier, et qu'il vaut mieux éviter de mélanger les deux. L'avantage est que le pointeur de pile est censé être adressé implicitement. Si tout le banc de registres est utilisé pour des registres entiers, on ne gâche pas un registre adressable pour un pointeur de pile qui n'est pas adressable. Pour le dire autrement si on a un banc de registre de 16 registres, on a bien 16 registres adressables. L'inconvénient est dans la mise à jour du pointeur de pile, comme on le verra dans le paragraphe suivant.
Mais d'autres processeurs placent le pointeur de pile dans le même banc de registre que les nombres entiers. C'est le cas sur le Z-80, par exemple, où le pointeur de pile est dans le même banc de registre que les registres entiers. le désavantage est qu'on perd un registre adressable; Avec un banc de registres de 16 registres, on ne peut plus en adresser que 15, le dernier étant pour un pointeur de pile non-adressable (en théorie). Mais l'avantage est que l'implémentation du processeur est plus simple. On n'a pas à ajouter un registre séparé du banc de registre, le chemin de données est plus simple et le séquenceur peut gérer la situation assez simplement. De plus, les opérations réalisées sur le pointeur de pile sont de simples additions et soustractions, réalisées par l'ALU. Or, le banc de registre est déjà connecté à l'ALU, ce qui facilite l'implémentation des instructions de gestion de la pile, comme PUSH et POP. Si on utilisait un registre séparé pour le pointeur de pile, il faudrait trouver un moyen de le relier à l'ALU, voire d'ajouter une ALU spécialement dédiée au pointeur de pile. Les deux solutions ajoute des circuits, complexifient le chemin de données et le séquenceur.
L'interface de communication avec la mémoireModifier
L'unité de communication avec la mémoire possède une entrée pour spécifier l'adresse à lire ou écrire, une entrée pour indiquer si l'accès est une lecture ou une écriture, et au moins une entrée/sortie connectée au bus de données.
Sur certains processeurs, l'unité de communication avec la mémoire gère les mémoires multiport.
Certaines unités de communication avec la mémoire peuvent gérer certains modes d'adressage elle-mêmes, et faire des calculs d'adresse à partir d'un indice ou d'un décalage. Pour cela, l'unité de communication avec la mémoire contient une unité de calcul interne, l'Address generation unit, ou AGU.
Pour simplifier la conception du processeur, le bus mémoire est parfois relié à des registres d’interfaçage mémoire, intercalés entre le bus mémoire et le chemin de données. Au lieu d'aller lire ou écrire directement sur le bus, on va lire ou écrire dans ces registres spécialisés. Généralement, on trouve deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.
Le bus interne au processeurModifier
Pour échanger des informations entre les composants du chemin de données, on utilise un ou plusieurs bus internes. Toute micro-instruction configure ce bus, configuration qui est commandée par le séquenceur. Chaque composant du chemin de données est relié au bus via des transistors, qui servent d'interrupteurs. Pour connecter le banc de registres ou l'unité de calcul à ce bus, il suffit de fermer les bons interrupteurs et d'ouvrir les autres.
Les micro-architecture à un seul bus, à accumulateur interneModifier
Dans le cas le plus simple, le processeur utilise un seul bus interne. Sur ces processeurs, l’exécution d'une instruction dyadique (deux opérandes) peut prendre plusieurs étapes. Si les deux opérandes sont dans les registres, il faut relier l'ALU à ces deux registres, ce qui est impossible avec un seul bus. Pour résoudre ce problème, on doit utiliser un registre spécial pour mettre en attente un opérande pendant qu'on charge l'autre. De même, il est préférable d'utiliser un registre temporaire pour le résultat, pour les mêmes raisons. Le déroulement d'une addition est donc simple : il faut recopier la première opérande dans le registre temporaire, connecter le registre contenant la deuxième opérande sur l’unité de calcul, lancer l’addition, et recopier le résultat du registre temporaire dans le banc de registres.
Sur les architectures à accumulateur, ce registre temporaire n'est autre que l'accumulateur lui-même.
Les exemples qui vont suivre montrent cependant que d'autres registres temporaires peuvent être ajoutés, pour les autres opérandes.
Cette organisation se marie bien avec un banc de registre à un seul port. Cette organisation fait que l'on doit lire les opérandes unes par unes, pour les stocker dans les registres temporaires, avant d'effectuer une instruction dessus. La présence d'un seul bus fait de toute façon qu'on ne peut connecter qu'un seul port dessus.
Les micro-architectures à plusieurs busModifier
Certains processeurs s'arrangent pour relier les composants du chemin de données en utilisant plusieurs bus, histoire de simplifier la conception du processeur ou d'améliorer ses performances. Le cas le plus simple est celui des architectures de type LOAD-STORE. Sur ces architectures, les accès mémoire se font avec une instruction de lecture, une instruction d'écriture, et éventuellement une instruction de copie entre registres.
Il faut aussi ajouter de quoi effectuer une lecture, ce qui demande de relier le bus mémoire sur l'entrée d'écriture du banc de registres. L'écriture demande de faire l'inverse : de connecter la sortie de lecture du banc de registre vers le bus mémoire. Enfin, les opérations arithmétiques demandent de lire deux opérandes depuis deux sorties de lecture, de les envoyer à l'unité de calcul, puis de connecter la sortie de l'ALU (donc le résultat de l'opération) sur l'entrée d'écriture du banc de registres. Avec quelques multiplexeurs, on arrive à s'en sortie. Voici ce que cela donne :
Mais on peut faire légèrement mieux sur un point : les instructions de copie entre registres. Elles existent sur la plupart des architectures LOAD-STORE, mais cela ne signifie pas que l'on doit modifier le chemin de données pour cela. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU ne fait rien et recopie simplement une de ses opérandes sur sa sortie. Ou alors elle effectue une instruction logique qui a pour résultat sa première opérande, comme un OU entre un registre et lui-même. Un autre solution modifie le chemin de données pour implémenter les copies entre registres. Pour cela, on doit pouvoir de relier les deux registres directement, ce qui demande de boucler l'entrée du banc de registres sur son entrée.
N'oublions pas que l'ALU consomme deux opérandes par opération, du moins pour la plupart des opérations logiques et arithmétiques. Si on omet la voie pour les transferts entre registres, et qu'on considère qu'ils passent par l'ALU, cela donne ceci :
Nous n'avons pas représenté les connexions avec le séquenceur, mais elles existent. Notamment, le séquenceur doit configurer le banc de registres, et l'unité de calcul. Pour les connexions avec le banc de registre, cela inclus le fait d'envoyer les noms de registres adéquats au banc de registre. Cela permet de gérer l'adressage inhérent, où les opérandes sont précisées par des noms de registre.
Le schéma précédent permet d'implémenter la plupart des modes d'adressage présents sur les architectures LOAD-STORE, mais pas tous. La raison principale est que ce bus ne contient aucune connexion avec le bus d'adresse. Impossible donc de préciser l'adresse à lire ou écrire, impossible de placer une adresse sur le bus d'adresse. Il faut donc rajouter des connexions avec le bus d'adresse, ce qui permet d'implémenter pas mal de modes d'adressage utiles. Ensuite, les modes d'adressage immédiat et directs ne sont pas supportés. Nous rappellerons ce que font ces modes d'adressage en temps voulu, mais nous pouvons expliquer pourquoi ils ne sont pas implémentés avec l'organisation précédente. Les modes d'adressage immédiat et directs incorporent une adresse ou une constante dans l'instruction elle-même. Il faut donc les extraire de l'instruction, pour placer le tout sur le bus interne du processeur. Cela demande que le séquenceur fasse l'extraction.
Les modes d'adressage qui impliquent des adressesModifier
Divers modes d'adressages demandent de placer une adresse sur le bus d'adresse. Les implémenter demande donc d'ajouter le bus d'adresse dans le schéma précédent, et de le connecter aux bons composants. Pour simplifier les schémas, nous allons omettre le cas où les copies entre registres passent par l'ALU, afin d'enlever une voie de transfert possible. Mais nous allons supposer que cette voie existe, et qu'elle est implémentée en ajoutant quelques multiplexeurs et démultiplexeurs.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre. Pour rappel, l'adressage indirect à registre correspond au cas où un registre contient une adresse à lire/écrire. L'implémenter demande donc de connecter la sortie des registres au bus d'adresse. Notons que le bus de données est utilisables pour effectuer une lecture ou une écriture.
L'adressage direct est celui où une instruction contient une adresse. Pour cela, le séquenceur doit extraire l'adresse à lire/écrire et l'envoyer sur le bus d'adresse.
Le mode d'adressage base+index est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Dans ce cas, on doit connecter la sortie de l'éunité de calcul au bus d'adresser.
L'adressage immédiatModifier
Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. il faut donc extraite cette constante de l'instruction et la placer au bon endroit dans le bus interne du processeur. La constante est extraite et fournie par le séquenceur. L'implémentation dépend de si on parle d'une instruction arithmétique ou d'une instruction MOV qui copie une constante dans un registre. Les deux situations ne sont en effet pas identiques, car il faut insérer la constante à un endroit différent dans les deux cas.
Pour les opérations arithmétiques ou une opérande est transmise par adressage immédiat, placer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors être convertie en un nombre de même taille que l'entrée de l'ALU. Pour effectuer cette extension de signe, on peut implanter un circuit spécialisé.
Passons maintenant à l'instruction MOV qui copie une constante dans un registre et/ou une adresse mémoire. Dans ce cas, la constante doit être envoyée sur l'entrée d'écriture du banc de registre.
Les simplifications possibles des chemins de données précédentsModifier
les chemins de données précédents sont assez complexes. Mais il existe un moyen de fortement les simplifier. Pour cela, il faut juste que l'unité de calcul soit capable d'effectuer une opération NOP, c'est à dire une opération qui ne fait rien et recopie la première opérande sur sa sortie. En faisant cela, le chemin de données est fortement simplifié, car certaines connexions deviennent redondantes. le tout donne le chemin de données illustré ci-dessous.
Par exemple, prenons la voie qui relie les registres au bus d'adresse, pour le mode d'adressage indirect à registre : elle devient redondante avec la voie pour le mode d'adressage base+index. Les deux demandent de connecter le bus d'adresse sur le chemin de données, mais l'une doit le faire avant l'ALU et l'autre après. Mais si l'ALU supporte l'opération NOP, les deux peuvent passer par l'ALU, puis être redirigées vers le bus d'adresse. La seule différence est que l'ALU fera une opération NOP pour le mode d'adressage indirect à registre, et un calcul d'adresse pour le mode d'adressage base + index.
Pareil pour le mode d'adressage immédiat, qui peut être simplifié. Le mode d'adressage immédiat demande d'insérer la constante soit avant l'ALU pour une instruction arithmétique, soit en entrée des registres (après l'ALU et après le bus d'adresse) pour une copie inter-registres. Avec l'organisation ci-dessous, il suffit d'insérer la constante en avant de l'ALU. Si on veut faire une opération dessus, l'ALU sera configurée pour faire une opération. Mais si on veut juste copier un registre dans un autre, alors l'ALU est configurée pour faire un NOP.
Le mode d'adressage direct peut être traité de la même manière. La logique veut l'adresse sorte du séquenceur et soit envoyée au bus d'adresse. Mais, on a connecté la sortie de l'ALU au bus d'adresse pour gérer le mode d'adressage base+index, et on a connecté le séquenceur à l'entrée de l'ALU pour le mode d'adressage immédiat. Sachant cela, on peut faire comme suit : l'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses.
Au final, le chemin de données devient le suivant avec ces simplifications. En faisant cela, de nombreux modes d'adressage sont supportés.
La liste des modes d'adressage supportés se détermine assez facilement. Premièrement, étudions le cas où l'ALU ne fait rien, elle effectue un NOP. Dans ce cas, on peut relier le banc de registre à lui-même pour faire un MOV entre registres. On eut aussi relier le banc de registre en lecture au bus d'adresse, ce qui permet de faire de l'adressage indirect. Le séquenceur peut envoyer une adresse à l'ALU, qui passe alors sur le bus d'adresse. Ou le séquencuer peut envoyer une constante, qui traverse l'ALU et termine dans les registres. Deuxièmement, étudions le cas où l'ALU fonctionne. Dans ce cas, si la sortie de l'ALU est connectée aux registres, on gère les opérations arithmétiques entre registres, les opérations arithmétiques avec adressage immédiat. Mais si on connecte la sortie de l'ALU au bus d'adresse, on gère le mode d'adressage base+index (les deux opérandes sont lues dans les registres), mais aussi les deux modes d'adressage base+décalage et indexed absolute (les deux demandent une opérande en mode immédiat, une autre lue depuis les registres). Si l'ALU gère une opération d'incrémentation, on peut me^me gérer le mode d'adressage indirect à registre avec auto-incrément : il suffit de lire l'adresse dans les registres, incrémenter le tout, et envoyer le résultat sur le bus d'adresse.