Il est maintenant temps de voir l'unité de contrôle. Pour rappel, celle-ci s'occupe du chargement des instructions et de leur interprétation, leur décodage. Elle contient deux circuits :
Les deux circuits sont séparés, mais ils communiquent entre eux pour gérer les branchements, pour charger les instructions au bon moment, etc. L'unité de chargement contient le "program counter" et les circuits associés, ainsi que des circuits pour communiquer avec la mémoire. Les circuits connectés à la mémoire permettent non seulement d'envoyer le "program counter" sur le bus d'adresse, mais aussi de récupérer l'instruction chargée sur le bus de données. A l'opposé, le séquenceur ne communique pas avec la mémoire. Il recoit l'instruction chargée de la part du circuit de chargement, et communique avec le chemin de données.
L’étape de chargement.
L'étape de chargement (ou "fetch") est toujours décomposée en trois étapes :
À chaque chargement, le "program counter" est mis à jour afin de pointer sur la prochaine instruction à charger. Et la méthode pour ce faire dépend du processeur. Sur certains processeurs assez rares, chaque instruction précise l'adresse de la suivante, qui est incorporée dans l'instruction. Mais sur la majorité des processeurs, on profite du fait que les instructions sont placées dans l'ordre d’exécution dans la RAM. En faisant ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction courante au contenu du "program counter". L'adresse de l'instruction en cours est connue dans le "program counter", reste à en connaitre la longueur. Le calcul est simple quand les instructions ont toutes la même taille, mais certains processeurs ont des instructions de taille variable, ce qui complique le calcul.
Le registre d'instruction.
Sur certains processeurs, l'instruction chargée est parfois stockée dans un registre situé juste avant le séquenceur. Ce registre est appelé le registre d'instruction.
Les processeurs basés sur une architecture Harvard peuvent se passer de ce registre, vu que l'instruction reste disponible sur le bus des instructions pendant toute son exécution. Mais sur les architectures de type Von Neumann, le bus doit être libéré pour un éventuel accès mémoire pendant l'instruction. On doit donc charger l'instruction et accéder aux données en deux temps : on charge l'instruction d'abord, puis on accède aux données. Mais cela demande de mémoriser l'instruction en cours dans le processeur, pour libérer le bus, d'où l'existence du registre d'instruction.
Le "program counter".
Dans la quasi-totalité des processeurs modernes, le "program counter" est séparé des autres registres et est contrôlé par des circuits dédiés. Le "program counter" est un vulgaire compteur comme on en a vu dans les chapitres sur les circuits, ce qui fait qu'il passe automatiquement d'une adresse à la suivante. Sur les processeurs très anciens, le "program counter" était un simple incrémenteur, car ces processeurs arrivaient à caler leurs instructions sur un seul mot mémoire.
La mise à jour du "program counter" se fait par l'ALU ou par un incrémenteur dédié.
Sur les processeurs modernes, on n'utilise pas exactement un circuit compteur comme on en a vu dans les chapitres sur les circuits. A la place, le "program counter" est relié à un circuit qui calcule l'adresse de la prochaine instruction, circuit appelé le compteur ordinal. Le compteur ordinal contient un additionneur et quelques circuits annexes pour gérer les branchements. L'usage d'un compteur ordinal simplifie fortement l'architecture du processeur et la conception du séquenceur. C'est une méthode simple et facile à implémenter. De plus, avec elle, le séquenceur n'a pas à gérer la mise à jour du "program counter", sauf en cas de branchements. Un autre avantage est que le "program counter" peut être mis à jour pendant que l'unité de calcul effectue une opération arithmétique, ce qui améliore grandement les performances. Le seul désavantage est qu'elle demande d'ajouter des circuits, dont un additionneur
Mais sur certains processeurs, le calcul de la prochaine adresse est effectué par l'ALU. Après tout, celle-ci est capable d'effectuer une addition, ce qui l'opération demandée pour mettre à jour le "program counter". L'avantage de cette méthode est qu'elle économise des circuits : on économise un additionneur et quelques circuits annexes. Par contre, cela se paye au prix d'une augmentation de la complexité du séquenceur, qui doit maintenant gérer la mise à jour du "program counter". Un autre défaut est que la mise à, jour du "program counter" ne peut pas se faire en même temps qu'une opération arithmétique, ce qui réduit les performances. Cette méthode était utilisée sur certains processeurs assez anciens, où l'économie de circuit était primordiale. Il faut dire que les microprocesseurs de l'époque n'avaient qu'un nombre limité de transistors, la technologie des semi-conducteurs ne permettant pas de mettre beaucoup de transistors sur une puce électronique. Et un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Autant dire que les processeurs de l'époque gagnaient à utiliser cette technique.
Si on met à jour le "program counter" avec l'ALU, alors il est intéressant de le placer dans le banc de registres, de ne pas le séparer des registres pour les nombres entiers. Mettre le "program counter" dans le banc de registres n'est pas une obligation, c'est juste plus simple pour utiliser l'ALU. L'avantage est que la conception du processeur est la plus simple possible. Par contre, on perd un registre. Par exemple, si on a un banc de registre de 16 registres, on ne peut utiliser que 15 registres généraux. Non seulement on perd un registre, mais en plus ce n'est pas l'idéal pour le décodage des instructions et pour le séquenceur.
D'autres processeurs anciens utilisent l'ALU pour mettre à jour le "program counter", mais disposent bien d'un registre séparé pour le "program counter". Cette méthode est relativement simple à implémenter : il suffit de connecter/déconnecter le "program counter" du bus interne suivant les besoins. Le "program counter" est déconnecté pendant l’exécution d'une instruction, mais il est connecté au bus interne lors de sa mise à jour. C'est le séquenceur qui gère le tout. Outre sa simplicité, l'avantage de cette séparation est qu'elle est plus facile à gérer pour le séquenceur. De plus, on gagne un registre général/entier dans le banc de registre.
Pour résumer, il existe trois méthodes principales pour calculer l'adresse de la prochaine instruction, l'une étant une sorte d'intermédiaire entre les deux autres. Elles se distinguent sur deux points : le circuit qui fait le calcul, l'organisation des registres. L'implémentation du circuit de calcul est sujet à deux possibilités : soit le "program counter" a son propre additionneur, soit le "program counter" est mis à jour par l'ALU. Pour ce qui est de l'organisation des registres, soit le "program counter" est un registre séparé des autres, soit il est regroupé avec d'autres registres dans un banc de registre. Le fait de placer le "program counter" dans un banc de registre implique que celui-ci soit mis à jour par l'ALU. PAr contre, on peut avoir un "program counter" séparé des autres registres, mais qui est mis à jour par l'ALU. Au total, cela donne les trois solutions suivantes.
L'interaction entre le "program counter" et les autres registres.
À noter que placer le "program counter" dans le banc de registres marche bien dans le cas où on a un seul banc de registre banalisé, qui contient des registres généraux. Mais elle doit être adaptée sur les processeurs qui ont des registres séparés pour les adresses mémoires. Nous allons nous intéresser au pointeur de pile, qui a quelques ressemblances avec le "program counter". En premier lieu, le "program counter" et le pointeur de pile sont tous deux des registres contenant une adresse, ce qui fait qu'ils sont régulièrement connectés au bus d'adresse. Ensuite, ils sont incrémentés ou décrémentés à chaque fois qu'on les modifie. Pour rappel, mettre à jour le pointeur de pile demande d'effectuer une addition ou une soustraction, qui est réalisée soit par l'ALU généraliste, soit par une ALU spécialisée pour les adresses, soit par un simple incrémenteur séparé de l'ALU. Ces deux ressemblances peuvent être exploitées, dans le sens où certains circuits peuvent être partagés entre circuits pour la pile et "program counter".
Un premier cas est celui où le processeur dispose de registres spécialisés pour la gestion de la pile d'appel, placés en-dehors du banc de registres. Typiquement, un processeur de ce type a un registre pour le pointeur de pile, éventuellement un autre pour le "frame pointer". Généralement, de tels processeurs ont un incrémenteur séparé de l'ALU pour le pointeur de pile. Pour beaucoup de ces processeurs, le pointeur de pile est regroupé avec le "program counter" et les deux sont mis à jour par le même incrémenteur. C'est là une utilisation intelligente de l'incrémenteur, qui permet d'éliminer une redondance, même si cela impose quelques contraintes sur la taille des cadres de pile. Cette technique marche bien si les cadres de pile ont la même taille, s'ils sont de taille fixe. Typiquement, si un cadre de pile a la même taille qu'une instruction, cette solution peut être envisagée. Mais si les cadres de pile sont de taille variable, alors cette solution ne marche pas.
Un second cas est celui des processeurs avec un banc de registres et une ALU spécialisée pour les adresses. Il y a quelques chapitres, nous avons vu que certains processeurs ont des registres séparés pour les entiers et les adresses, ou utilisent des registres d'index séparés des registres entiers. Typiquement, le processeur incorpore alors un banc de registre pour les adresses/indices, et un autre banc de registres pour les entiers. De tels processeurs avaient souvent deux unités de calculs : une pour les calculs entiers, et un autre pour les calculs d'adresse. La seconde ALU était plus simple, et se résumait souvent à un vulgaire incrémenteur/décrémenteur. Sur de tels processeurs, le "program counter" était placé dans le banc de registre pour les adresses et était mis à jour par l'ALU spécialisée dans les calculs d'adresse. L'avantage est une économie de circuit : pas besoin de rajouter un troisième additionneur/incrémenteur. Après tout, le "program counter" est une adresse, et sa mise à jour est un calcul d'adresse comme un autre.
Enfin, il est des processeurs pour lesquels l'organisation est simplement déroutante. Le Z80 est un processeur dans ce style. Il dispose d'un circuit incrémenteur séparé de l'ALU, qui agit comme une ALU secondaire. Le circuit incrémenteur est utilisé pour mettre à jour le "program counter", mais aussi pour mettre à jour le pointeur de pile et pour le rafraichissement mémoire. De plus, il est utilisé pour les instructions INC et DEC qui incrémentent un registre de 16 bits. Pour être plus précis, le Z-80 a des registres de 8 bits, mais qui sont regroupés par paires. Certaines instructions manipulent des registres de 8 bits isolés, alors que d'autres, comme INC et DEC, prennent une paire de registres comme un registre de 16 bit. En clair, on a une ALU principale connectée uniquement aux registres entiers, accompagnée d'un incrémenteur relié à tous les registres (entier, pointeur de pile, "program counter", ...).
Au niveau du jeu d'instruction, le "program counter" semble regroupé avec les autres registres. Mais l'étude du silicium du processeur montre que ce n'est pas le cas. Le "program counter" est regroupé avec un registre utilisé pour le rafraichissement mémoire, vu que c'était le processeur qui s'en occupait à l'époque. Ce registre, nommé IR, mémorise la prochaine adresse mémoire à rafraichir. Les deux registres sont connectés au banc de registre avec un bus interne au processeur, ce qui permet de copier un registre dans le "program counter", afin d'effectuer un branchement indirect. Mais le bus est déconnecté en dehors d'un branchement indirect, ce qui fait que la mise à jour du "program counter" se fait en parallèle des calculs arithmétiques. On a donc deux bancs de registres : un avec les entiers et le pointeur de pile, un autre avec le "program counter" et un registre pour le rafraichissement mémoire. Ces deux derniers sont les principaux utilisateurs de l'incrémenteur, mais ils ne sont pas les seuls.
Quand est mis à jour le "program counter" ?
Le "program counter" est mis à jour quand il faut charger une nouvelle instruction. Reste qu'il faut savoir quand le mettre à jour. Incrémenter le "program counter" à intervalle régulier, par exemple à chaque cycle d’horloge, fonctionne sur les processeurs où toutes les instructions mettent le même temps pour s’exécuter, mais de tels processeurs sont très rares. Sur la quasi-totalité des processeurs, les instructions ne prennent pas le même nombre de cycle d'horloge pour s’exécuter. Le temps d’exécution d'une instruction varie selon l'instruction, certaines sont plus longues que d'autres. La mise à jour du "program counter" démarre donc quand l'instruction précédente a terminée de s’exécuter. Quelques instructions simples peuvent s’exécuter en une seule instruction machine, mais beaucoup ne sont pas dans ce cas. Une conséquence est que les instructions ont une durée d’exécution variable. Tout cela amène un premier problème : comment incrémenter le "program counter" avec des instructions avec des temps d’exécution variables ?
La réponse est que la mise à jour du "program counter" démarre donc quand l'instruction précédente a terminée de s’exécuter, plus précisément un cycle avant. C'est un cycle avant pour que l'instruction chargée soit disponible au prochain cycle pour le décodeur. Pour cela, le séquenceur doit déterminer quand l'instruction est terminée et prévenir le "program counter" au bon moment. Il faut donc une interaction entre séquenceur et circuit de chargement. Le circuit de chargement contient une entrée Enable, qui autorise la mise à jour du "program counter". Le séquenceur met à 1 cette entrée pour prévenir au "program counter" qu'il doit être incrémenté au cycle suivant ou lors du cycle courant. Cela permet de gérer simplement le cas des instructions multicycles. 
Toute la difficulté est alors reportée dans le séquenceur, qui doit déterminer la durée d'une instruction. Pour gérer la mise à jour du "program counter", le séquenceur doit déterminer la durée d'une instruction. Cela est simple pour les instructions arithmétiques et logiques, qui ont un nombre de cycles fixe, toujours le même. Une fois l'instruction identifiée par le séquenceur, il connait son nombre de cycles et peut programmer un compteur interne au séquenceur, qui compte le nombre de cycles de l'instruction, et fournit le signal Enable au "program counter". Mais cette technique ne marche pas vraiment pour les accès mémoire, dont la durée n'est pas connue à l'avance, surtout sur les processeurs avec des mémoires caches. Pour cela, le séquenceur détermine quand l'instruction mémoire est finie et prévient le "program counter" quand c'est le cas.
Le chargement des instructions de longueur variable.
Le chargement des instructions de longueur variable est assez compliqué. Le problème est que mettre à jour le "program counter" demande de connaitre la longueur de l'instruction chargée, pour l'ajouter au "program counter". Si la longueur d'une instruction est variable, on doit connaitre cette longueur d'une manière où d'une autre.
La plus simple consiste à indiquer la longueur de l'instruction dans une partie de l'opcode ou de la représentation en binaire de l'instruction.
Une autre solution consiste à charger l'instruction morceau par morceau, typiquement par blocs de 16 ou 32 bits. Ceux-ci sont alors accumulés les uns à la suite des autres dans le registre d'instruction, jusqu'à ce que le séquenceur reconstitue une instruction complète. Le seul défaut de cette approche, c'est qu'il faut détecter quand une instruction complète a été reconstituée. Une solution similaire permet de se passer d'un registre d'instruction, en transformant le séquenceur en circuit séquentiel. Les mots mémoire de l'instruction sont chargés à chaque cycle, faisant passer le séquenceur d'un état interne à un autre à chaque mot mémoire. Tant qu'il n'a pas reçu tous les mots mémoire de l'instruction, chaque mot mémoire non terminal le fera passer d'un état interne d'attente à un autre. S'il tombe sur le dernier mot mémoire d'une instruction, il rentre dans un état de décodage.
Et enfin, il existe une dernière solution, qui est celle qui est utilisée dans les processeurs haute performance de nos PC : charger un bloc de mots mémoire qu'on découpe en instructions, en déduisant leurs longueurs au fur et à mesure. Généralement, la taille de ce bloc est conçue pour être de la même longueur que l'instruction la plus longue du processeur. Ainsi, on est sûr de charger obligatoirement au moins une instruction complète, et peut-être même plusieurs. Cette solution marche parfaitement si les instructions sont alignées en mémoire (relisez le chapitre sur l'alignement et le boutisme si besoin). Par contre, elle pose problème avec des instructions à cheval entre deux blocs. Il se peut qu'on n'ait pas une instruction complète lorsque l'on arrive à la fin du bloc, mais seulement un morceau.
Dans ce cas, on doit décaler le morceau de bloc pour le mettre au bon endroit (au début du registre d'instruction), et charger le prochain bloc juste à côté. On a donc besoin d'un circuit qui décale tous les bits du registre d'instruction, couplé à un circuit qui décide où placer dans le registre d'instruction ce qui a été chargé, avec quelques circuits autour pour configurer les deux circuits précédents.
Le "CPU reset vector".
Au démarrage d'un ordinateur, le "program counter" est initialisé avec l'adresse de la première instruction. Pour déterminer l'adresse de démarrage, on a deux solutions : soit on utilise une adresse fixée une fois pour toutes, soit l'adresse peut être précisée par le programmeur en la plaçant en mémoire ROM.
Dans tous les cas, le processeur est câblé pour lire une adresse bien précise lors du "boot". Naturellement, cette adresse est appelée l'adresse de démarrage. En anglais, elle est appelée le CPU Reset vector. L'adresse de démarrage n'est pas toujours l'adresse 0 : les premières adresses peuvent être réservées pour la pile ou le vecteur d'interruptions. Les deux solutions évoquées plus haut interprètent différemment l'adresse de démarrage. C'est l'adresse de la première instruction avec la première solution, c'est un pointeur vers la première instruction dans l'autre.
Le séquenceur.
Pour rappel, le chemin de données est rempli de composants à configurer d'une certaine manière pour exécuter une instruction. Pour configurer le chemin de données, il faut envoyer les signaux de commande adéquats sur l'entrée de sélection de l'unité de calcul, les entrées du banc de registres, ou les circuits du bus du processeur. Lorsque les circuits du chemin de données reçoivent ces signaux de commande, ils sont conçus pour effectuer une action précise et déterminée. Mais l'instruction chargée depuis la mémoire ne précise pas les signaux de commande, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une série de signaux de commandes adéquats. C'est le rôle de l'unité de décodage d'instruction, une portion du séquenceur qui « décode » l'instruction. Le séquenceur traduit une instruction en suite de micro-opérations et émet les signaux de commande pour chaque micro-opération.
Mais la traduction en question n'est pas simple, pour une raison assez importante : les instructions sont décomposées en plusieurs étapes, appelées micro-instructions, chacune configurant le chemin de donnée d'une manière bien précise. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Dans le cas le plus simple, chaque instruction correspond à une micro-opération et la traduction est alors triviale : le circuit est un simple circuit combinatoire. Mais dès que ce n'est pas le cas, le séquenceur devient un circuit séquentiel avec toute la complexité que cela implique.
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é. Celui-ci est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, tout circuit combinatoire peut être remplacé par une mémoire ROM au contenu adéquat, les deux étant totalement équivalents. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. 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.
Si les instructions sont décodées par un circuit à base d'un assemblage de portes logiques et de registres, on parle de séquenceur câblé. La présence du registre s’explique par le fait qu'il faille exécuter une suite de micro-opérations pour effectuer une instruction machine. Le séquenceur a besoin de savoir à quelle micro-opération il en est, ce qui fait qu'il mémorise cette information dans un registre. En conséquence, les séquenceurs câblés sont des circuits séquentiels.
Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, son numéro : s'il en est à la première, seconde, troisième micro-opération, etc. En combinant cette information avec l'opcode, le séquenceur détermine quelle est la micro-opération à effectuer. Pour cela, un circuit combinatoire prend en entrée le compteur et l'opcode de l'instruction machine voulue, puis fournit en sortie la micro-opération adéquate. Le compteur est incrémenté à chaque cycle d'horloge, ou du moins à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM, par exemple).
La technique précédente fonctionne 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. Par 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 certaines opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opération 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ée, 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. 
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. Mais on peut aussi remplacer ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. Nous avions déjà dit dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat, cela peut être fait pour un séquenceur combinatoire. Le défaut principal de cette solution est que la mémoire ROM prend beaucoup de place et utilise beaucoup de circuits, de transistors. En comparaison, un circuit combinatoire proprement dit peut être simplifié et optimisé, de manière à utiliser peu de circuits. Une solution intermédiaire est d'utiliser un circuit dit PLA ("Programmable Logic Array").
Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides. Par contre, les séquenceurs câblés sont souvent utilisés sur les processeurs RISC, qui ont peu d'instructions, pour lesquels la complexité du séquenceur et le nombre de portes est assez faible et supportable. La complexité du séquenceur dépend de la complexité des instructions machine.
Les séquenceurs microcodés.
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. 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.
Les séquenceurs micro-codés sont plus simples à concevoir et cela simplifie beaucoup le travail des concepteurs de processeurs. L'usage du microcode a un autre avantage majeur : il permet d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie de sa simplicité de conception, un séquenceur microcodé a des inconvénients qui ne sont pas négligeables. Par exemple, ils utilisent plus de portes logiques que les séquenceurs câblés, vu qu'une ROM est un circuit gourmand en portes logique. De plus, un séquenceur micro-codé est plus lent qu'un séquenceur câblé, la raison principale étant qu'une mémoire ROM est bien plus lente qu'un circuit combinatoire fabriqué directement avec des portes logiques.
Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et complexe, avec beaucoup de modes d'adressages différents. Implémenter un grand nombre d'instruction avec un séquenceur câblé aurait été beaucoup trop compliqué, ce qui fait que les concepteurs de processeur préféraient utiliser un microcode. Entre une instruction émulée par une suite d'instructions machines, et la même instruction microcodée, les performances étaient généralement similaires. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, le microcode pouvant être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Pour résumer, les instructions microcodées n'étaient pas forcément plus rapides que leur équivalent logiciel, mais elles existent et cela suffisait sur les architectures CISC qui privilégiaient la taille du programme - la "code size". L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé.
Le "control store".
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.
L'implémentation interne du "control store" ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le "control store" contenait 512 bytes/microinstructions de 21 bits chacune. Le "control store" n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes.
Le "control store" a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le "control store" contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un "control store" de 128 bytes, ce qui laisse 8 bytes vides. La position des vides dans le "control store" dépend de la solution utilisé. On pourrait croire que les vides sont généralement placés à la fin du "control store", mais il est parfois préférable de disperser les vides dans le "control store", afin de simplifier les circuits adossés au microcode, que nous allons voir dans ce qui suit.
Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le "control store". C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles.
La première optimisation de ce genre consiste à partager des bouts de microcode entre instructions machines, sur le même principe que les fonctions/sous-programmes/routines logicielles. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage.
La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du "control store", dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de al version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. Une autre solution consiste à décoder certaines instructions simples sans passer par le microcode, ce qui donne les séquenceur hybrides dont nous parlerons dans la suite du chapitre.
Les circuits d’exécution du microcode.
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".
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.
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 faire des corrections de bugs ou de corriger des problèmes de sécurité assez tordus. Il est en effet fréquent que les processeurs soient sujets à des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans une documentation officielle assez imposante, preuve que ces bugs ne sont pas des exceptions d'une grande rareté. Si la plupart de ces bugs ne peuvent pas être corrigés, quelques bugs peuvent cependant se corriger avec des mises à jour du microcode interne au processeur. Les bugs en question peuvent être liés à des bugs dans le microcode lui-même, ou à des bugs situés ailleurs, mais qui peuvent être corrigés ou mitigés en bidouillant le microcode. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert.
Mais il existe des processeurs dont le microcode est facilement programmable, accessible par le programmeur, ce qui permet d'ajouter des instructions à la volée. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction. Cela permet de réduire le nombre d'instructions du programme, ce qui réduit la taille du code (la mémoire prise par le programme exécutable), mais facilite aussi la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un "control store" assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité pose des problèmes : si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui n'ont pas intérêt à utiliser des instructions microcodées, sous peine d'exécuter des instructions microcodées incorrectes. Les problèmes de compatibilité entre processeurs sont aussi légion (les programmes codés ainsi ne marchent que sur un seul processeur, pas les autres). Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Aussi, il n'est pas étonnant que les microcodes nus, facilement accessibles, sont très très rares. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastique intégrées au processeur (microcode fournit chiffré avec des clés connues seulement des fabricants de CPU, autres).
La mise à jour du microcode peut se faire de deux grandes manières, l'une étant permanente, l'autre étant temporaire. La mise à jour permanente du microcode signifie que le "control store" est une EEPROM ou une mémoire ROM reprogrammable. Mais c'est une solution rarement utilisée, car ces mémoires sont très difficiles à mettre en œuvre dans les processeurs. Le "control store" doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. Les mémoires "mask ROM", non-reprogrammables, rentrent clairement dans ce cadre car elles sont fabriquées avec les mêmes transistors que le reste du processeur. Mais ce n'est pas le cas des autres mémoires ROM basées sur des transistors à grille flottante, totalement différents des transistors normaux. Il faut donc trouver une autre solution.
L'autre solution permet de mettre à jour le "control store" temporairement. Pour cela, le "control store" est implémenté avec deux mémoires. Une mémoire ROM qui contient le microcode originel, et une mémoire RAM. Pour simplifier ls explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. La micro-RAM est utilisée lors du décodage des instructions. L'idée est de permettre de modifier le microcode dans la micro-RAM avec un microcode corrigé. Ce dernier est chargé dans le processeur, dans la micro-RAM, peu après l'allumage du processeur. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Des mises à jour du BIOS peuvent contenir un microcode corrigé, capable de corriger des bugs ou des failles de sécurité.
Les séquenceurs hybrides.
Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés et mélangent les deux. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. 
L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un circuit de prédécodage. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un "control store" entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés.
Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. L'Intel 8086 ne contient pas deux séquenceurs séparés, mais est organisé comme suit : un "control store" de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une "Group Decode ROM" qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée "Group Decode ROM" est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instruction qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode.
La gestion des branchements.
L'implémentation des branchements est assez facile, mais elle implique tout le séquenceur. 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é. Pour cela, on doit ajouter un circuit de détection des branchements. Celui-ci détecte si l'instruction exécutée est un branchement ou non. Mais ce circuit est situé dans le décodeur d'instruction, c’est-à-dire dans la partie du séquenceur qui est située après l'étape de chargement. Autant dire que l’exécution des branchements demande une coopération de l'étape de chargement et le décodeur d'instruction. Aussi, nous allons voir le cas des branchements maintenant, à part des sections sur l'étape de chargement et de décodage d’instruction.
L'implémentation des branchements en fonction de leur mode d'adressage.
Pour rappel, il existe plusieurs types de branchements, et l'implémentation de chaque type est légèrement différente. Au niveau de l'étape de chargement, on ne fait pas de différence entre branchements inconditionnels (toujours exécutés) et conditionnels (exécutés si une condition précise est remplie). Par contre, il faut tenir compte du mode d'adressage du branchement. Pour les modes d'adressage, on distingue quatre types de branchements : les branchements directs, indirects, relatifs et implicites. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le reste du séquenceur. Pour les branchements indirects, l'adresse de destination est dans un registre. Pour les branchements relatifs, il faut ajouter un décalage au "program counter". Enfin, les instructions de retour de fonction lisent l'adresse de destination depuis la pile. L'implémentation de ces quatre types de branchements est très différente. De plus, tout dépend de si le "program counter" est dans un compteur séparé du reste des registres, ou s'il est dans le banc de registre.
L’implémentation des branchements avec un "program counter" intégré au banc de registres.
Le cas le plus simple est clairement celui où le "program counter" est intégré au banc de registre. Dans ce cas, branchements indirects, relatifs et directs sont simples à implémenter et tout se passe dans le chemin de données. L'étape de chargement n'est en soi pas concernée par la mise à jour du "program counter", mais s'occupe juste de connecter celui-ci au bus d'adresse. Le calcul d'adresse est alors réalisé dans le chemin de données, branchement ou non.
Reste que les trois types de branchements s'implémentent différemment, dans le sens où le chemin de donnée est configuré différemment suivant le mode d'adressage.
Notons que sur certaines architectures, le "program counter" est non seulement dans le banc de registres, mais il est adressable au même titre que les registres de données. C'est étrange, mais cela permet de se passer d'instructions de branchement. Pour cela, il suffit d'adresser le "program counter" avec des instructions MOV ou des instructions arithmétique. Une simple instruction MOV reg -> "program counter" fait un branchement indirect, une instruction MOV constante -> "program counter" fait un branchement direct, et une instruction ADD constante "program counter" -> "program counter" fait un branchement relatif.
L’implémentation des branchements avec un "program counter" séparé, mis à jour par l'ALU.
Le second cas que nous allons voir est celui où le "program counter" est séparé du banc de registres, mais qu'il est incrémenté par l'ALU. Sur ce genre d'architectures, la gestion des branchements se fait d'une manière fortement similaire à ce qu'on a vu dans la section précédente. Là encore, l'ALU est utilisée pour incrémenter le "program counter" sans branchements, mais elle est aussi réutilisée pour les branchements relatifs. La seule différence est que le "program counter" est séparé du banc de registres.
L’implémentation des branchements avec un "program counter" séparé.
Étudions d'abord le cas où le "program counter" est dans un registre/compteur séparé. Rappelons que tout compteur digne de ce nom possède une entrée de réinitialisation, qui remet le compteur à zéro. De plus, on peut préciser à quelle valeur il doit être réinitialisé. Ici, la valeur à laquelle on veut réinitialiser le compteur n'est autre que l'adresse de destination du branchement. Implémenter un branchement est donc simple : l'entrée de réinitialisation est commandée par le circuit de détection des branchements, alors que la valeur de réinitialisation est envoyée sur l'entrée adéquate. En clair, nous partons du principe que le "program counter" est implémenté avec ce type de compteur :
Toute la difficulté est de présenter l'adresse de destination au "program counter". Pour les branchements directs, l'adresse de destination est fournie par le séquenceur. L'adresse de destination du branchement sort du séquenceur et est présentée au "program counter" sur l'entrée adéquate. Voici donc comment sont implémentés les branchements directs avec un "program counter" séparé du banc de registres. Le schéma ci-dessous marche peu importe que le "program counter" soit incrémenté par l'ALU ou par un additionneur dédié.
Les branchements relatifs sont ceux qui demandent de sauter X instructions plus loin dans le programme. En clair, ils ajoutent un décalage au "program counter". Leur implémentation demande d'ajouter une constante au "program counter", la constante étant fournie dans l’instruction. Pour prendre en compte les branchements relatifs, on a encore deux solutions : réutiliser l'ALU pour calculer l'adresse, ou rajouter un additionneur qui fait le calcul d'adresse. En théorie, l'additionneur ajouté peut être fusionné avec l'additionneur qui incrémente le "program counter" pour passer à l’instruction suivante, mais ce n'est pas toujours fait et on a parfois deux circuits séparés.
Pour les branchements indirects, il suffit de lire le registre voulu et d'envoyer le tout sur l'entrée adéquate du "program counter". Il faut alors rajouter un multiplexeur pour que l'entrée de réinitialisationr ecoive la bonne adresse de destination.
Toute la difficulté de l'implémentation des branchements est de configurer les multiplexeurs, ce qui est réalisé par le séquenceur en fonction du mode d'adressage du branchement.
L'implémentation des branchements conditionnels.
Les branchements inconditionnels s’exécutent systématiquement, alors que les branchements conditionnels 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 :
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.
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. Le circuit soustracteur soustrait alors 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 demande de calculer une condition et de sauter ou non à une certaine adresse en fonction du résultat de cette dernière. En soi, 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é consiste donc à mettre la bonne valeur sur l'entrée de réinitialisation. 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 : son résultat 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 le processeur fournit des instructions où 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 belle séparation entre l'unité de calcul pour les tests, le séquenceur et l'unité de chargement. Les trois ont des rôles bien précis : calculer le test pour l'ALU dédiée, décider s'il faut faire le saut ou non et modifier le "program counter" pour l'unité de chargement, générer les signaux de commande pour l'ALU et l'unité de chargement pour le séquenceur. 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. 
Le registre d'état ou les registres à prédicats et les circuits associés.
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.
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".
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.
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.
La "Prefetch input queue".
Sur certains processeurs, l'étage de chargement et le chemin de données sont séparés par une mémoire tampon, appelée la file d’instruction, aussi appelée "Prefetch input queue". On peut la voir comme un registre d'instruction sous stéroïde, capable de mémoriser plusieurs instructions consécutives. Elle se situe après l'unité de chargement, même si on peut en théorie la mettre après l'unité de décodage. Elle est à l'origine de nombreux avantages en termes de performance, surtout si le cache ou la mémoire RAM a un gros temps d'accès. Mais elle est surtout pertinente sur des processeurs particuliers, que nous verrons à la fin du cours. Pour les connaisseurs, c'est très utile si le processeur a un pipeline, ou d'autres techniques du genre comme l’exécution dans le désordre. Pour les processeurs simples, cette technique a un intérêt limité.
Le préchargement des instructions.
La "prefetch input queue" permet au processeur de charger des instructions à l'avance, si les conditions adéquates sont réunies. À chaque cycle d'horloge, l'étape de chargement charge une nouvelle instruction dans cette file d'instruction. Rappelons qu'une instruction peut prendre plusieurs cycles d'horloge pour s’exécuter, ce qui fait que des instructions sont donc préchargées à l'avance. Ce faisant, l'étape de chargement et de décodage n'ont pas à être synchronisées parfaitement. Les instructions y sont conservées dans leur ordre d'arrivée, afin qu'elles soient exécutées dans le bon ordre. La file d'instruction est donc une mémoire de type FIFO. L'étape de décodage pioche l'instruction à décoder dans la file d'instruction, cette instruction étant par définition la plus ancienne, puis la retire de la file. Le premier processeur commercial grand public à utiliser cette méthode était le 8086, un processeur d'Intel de jeu d'instruction x86. Il pouvait précharger à l'avance 6 octets d’instruction.
Le premier avantage de cette méthode est que l'étape de décodage peut prendre son temps pour décoder des instructions complexes, sans pour autant mettre en pause l'étape de chargement. L'utilité de la file d’instruction prend tout son sens si une instruction met plusieurs cycles d'horloge à être décodée. C'est le cas pour les instructions très complexes, notamment sur les décodeurs micro-codés. Sans file d'instruction, le décodage de ces instructions prendrait plusieurs cycles et stopperait l'étape de chargement. Un autre exemple est quand le processeur exécute une instruction très longue, qui prend plusieurs cycles d'horloge, ce qui bloque l’exécution de nouvelles opérations et potentiellement leur décodage. Mais avec la file d’instruction, les instructions suivantes sont chargées en avance et s'accumulent dans la file d'instruction. Le processeur n'a ainsi pas besoin d'attendre la mémoire pour charger une nouvelle instruction : tout a été chargé à l'avance.
Un autre avantage est que le processeur n'a pas à attendre la mémoire pour commencer le décodage des instructions, ce qui est un gros avantage si la mémoire est lente. Par exemple, prenons l'exemple d'un processeur qui exécute une instruction à chaque cycle d’horloge, chaque instruction étant codée sur 2 octets. On suppose que la mémoire est capable de charger 8 octets à chaque cycle. La mémoire doit garantir un débit constant de 2 octets par cycle, ce qui donne une belle marge. Le problème est que la mémoire peut être utilisée pour des accès mémoire concurrents, simultanés avec la lecture de l'instruction, pour lire des données ou autre. Si le processeur précharge 8 octets d'un coup, il peut lire les 4 instructions depuis la file d'instruction, sans accéder à la mémoire. La même chose est impossible avec un simple registre d'instruction, car il ne peut conserver qu'une instruction, pas plusieurs.
Une autre situation est celle où la mémoire a un temps d'accès important. Par exemple, supposons que le processeur puisse charger une instruction de 2 octets par cycle, mais que la mémoire puisse charger 8 octets en deux cycles. Les temps d'accès semblent incompatibles : 1 cycle pour le processeur, 2 pour la mémoire. Mais la différence de débit peut être utilisée pour précharger des instructions. Le processeur peut charger plus de 2 instructions à la fois, en un seul accès mémoire, puis les exécute alors l'une après l'autre, une par cycle d'horloge. Et quand la dernière instruction préchargée est exécutée, la mémoire est de nouveau libre, ce qui masque la latence des accès mémoire. L'exemple en question est certes un peu tordu, mais les processeurs modernes ne sont pas à l'abri de ce genre de problèmes. Rappelons que le temps d'accès d'un cache est assez long, surtout pour les niveaux de cache inférieurs.
La même méthode peut s'appliquer, mais entre le décodeur et le chemin de données. Il est possible de mettre une file d'attente entre le séquenceur et le chemin de donnée, qui contient des micro-opérations, des instructions décodées. Les micro-opérations peuvent s'accumuler dans cette mémoire, au cas où. Là encore, c'est très utile si l’exécution d'une instruction prend trop de temps. Au lieu de mettre en attente le séquenceur, celui-ci peut prendre de l'avance et charger/décoder les instructions suivantes à l'avance, pendant qu'une instruction multicycles s’exécute. Les instructions chargées à l'avance sont disponibles immédiatement, une fois que le processeur a fini de décoder l'instruction complexe. Cela évite de démarrer le chargement/décodage une fois que l'instruction est terminée, ce qui gâche un peu de temps. La file d'attente entre séquenceur/décodeur et chemin de données est appelé la fenêtre d'instruction, ou encore l"'instruction window" en anglais.
Les problèmes liés à la "Prefetch input queue".
Notons que cette méthode permet de charger à l'avance des instructions dont on ne sait pas si elles seront exécutées et que cela peut poser problème. Le problème en question tient dans les branchements. La file d'attente charge à l'avance plusieurs instructions. Mais si un branchement est chargé, toutes les instructions chargées après sont potentiellement invalides. Si le branchement est non-pris, les instructions chargées sont valides, elles sont censées s’exécuter. Mais si le branchement est pris, elles ont été chargées à tort et ne doivent pas s’exécuter. Pour éviter cela, la file d'attente est vidée quand le processeur détecte un branchement. Cette détection ne peut se faire qu'une fois le branchement décodé : on ne sait pas si l'instruction est un branchement ou autre chose tant que le décodage n'a pas eu lieu, par définition. En clair, le décodeur invalide la file d'attente et la vide quand il détecte un branchement.
Les interruptions posent le même genre de problèmes. Il faut impérativement vider la "Prefetch input queue" quand une interruption survient, avant de la traiter.
Un autre défaut est que la "Prefetch input queue" se marie assez mal avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. De nos jours, ces techniques peuvent être utilisées très rarement pour compresser un programme et/ou le rendre indétectable (très utile pour les virus informatiques). Le problème avec la "Prefetch input queue" survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans la file d'attente sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas est quelque peu complexe. Il faut en effet vider la "Prefetch input queue" si le cas arrive, ce qui demande d'identifier les écritures qui écrasent des instructions préchargées. C'est parfaitement possible, mais demande de transformer la "Prefetch input queue" en une mémoire hybride, à la fois mémoire associative et mémoire FIFO. Cela ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place, le code automodifiant est alors buggé.
La macro-fusion et la micro-fusion.
La "Prefetch input queue" permet d'ajouter des optimisations au processeur, qui ne seraient pas possibles sans elle. L'une d'entre elle est la macro-fusion, une technique qui permet de fusionner une suite d'instructions consécutives en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD ("multiply and add"), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Et enfin, il est possible de fusionner une instruction de test et une instruction de branchement en une seule micro-opération de comparaison-branchement.
La macro-fusion est faisable avant le décodage, quand les instructions sont encore dans la "Prefetch input queue". La fusion a lieu au moment du chargement d'une nouvelle instruction. Aussi, on ne devrait pas dire que les instructions machine sont fusionnées en micro-opération, ces dernières n'apparaissant qu'après l'étape de décodage. Mais l'idée est que deux instructions machines sont fusionnées en une seule, qui est décodée en une seule instruction machine. L'avantage de cette technique est que les décodeurs et le chemin de données sont utilisés plus efficacement.