Fonctionnement d'un ordinateur/Version imprimable 2

Circuit électronique.

Un ordinateur est un appareil électronique parmi tant d'autres. La conception de ces appareils est un domaine appelé l’électronique et les gens qui conçoivent ces appareils sont appelés des électroniciens. Tous les appareils électroniques contiennent plusieurs composants électroniques simples, qui sont placés sur un support plat, en plastique ou en céramique, appelé la carte électronique. Les composants sont soudés sur la carte, histoire qu’ils ne puisse pas s’en décrocher. Ils sont reliés entre eux par des fils conducteurs, le plus souvent du cuivre ou de l’aluminium, ce qui leur permet de s’échanger des données. Sur les cartes simples, ces fils sont intégrés dans la carte électroniques, dans des creux du plastique. Ils portent le nom de pistes. Évidemment, tous les composants ne sont pas tous reliés entre eux. Par exemple, si je prends un ordinateur, l’écran n’est pas relié au clavier.

Appareils programmables et non-programmables modifier

Les appareils simples sont non-programmables, ce qui veut dire qu’ils sont conçus pour une utilisation particulière et qu’ils ne peuvent pas faire autre chose. Par exemple, les circuits électroniques d’un lecteur de DVD ne peuvent pas être transformé en lecteur audio ou en console de jeux... Les circuits non-programmables sont câblés une bonne fois pour toute, et on ne peut pas les modifier. On peut parfois reconfigurer le circuit, pour faire varier certains paramètres, via des interrupteurs ou des boutons, mais cela s’arrête là. Et cela pose un problème : à chaque problème qu'on veut résoudre en utilisant un automate, on doit recréer un nouveau circuit.

À l'inverse, un ordinateur n’est pas conçu pour une utilisation particulière, contrairement aux autres objets techniques. Il est possible de modifier leur fonction du jour au lendemain, on peut lui faire faire ce qu’on veut. On dit qu'ils sont programmables. Pour cela, il suffit d’utiliser un logiciel, une application, un programme (ces termes sont synonymes), qui fait ce que l'on souhaite. La totalité des logiciels présents sur un ordinateur sont des programmes comme les autres, même le système d'exploitation (Windows, Linux, ...) ne fait pas exception.

Les programmes sont donnés au circuit/à l’ordinateur en passant par les périphériques. De nos jours, ils sont gravés sur des CD ou DVD ou téléchargés depuis Internet, et sont ensuite installés sur l’ordinateur. Sur d’anciens ordinateurs, les programmes étaient mis sur des disquettes, de petits supports magnétiques qu’on insérait dans l’ordinateur le temps de leur utilisation. Avant les disquettes, on utilisait d’autres supports, comme des cartes perforées en plastique, sur lesquelles on inscrivait le programme. Sur d’anciens ordinateur personnels, comme l’Amstrad ou le Commodore, on pouvait aussi taper les programmes à exécuter à la main, au clavier, avant d’appuyer une touche pour les exécuter. Sur Arduino, le programme est envoyé via le port USB de la carte, si on se débrouille convenablement.

Les composants d'un ordinateur modifier

Éclaté d'un ordinateur de type PC :
1 : Écran ;
2 : Carte mère ;
3 : Processeur ;
4 : Câble Parallel ATA ;
5 : Mémoire vive (RAM) ;
6 : Carte d'extension ;
7 : Alimentation électrique ;
8 : Lecteur de disque optique ;
9 : Disque dur ;
10 : Clavier ;
11 : Souris.

De nombreux appareils sont programmables, mais tous ne sont pas des ordinateurs. Par exemple, certains circuits programmables nommés FPGA n'en sont pas. Pour être qualifié d'ordinateur, un appareil programmable doit avoir d'autres propriétés. L'une d'entre elle est de contenir plusieurs composants séparés, aux fonctions bien distinctes. De l'extérieur, l'ordinateur est composé d'une unité centrale sur laquelle on branche des périphériques.

Les périphériques regroupent l'écran, la souris, le clavier, l'imprimante, et bien d'autres choses. Ils permettent à l'utilisateur d'interagir avec l'ordinateur : un clavier permet de saisir du texte sous dans un fichier, une souris enregistre des déplacements de la main en déplacement du curseur, un écran affiche des données d’images/vidéos, un haut-parleur émet du son, etc. Tout ce qui est branché sur un ordinateur est, formellement un périphérique.

L'unité centrale est là où se trouvent tous les composants importants d'un ordinateur, ceux qui font des calculs, qui exécutent des logiciels, qui mémorisent vos données, etc. Dans les ordinateurs portables, l'unité centrale est bien là, située sous le clavier ou dans l'écran (le plus souvent sous le clavier). À l'intérieur de l'unité centrale, on trouve trois composants principaux : le processeur, la mémoire et les entrée-sorties.

  • Le processeur traite les données, les modifie, les manipule. Pour faire simple, il s’agit d’une grosse calculatrice hyper-puissante. Il comprend à la fois un circuit qui fait des calculs, et un circuit de contrôle qui s'occupe de séquencer les calculs dans l'ordre demandé.
  • La mémoire vive conserve des informations/données temporairement, tant que le processeur en a besoin.
  • Les entrée-sorties permettent à l’ordinateur de communiquer avec l’extérieur. C'est sur celles-ci que l'on branche les périphériques.
  • La carte mère n'est autre que le circuit imprimé, la carte électronique, sur laquelle sont soudés les autres composants.

Outre ces composants dits principaux, un ordinateur peut comprendre plusieurs composants moins importants, surtout présents sur les ordinateurs personnels. Ils sont techniquement facultatifs, mais sont très présents dans les ordinateurs personnels. Cependant, certains ordinateurs spécialisés s'en passent. Les voici :

  • L'alimentation électrique convertit le courant de la prise électrique en un courant plus faible, utilisable par les autres composants.
  • Diverses cartes d'extension sont branchées sur la carte mère. Elles permettent d’accélérer certains calculs ou certaines applications, afin de décharger le processeur. Par exemple, la carte graphique s'occupe des calculs graphiques, qu'il s'agisse de graphismes 3D de jeux vidéos ou de l'affichage en 2D du bureau. Dans un autre registre, la carte son prend en charge le microphone et les haut-parleurs.
  • Les disques durs sont des mémoires de stockage, qui mémorisent vos données de manière permanente.
  • Les lecteurs CD-ROM ou DVD-ROM permettent de lire des CD ou des DVD.
  • Et ainsi de suite.

Résumé modifier

Pour résumer, les ordinateurs sont des appareils électroniques qui ont les propriétés suivantes.

  • Ils sont programmables.
  • Ils contiennent un processeur et une mémoire, reliés à des périphériques.
  • et surtout, ils utilisent un codage numérique, chose que nous allons aborder dans le chapitre suivant.

Ces points peuvent paraitre assez abstraits, mais rassurez-vous : nous allons les détailler tout au long de ce cours, au point qu'ils n'auront plus aucun secret pour vous d'ici quelques chapitres. Dans ce chapitre, nous allons détailler le premier point de la liste précédente, les autres étant laissés aux chapitres suivants.


Le codage des informations modifier

Vous savez déjà qu'un ordinateur permet de faire plein de choses totalement différentes : écouter de la musique, lire des films/vidéos, afficher ou écrire du texte, retoucher des images, créer des vidéos, jouer à des jeux vidéos, etc. Pour être plus général, on devrait dire qu'un ordinateur manipule des informations, sous la forme de fichier texte, de vidéo, d'image, de morceau de musique, de niveau de jeux vidéos, etc. Dans ce qui suit, nous allons appeler ces informations par le terme données. On pourrait définir les ordinateurs comme des appareils qui manipulent des données et/ou qui traitent de l'information, mais force est de constater que cette définition, oh combien fréquente, n'est pas la bonne. Tous les appareils électroniques manipulent des données, même ceux qui ne sont pas des ordinateurs proprement dit : les exemples des décodeurs TNT et autres lecteurs de DVD sont là pour nous le rappeler. Même si la définition d’ordinateur est assez floue et que plusieurs définitions concurrentes existent, il est évident que les ordinateurs se distinguent des autres appareils électroniques programmables sur plusieurs points. Notamment, ils stockent leurs données d'une certaine manière (le codage numérique que nous allons aborder).

Le codage de l'information modifier

Avant d'être traitée, une information doit être transformée en données exploitables par l'ordinateur, sans quoi il ne pourra pas en faire quoi que ce soit. Eh bien, sachez qu'elles sont stockées… avec des nombres. Toute donnée n'est qu'un ensemble de nombres structuré pour être compréhensible par l'ordinateur : on dit que les données sont codées par des nombres. Il suffit d'utiliser une machine à calculer pour manipuler ces nombres, et donc sur les données. Une simple machine à calculer devient une machine à traiter de l'information. Aussi bizarre que cela puisse paraitre, un ordinateur n'est qu'une sorte de grosse calculatrice hyper-performante. Mais comment faire la correspondance entre ces nombres et du son, du texte, ou toute autre forme d'information ? Et comment fait notre ordinateur pour stocker ces nombres et les manipuler ? Nous allons répondre à ces questions dans ce chapitre.

Toute information présente dans un ordinateur est décomposée en petites informations de base, chacune représentée par un nombre. Par exemple, le texte sera décomposé en caractères (des lettres, des chiffres, ou des symboles). Pareil pour les images, qui sont décomposées en pixels, eux-mêmes codés par un nombre. Même chose pour la vidéo, qui n'est rien d'autre qu'une suite d'images affichées à intervalles réguliers. La façon dont un morceau d'information (lettre ou pixel, par exemple) est représenté avec des nombres est définie par ce qu'on appelle un codage, parfois appelé improprement encodage. Ce codage va attribuer un nombre à chaque morceau d'information. Pour montrer à quoi peut ressembler un codage, on va prendre trois exemples : du texte, une image et du son.

Texte : standard ASCII modifier

Caractères ASCII imprimables.

Pour coder un texte, il suffit de savoir coder une lettre ou tout autre symbole présent dans un texte normal (on parle de caractères). Pour coder chaque caractère avec un nombre, il existe plusieurs codages : l'ASCII, l'Unicode, etc.

Le codage le plus ancien, appelé l'ASCII, a été inventé pour les communications télégraphiques et a été ensuite réutilisé dans l'informatique et l'électronique à de nombreuses occasions. Il est intégralement défini par une table de correspondance entre une lettre et le nombre associé, appelée la table ASCII. Le standard ASCII originel utilise des nombres codés sur 7 bits (et non 8 comme beaucoup le croient), ce qui permet de coder 128 symboles différents. Les lettres sont stockées dans l'ordre alphabétique, pour simplifier la vie des utilisateurs : des nombres consécutifs correspondent à des lettres consécutives. L'ASCII ne code pas seulement des lettres, mais aussi d'autres symboles, dont certains ne sont même pas affichables ! Cela peut paraitre bizarre, mais s'explique facilement quand on connait les origines du standard. Ces caractères non-affichables servent pour les imprimantes, FAX et autres systèmes de télécopies. Pour faciliter la conception de ces machines, on a placé dans cette table ASCII des symboles qui n'étaient pas destinés à être affichés, mais dont le but était de donner un ordre à l'imprimante/machine à écrire... On trouve ainsi des symboles de retour à la ligne, par exemple.

La table ASCII a cependant des limitations assez problématiques. Par exemple, vous remarquerez que les accents n'y sont pas, ce qui n'est pas étonnant quand on sait qu'il s'agit d'un standard américain. De même, impossible de coder un texte en grec ou en japonais : les idéogrammes et les lettres grecques ne sont pas dans la table ASCII. Pour combler ce manque, de nombreuses autres codages du texte sont apparus. Les premiers codages étendus se sont contentés d'étendre l'ASCII en rajoutant des caractères à la table ASCII de base. De tels codages sont appelés des codages ASCII étendus, ils sont assez nombreux et ne sont pas compatibles entre eux. Le plus connu et le plus utilisé est certainement le codage ISO 8859 et ses dérivés, utilisés par de nombreux systèmes d'exploitation et logiciels en occident. Ce codage code ses caractères sur 8 bits et est rétrocompatible ASCII, ce qui fait qu'il est parfois confondu avec ce dernier alors que les deux sont très différents. Aujourd'hui, le standard de codage de texte le plus connu est certainement l’Unicode. L'Unicode est parfaitement compatible avec la table ASCII : les 128 premiers symboles de l’Unicode sont ceux de la table ASCII, et sont rangés dans le même ordre. Là où l'ASCII ne code que l'alphabet anglais, les codages actuels comme l'Unicode prennent en compte les caractères chinois, japonais, grecs, etc.

Image modifier

Image matricielle.

Le même principe peut être appliqué aux images : l'image est décomposée en morceaux de même taille qu'on appelle des pixels. L'image est ainsi vue comme un rectangle de pixels, avec une largeur et une longueur. Le nombre de pixels en largeur et en longueur définit la résolution de l'image : par exemple, une image avec 800 pixels de longueur et 600 en largeur sera une image dont la résolution est de 800*600. Il va de soi que plus cette résolution est grande, plus l'image sera fine et précise. On peut d'ailleurs remarquer que les images en basse résolution ont souvent un aspect dit pixelisé, où les bords des objets sont en marche d'escaliers.

Chaque pixel a une couleur qui est codée par un ou plusieurs nombres entiers. D'ordinaire, la couleur d'un pixel est définie par un mélange des trois couleurs primaires rouge, vert et bleu. Par exemple, la couleur jaune est composée à 50 % de rouge et à 50 % de vert. Pour coder la couleur d'un pixel, il suffit de coder chaque couleur primaire avec un nombre entier : un nombre pour le rouge, un autre pour le vert et un dernier pour le bleu. Ce codage est appelé le codage RGB. Mais il existe d'autres méthodes, qui codent un pixel non pas à partir des couleurs primaires, mais à partir d'autres espaces de couleur.

Pour stocker une image dans l'ordinateur, on a besoin de connaitre sa largeur, sa longueur et la couleur de chaque pixel. Une image peut donc être représentée dans un fichier par une suite d'entiers : un pour la largeur, un pour la longueur, et le reste pour les couleurs des pixels. Ces entiers sont stockés les uns à la suite des autres dans un fichier. Les pixels sont stockés ligne par ligne, en partant du haut, et chaque ligne est codée de gauche à droite. Les fichiers image actuels utilisent des techniques de codage plus élaborées, permettant notamment décrire une image en utilisant moins de nombres, ce qui prend moins de place dans l'ordinateur.

Son modifier

Pour mémoriser du son, il suffit de mémoriser l'intensité sonore reçue par un microphone à intervalles réguliers. Cette intensité est codée par un nombre entier : si le son est fort, le nombre sera élevé, tandis qu'un son faible se verra attribuer un entier petit. Ces entiers seront rassemblés dans l'ordre de mesure, et stockés dans un fichier son, comme du wav, du PCM, etc. Généralement, ces fichiers sont compressés afin de prendre moins de place.

Le support physique de l'information codée modifier

Pour pouvoir traiter de l'information, la première étape est d'abord de coder celle-ci, c'est à dire de la transformer en nombres. Et peu importe le codage utilisé, celui-ci a besoin d'un support physique, d'une grandeur physique quelconque. Et pour être franc, on peut utiliser tout et n’importe quoi. Par exemple, certains calculateurs assez anciens étaient des calculateurs pneumatiques, qui utilisaient la pression de l'air pour représenter des chiffres ou nombres : soit le nombre encodé était proportionnel à la pression, soit il existait divers intervalles de pression correspondant chacun à un nombre entier bien précis. Il a aussi existé des technologies purement mécaniques pour ce faire, comme les cartes perforées ou d'autres dispositifs encore plus ingénieux. De nos jours, ce stockage se fait soit par l'aimantation d'un support magnétique, soit par un support optique (les CD et DVD), soit par un support électronique. Les supports magnétiques sont réservés aux disques durs magnétiques, destinés à être remplacés par des disques durs entièrement électroniques (les fameux Solid State Drives, que nous verrons dans quelques chapitres).

Pour les supports de stockage électroniques, très courants dans nos ordinateurs, le support en question est une tension électrique. Ces tensions sont ensuite manipulées par des composants électriques/électroniques plus ou moins sophistiqués : résistances, condensateurs, bobines, amplificateurs opérationnels, diodes, transistors, etc. Certains d'entre eux ont besoin d'être alimentés en énergie. Pour cela, chaque circuit est relié à une tension qui l'alimente en énergie : la tension d'alimentation. Après tout, la tension qui code les nombres ne sort pas de nulle part et il faut bien qu'il trouve de quoi fournir une tension de 2, 3, 5 volts. De même, on a besoin d'une tension de référence valant zéro volt, qu'on appelle la masse, qui sert pour le zéro.

Dans les circuits électroniques actuels, ordinateurs inclus, la tension d'alimentation varie généralement entre 0 et 5 volts. Mais de plus en plus, on tend à utiliser des valeurs de plus en plus basses, histoire d'économiser un peu d'énergie. Eh oui, car plus un circuit utilise une tension élevée, plus il consomme d'énergie et plus il chauffe. Pour un processeur, il est rare que les modèles récents utilisent une tension supérieure à 2 volts : la moyenne tournant autour de 1-1.5 volts. Même chose pour les mémoires : la tension d'alimentation de celle-ci diminue au cours du temps. Pour donner des exemples, une mémoire DDR a une tension d'alimentation qui tourne autour de 2,5 volts, les mémoires DDR2 ont une tension d'alimentation qui tombe à 1,8 volts, et les mémoires DDR3 ont une tension d'alimentation qui tombe à 1,5 volts. C'est très peu : les composants qui manipulent ces tensions doivent être très précis.

Les différents codages : analogique, numérique et binaire modifier

Codage numérique : exemple du codage d'un chiffre décimal avec une tension.

Le codage, la transformation d’information en nombre, peut être fait de plusieurs façons différentes. Dans les grandes lignes, on peut identifier deux grands types de codages.

  • Le codage analogique utilise des nombres réels : il code l’information avec des grandeurs physiques (quelque chose que l'on peut mesurer par un nombre) comprises dans un intervalle. Par exemple, un thermostat analogique convertit la température en tension électrique pour la manipuler : une température de 0 degré donne une tension de 0 volts, une température de 20 degrés donne une tension de 5 Volts, une température de 40 degrés donnera du 10 Volts, etc. Un codage analogique a une précision théoriquement infinie : on peut par exemple utiliser toutes les valeurs entre 0 et 5 Volts pour coder une information, même des valeurs tordues comme 1, 2.2345646, ou pire…
  • Le codage numérique n'utilise qu'un nombre fini de valeurs, contrairement au codage analogique. Pour être plus précis, il code des informations en utilisant des nombres entiers, représentés par des suites de chiffres. Le codage numérique précise comment coder les chiffres avec une tension. Comme illustré ci-contre, chaque chiffre correspond à un intervalle de tension : la tension code pour ce chiffre si elle est comprise dans cet intervalle. Cela donnera des valeurs de tension du style : 0, 0.12, 0.24, 0.36, 0.48… jusqu'à 2 volts.

Les avantages et désavantages de l'analogique et du numérique modifier

Un calculateur analogique (qui utilise le codage analogique) peut en théorie faire ses calculs avec une précision théorique très fine, impossible à atteindre avec un calculateur numérique, notamment pour les opérations comme les dérivées, intégrations et autres calculs similaires. Mais dans les faits, aucune machine analogique n'est parfaite et la précision théorique est rarement atteinte, loin de là. Les imperfections des machines posent beaucoup plus de problèmes sur les machines analogiques que sur les machines numériques.

Obtenir des calculs précis sur un calculateur analogique demande non seulement d'utiliser des composants de très bonne qualité, à la conception quasi-parfaite, mais aussi d'utiliser des techniques de conception particulières. Même les composants de qualité ont des imperfections certes mineures, qui peuvent cependant sévèrement perturber les résultats. Les moyens pour réduire ce genre de problème sont très complexes, ce qui fait que la conception des calculateurs analogiques est diablement complexe, au point d'être une affaire de spécialistes. Concevoir ces machines est non seulement très difficile, mais tester leur bon fonctionnement ou corriger des pannes est encore plus complexe.

De plus, les calculateurs analogiques sont plus sensibles aux perturbations électromagnétiques. On dit aussi qu'ils ont une faible immunité au bruit. En effet, un signal analogique peut facilement subir des perturbations qui vont changer sa valeur, modifiant directement la valeur des nombres stockés ou manipulés. Avec un codage numérique, les perturbations ou parasites vont moins perturber le signal numérique. La raison est qu'une variation de tension qui reste dans un intervalle représentant un chiffre ne changera pas sa valeur. Il faut que la variation de tension fasse sortir la tension de l'intervalle pour changer le chiffre. Cette sensibilité aux perturbations est un désavantage net pour l'analogique et est une des raisons qui font que les calculateurs analogiques sont peu utilisés de nos jours. Elle rend difficile de faire fonctionner un calculateur analogique rapidement et limite donc sa puissance.

Un autre désavantage est que les calculateurs analogiques sont très spécialisés et qu'ils ne sont pas programmables. Un calculateur analogique est forcément conçu pour résoudre un problème bien précis. On peut le reconfigurer, le modifier à la marge, mais guère plus. Typiquement, les calculateurs analogiques sont utilisés pour résoudre des équations différentielles couplées non-linéaires, mais n'ont guère d'utilité pratique au-delà. Mais les ingénieurs ne font cela que pour les problèmes où il est pertinent de concevoir de zéro un calculateur spécialement dédié au problème à résoudre, ce qui est un cas assez rare.

Le choix de la base modifier

Au vu des défauts des calculateurs analogiques, on devine que la grosse majorité des circuits électronique actuels sont numériques. Mais il faut savoir que les ordinateurs n'utilisent pas la numération décimale normale, celle à 10 chiffres qui vont de 0 à 9. De nos jours, les ordinateurs n'utilisent que deux chiffres, 0 et 1 (on parle de « bit ») : on dit qu'ils comptent en binaire. On verra dans le chapitre suivant comment coder des nombres avec des bits, ce qui est relativement simple. Pour le moment, nous allons justifier ce choix de n'utiliser que des bits et pas les chiffres décimaux (de 0 à 9). Avec une tension électrique, il y a diverses méthodes pour coder un bit : codage Manchester, NRZ, etc. Autant trancher dans le vif tout de suite : la quasi-intégralité des circuits d'un ordinateur se basent sur le codage NRZ.

Naïvement, la solution la plus simple serait de fixer un seuil en-dessous duquel la tension code un 0, et au-dessus duquel la tension représente un 1. Mais les circuits qui manipulent des tensions n'ont pas une précision parfaite et une petite perturbation électrique pourrait alors transformer un 0 en 1. Pour limiter la casse, on préfère ajouter une sorte de marge de sécurité, ce qui fait qu'on utilise en réalité deux seuils séparés par un intervalle vide. Le résultat est le fameux codage NRZ dont nous venons de parler : la tension doit être en dessous d'un seuil donné pour un 0, et il existe un autre seuil au-dessus duquel la tension représente un 1. Tout ce qu'il faut retenir, c'est qu'il y a un intervalle pour le 0 et un autre pour le 1. En dehors de ces intervalles, on considère que le circuit est trop imprécis pour pouvoir conclure sur la valeur de la tension : on ne sait pas trop si c'est un 1 ou un 0.

Il arrive que ce soit l'inverse sur certains circuits électroniques : en dessous d'un certain seuil, c'est un 1 et si c'est au-dessus d'un autre seuil c'est 0.
Codage NRZ

L'avantage du binaire par rapport aux autres codages est qu'il permet de mieux résister aux perturbations électromagnétiques mentionnées dans le chapitre précédent. À tension d'alimentation égale, les intervalles de chaque chiffre sont plus petits pour un codage décimal : toute perturbation de la tension aura plus de chances de changer un chiffre. Mais avec des intervalles plus grands, un parasite aura nettement moins de chance de modifier la valeur du chiffre codé ainsi. La résistance aux perturbations électromagnétiques est donc meilleure avec seulement deux intervalles.

Comparaison entre codage binaire et décimal pour l'immunité au bruit.


Dans le chapitre précédent, nous avons vu que les ordinateurs actuels utilisent un codage binaire. Ce codage binaire ne vous est peut-être pas familier. Aussi, dans ce chapitre, nous allons apprendre comment coder des nombres en binaire. Nous allons commencer par le cas le plus simple : les nombres positifs. Par la suite, nous aborderons les nombres négatifs. Et nous terminerons par les nombres à virgules, appelés aussi nombres flottants.

Le codage des nombres entiers positifs modifier

Pour coder des nombres entiers positifs, il existe plusieurs méthodes : le binaire, l’hexadécimal, le code Gray, le décimal codé binaire et bien d'autres encore. La plus connue est certainement le binaire, secondée par l'hexadécimal, les autres étant plus anecdotiques. Pour comprendre ce qu'est le binaire, il nous faut faire un rappel sur les nombres entiers tel que vous les avez appris en primaire, à savoir les entiers écrits en décimal. Prenons un nombre écrit en décimal : le chiffre le plus à droite est le chiffre des unités, celui à côté est pour les dizaines, suivi du chiffre des centaines, et ainsi de suite. Dans un tel nombre :

  • on utilise une dizaine de chiffres, de 0 à 9 ;
  • chaque chiffre est multiplié par une puissance de 10 : 1, 10, 100, 1000, etc. ;
  • la position d'un chiffre dans le nombre indique par quelle puissance de 10 il faut le multiplier : le chiffre des unités doit être multiplié par 1, celui des dizaines par 10, celui des centaines par 100, et ainsi de suite.

Exemple avec le nombre 1337 :

Pour résumer, un nombre en décimal s'écrit comme la somme de produits, chaque produit multipliant un chiffre par une puissance de 10. On dit alors que le nombre est en base 10.

Ce qui peut être fait avec des puissances de 10 peut être fait avec des puissances de 2, 3, 4, 125, etc : n'importe quel nombre entier strictement positif peut servir de base. En informatique, on utilise rarement la base 10 à laquelle nous sommes tant habitués. On utilise à la place deux autres bases :

  • La base 2 (système binaire) : les chiffres utilisés sont 0 et 1 ;
  • La base 16 (système hexadécimal) : les chiffres utilisés sont 0, 1, 2, 3, 4, 5, 6, 7, 8 et 9 ; auxquels s'ajoutent les six premières lettres de notre alphabet : A, B, C, D, E et F.

Le système binaire modifier

En binaire, on compte en base 2. Cela veut dire qu'au lieu d'utiliser des puissances de 10 comme en décimal, on utilise des puissances de deux : n'importe quel nombre entier peut être écrit sous la forme d'une somme de puissances de 2. Par exemple 6 s'écrira donc 0110 en binaire : . On peut remarquer que le binaire n'autorise que deux chiffres, à savoir 0 ou 1 : ces chiffres binaires sont appelés des bits (abréviation de Binary Digit). Pour simplifier, on peut dire qu'un bit est un truc qui vaut 0 ou 1. Pour résumer, tout nombre en binaire s'écrit sous la forme d'un produit entre bits et puissances de deux de la forme :

Les coefficients sont les bits, l'exposant n qui correspond à un bit est appelé le poids du bit.

La terminologie du binaire modifier

En informatique, il est rare que l'on code une information sur un seul bit. Dans la plupart des cas, l'ordinateur manipule des nombres codés sur plusieurs bits. Les informaticiens ont donné des noms aux groupes de bits suivant leur taille. Le plus connu est certainement l'octet, qui désigne un groupe de 8 bits. Moins connu, on parle de nibble pour un groupe de 4 bits (un demi-octet), de doublet pour un groupe de 16 bits (deux octets) et de quadruplet pour un groupe de 32 bits (quatre octets).

Précisons qu'en anglais, le terme byte n'est pas synonyme d'octet. En réalité, le terme octet marche aussi bien en français qu'en anglais. Quant au terme byte, il désigne un concept complètement différent, que nous aborderons plus tard (c'est la plus petite unité de mémoire que le processeur peut adresser). Il a existé dans le passé des ordinateurs où le byte faisait 4, 7, 9, 16, voire 48 bits, par exemple. Il a même existé des ordinateur où le byte faisait exactement 1 bit ! Mais sur presque tous les ordinateurs modernes, un byte fait effectivement 8 bits, ce qui fait que le terme byte est parfois utilisé en lieu et place d'octet. Mais c'est un abus de langage, attention aux confusions ! Dans ce cours, nous parlerons d'octet pour désigner un groupe de 8 bits, en réservant le terme byte pour sa véritable signification.

À l'intérieur d'un nombre, le bit de poids faible est celui qui est le plus à droite du nombre, alors que le bit de poids fort est celui non nul qui est placé le plus à gauche, comme illustré dans le schéma ci-dessous.

Bit de poids fort.
Bit de poids faible.

Cette terminologie s'applique aussi pour les bits à l'intérieur d'un octet, d'un nibble, d'un doublet ou d'un quadruplet. Pour un nombre codés sur plusieurs octets, on peut aussi parler de l'octet de poids fort et de l'octet de poids faible, du doublet de poids fort ou de poids faible, etc.

La traduction binaire→décimal modifier

Pour traduire un nombre binaire en décimal, il faut juste se rappeler que la position d'un bit indique par quelle puissance il faut le multiplier. Ainsi, le chiffre le plus à droite est le chiffre des unités : il doit être multiplié par 1 (). Le chiffre situé immédiatement à gauche du chiffre des unités doit être multiplié par 2 (). Le chiffre encore à gauche doit être multiplié par 4 (), et ainsi de suite. Mathématiquement, on peut dire que le énième bit en partant de la droite doit être multiplié par . Par exemple, la valeur du nombre noté 1011 en binaire est de .

Value of digits in the Binary numeral system

La traduction décimal→binaire modifier

La traduction inverse, du décimal au binaire, demande d'effectuer des divisions successives par deux. Les divisions en question sont des divisions euclidiennes, avec un reste et un quotient. En lisant les restes des divisions dans un certain sens, on obtient le nombre en binaire. Voici comment il faut procéder, pour traduire le nombre 34 :

Exemple d'illustration de la méthode de conversion décimal vers binaire.

L'hexadécimal modifier

L’hexadécimal est basé sur le même principe que le binaire, sauf qu'il utilise les 16 chiffres suivants :

Chiffre hexadécimal 0 1 2 3 4 5 6 7 8 9 A B C D E F
Nombre décimal correspondant 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Notation binaire 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

Dans les textes, afin de différencier les nombres décimaux des nombres hexadécimaux, les nombres hexadécimaux sont suivis par un petit h, indiqué en indice. Si cette notation n'existait pas, des nombres comme 2546 seraient ambigus : on ne saurait pas dire sans autre indication s'ils sont écrits en décimal ou en hexadécimal. Avec la notation, on sait de suite que 2546 est en décimal et que 2546h est en hexadécimal.

Dans les codes sources des programmes, la notation diffère selon le langage de programmation. Certains supportent le suffixe h pour les nombres hexadécimaux, d'autres utilisent un préfixe 0x ou 0h.

La conversion hexadécimal↔décimal modifier

Pour convertir un nombre hexadécimal en décimal, il suffit de multiplier chaque chiffre par la puissance de 16 qui lui est attribuée. Là encore, la position d'un chiffre indique par quelle puissance celui-ci doit être multiplié : le chiffre le plus à droite est celui des unités, le second chiffre le plus à droite doit être multiplié par 16, le troisième chiffre en partant de la droite doit être multiplié par 256 (16 * 16) et ainsi de suite. La technique pour convertir un nombre décimal vers de l’hexadécimal est similaire à celle utilisée pour traduire un nombre du décimal vers le binaire. On retrouve une suite de divisions successives, mais cette fois-ci les divisions ne sont pas des divisions par 2 : ce sont des divisions par 16.

La conversion hexadécimal↔binaire modifier

La conversion inverse, de l'hexadécimal vers le binaire est très simple, nettement plus simple que les autres conversions. Pour passer de l'hexadécimal au binaire, il suffit de traduire chaque chiffre en sa valeur binaire, celle indiquée dans le tableau au tout début du paragraphe nommé « Hexadécimal ». Une fois cela fait, il suffit de faire le remplacement. La traduction inverse est tout aussi simple : il suffit de grouper les bits du nombre par 4, en commençant par la droite (si un groupe est incomplet, on le remplit avec des zéros). Il suffit alors de remplacer le groupe de 4 bits par le chiffre hexadécimal qui correspond.

Les quatre opérations arithmétiques de base en binaire modifier

Maintenant que l'on sait coder des nombres en binaire normal, il est utile de savoir comment faire les quatre opérations usuelles en binaire. Par quatre opérations, je veux parler de l'addition, de la soustraction, de la multiplication et de la division. Ce qui est intéressant, c'est que les quatre opérations se font de la même manière en décimal et en binaire. La seule différence tient dans les tables d'addition et de multiplication qui deviennent beaucoup plus simples. Nous utiliserons les acquis de cette section dans la suite du chapitre, bien que de manière assez marginale. Sachez que nous ferons des rappels rapides quand nous parlerons des circuits électroniques qui font des opérations, comme les circuits additionneurs ou les circuits multiplieurs.

L'addition modifier

L'addition se fait en binaire de la même manière qu'en décimal, sauf que l'on additionne des bits. Heureusement, la table d'addition est très simple en binaire :

  • 0 + 0 = 0, retenue = 0 ;
  • 0 + 1 = 1, retenue = 0 ;
  • 1 + 0 = 1, retenue = 0 ;
  • 1 + 1 = 0, retenue = 1 ;

Pour faire une addition en binaire, on additionne les chiffres/bits colonne par colonne, une éventuelle retenue est propagée à la colonne d'à côté.

Exemple d'addition en binaire.

La soustraction modifier

Pour soustraire deux nombres entiers, on peut adapter l'algorithme de soustraction utilisé en décimal, celui que vous avez appris à l'école. Celui-ci ressemble fortement à l'algorithme d'addition : on soustrait les bits de même poids, et on propage éventuellement une retenue sur la colonne suivante. La retenue est soustraite, et non ajoutée. La table de soustraction nous dit quel est le résultat de la soustraction de deux bits. La voici :

  • 0 - 0 = 0 ;
  • 0 - 1 = 1 et une retenue ;
  • 1 - 0 = 1 ;
  • 1 - 1 = 0.
Soustraction en binaire, avec les retenues en rouge.

La multiplication modifier

La multiplication se fait en binaire de la même façon qu'on a appris à le faire en primaire, si ce n'est que la table de multiplication est modifiée. Et les tables de multiplication sont vraiment très simples en binaire, jugez plutôt !

  • 0 × 0 = 0.
  • 0 × 1 = 0.
  • 1 × 0 = 0.
  • 1 × 1 = 1.

Pour poursuivre, petite précision de vocabulaire : une multiplication s'effectue sur deux nombres, le multiplicande et le multiplicateur. Une multiplication génère des résultats temporaires, chacun provenant de la multiplication du multiplicande par un chiffre du multiplicateur : ces résultats temporaires sont appelés des produits partiels. Multiplier deux nombres en binaire demande de générer les produits partiels, de les décaler, avant de les additionner. Voici un exemple :

Algebra1 05 fig019

La division modifier

En binaire, l'opération de division ressemble beaucoup à l’opération de multiplication. L'algorithme le plus simple que l'on puisse créer pour exécuter une division consiste à faire la division exactement comme en décimal. Pour simplifier, la méthode de division est identique à celle de la multiplication, si ce n'est que les additions sont remplacées par des soustractions.

Division en binaire.

Interlude propédeutique : la capacité d'un entier et les débordements d'entiers modifier

Dans la section précédente, nous avons vu comment coder des entiers positifs en binaire ou dans des représentations proches. La logique voudrait que l'on aborde ensuite le codage des entiers négatifs. Mais nous allons déroger à cette logique simple, pour des raisons pédagogiques. Nous allons faire un interlude qui introduira des notions utiles pour la suite du chapitre. De plus, ces concepts seront abordés de nombreuses fois dans ce wikilivre et l'introduire ici est de loin la solution idéale.

Les ordinateurs manipulent des nombres codés sur un nombre fixe de bits modifier

Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits. Et si vous avez joué aux jeux vidéos durant votre jeunesse et êtes assez agé, vous avez entendu parler de consoles de jeu 8 bits, 16 bits, 32 bits, voire 64 bits (pour la Jaguar, et c'était un peu trompeur). Derrière cette appellation qu'on retrouvait autrefois comme argument commercial dans la presse se cache un concept simple. Tout ordinateur manipule des nombres entiers dont le nombre de bits est toujours le même : on dit qu'ils sont de taille fixe. Une console 16 bits manipulait des entiers codés en binaire sur 16 bits, pas un de plus, pas un de moins. Pareil pour les anciens ordinateurs 32 bits, qui manipulaient des nombres entiers codés sur 32 bits.

Aujourd'hui, les ordinateurs modernes utilisent presque un nombre de bits qui est une puissance de 2 : 8, 16, 32, 64, 128, 256, voire 512 bits. Mais cette règle souffre évidemment d'exceptions. Aux tout débuts de l'informatique, certaines machines utilisaient 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. De nos jours, il ne reste que les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaînes HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des nombres entiers de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits.

Anecdote amusante, il a existé des ordinateurs de 1 bit, qui sont capables de manipuler des nombres codés sur 1 bit, pas plus. Un exemple est le Motorola MC14500B, commercialisé de 1976.

Le lien entre nombre de bits et valeurs codables modifier

Évidemment, on ne peut pas coder tous les entiers possibles et imaginables avec seulement 8 bits, ou 16 bits. Et il parait intuitif que l'on ait plus de valeurs codables sur 16 bits qu'avec 8 bits, par exemple. Plus le nombre de bits est important, plus on pourra coder de valeurs entières différentes. Mais combien de plus ? Par exemple, si je passe de 8 bits à 16 bits, est-ce que le nombre de valeurs que l'on peut coder double, quadruple, pentuple ? De même, combien de valeurs différentes on peut coder avec bits. Par exemple, combien de nombres différents peut-on coder avec 4, 8 ou 16 bits ? La section précédente vous l'expliquer.

Avec bits, on peut coder valeurs différentes, dont le , ce qui fait qu'on peut compter de à . N'oubliez pas cette formule : elle sera assez utile dans la suite de ce tutoriel. Pour exemple, on peut coder 16 valeurs avec 4 bits, qui vont de 0 à 15. De même, on peut coder 256 valeurs avec un octet, qui vont de 0 à 255. Le tableau ci-dessous donne quelques exemples communs.

Nombre de bits Nombre de valeurs codables
4 16
8 256
16 65 536
32 4 294 967 296
64 18 446 744 073 709 551 615

Inversement, on peut se demander combien de bits il faut pour coder une valeur quelconque, que nous noterons N. Pour cela, il faut utiliser la formule précédente, mais à l'envers. On cherche alors tel que . L'opération qui donne est appelée le logarithme, et plus précisément un logarithme en base 2, noté . Le problème est que le résultat du logarithme ne tombe juste que si le nombre X est une puissance de 2. Si ce n'est pas le cas, le résultat est un nombre à virgule, ce qui n'a pas de sens pratique. Par exemple, la formule nous dit que pour coder le nombre 13, on a besoin de 3,70043971814 bits, ce qui est impossible. Pour que le résultat ait un sens, il faut arrondir à l'entier supérieur. Pour l'exemple précédent, les 3,70043971814 bits s'arrondissent en 4 bits.

Le lien entre nombre de chiffres hexadécimaux et valeurs codables modifier

Maintenant, voyons combien de valeurs peut-on coder avec chiffres hexadécimaux. La réponse n'est pas très différente de celle obtenue en binaire, si ce n'est qu'il faut remplacer le 2 par un 16 dans la formule précédente. Avec chiffres hexadécimaux, on peut coder valeurs différentes, dont le , ce qui fait qu'on peut compter de à . Le tableau ci-dessous donne quelques exemples communs.

Nombre de chiffres héxadécimaux Nombre de valeurs codables
1 (4 bits) 16
2 (8 bits) 256
4 (16 bits) 65 536
8 (32 bits) 4 294 967 296
16 (64 bits) 18 446 744 073 709 551 615

Inversement, on peut se demander combien faut-il de chiffres hexadécimaux pour coder une valeur quelconque en hexadécimal. La formule est là encore la même qu'en binaire, sauf qu'on remplace le 2 par un 16. Pour trouver le nombre de chiffres hexadécimaux pour encoder un nombre X, il faut calculer . Notons que le logarithme utilisé est un logarithme en base 16, et non un logarithme en base 2, comme pour le binaire. Là encore, le résultat ne tombe juste que si le nombre X est une puissance de 16 et il faut arrondir à l'entier supérieur si ce n'est pas le cas.

Une propriété mathématique des logarithmes nous dit que l'on peut passer d'un logarithme en base X et à un logarithme en base Y avec une simple division, en utilisant la formule suivante :

Dans le cas qui nous intéresse, on a Y = 2 et X = 16, ce qui donne :

Or, est tout simplement égal à 4, car il faut 4 bits pour coder la valeur 16. On a donc :

En clair, il faut quatre fois moins de chiffres hexadécimaux que de bits, ce qui est assez intuitif vu qu'il faut 4 bits pour coder un chiffre hexadécimal.

Les débordements d'entier modifier

On vient de voir que tout ordinateur manipule des nombres dont le nombre de bits est toujours le même : on dit qu'ils sont de taille fixe. Et cela limite les valeurs qu'il peut encoder, qui sont comprise dans un intervalle bien précis. Mais que ce passe-t-il si jamais le résultat d'un calcul ne rentre pas dans cet intervalle ? Par exemple, pour du binaire normal, que faire si le résultat d'un calcul atteint ou dépasse  ? Dans ce cas, le résultat ne peut pas être représenté par l'ordinateur et il se produit ce qu'on appelle un débordement d'entier.

On peut imaginer d'autres codages pour lesquels les entiers ne commencent pas à zéro ou ne terminent pas à . On peut prendre le cas où l'ordinateur gère les nombres négatifs, par exemple. Dans le cas général, l'ordinateur peut coder les valeurs comprises dans un intervalle, qui va de la valeur la plus basse à la valeur la plus grande . Et encore une fois, si un résultat de calcul sort de cet intervalle, on fait face à un débordement d'entier.

La valeur haute de débordement désigne la première valeur qui est trop grande pour être représentée par l'ordinateur. Par exemple, pour un ordinateur qui peut coder tous les nombres entre 0 et 7, la valeur haute de débordement est égale à 8. Pour les nombres entiers, la valeur haute de débordement vaut , avec la plus grande valeur codable par l'ordinateur.

On peut aussi définir la valeur basse de débordement, qui est la première valeur trop petite pour être codée par l'ordinateur. Par exemple, pour un ordinateur qui peut coder tous les nombres entre 8 et 250, la valeur basse de débordement est égale à 7. Pour les nombres entiers, la valeur basse de débordement vaut , avec la plus petite valeur codable par l'ordinateur.

La gestion des débordements d'entiers modifier

Face à un débordement d'entier, l'ordinateur peut utiliser deux méthodes : l'arithmétique saturée ou l'arithmétique modulaire.

L'arithmétique saturée consiste à arrondir le résultat pour prendre la plus grande ou la plus petite valeur. Si le résultat d'un calcul dépasse la valeur haute de débordement, le résultat est remplacé par le plus grand entier supporté par l'ordinateur. La même chose est possible quand le résultat est inférieur à la plus petite valeur possible, par exemple lors d'une soustraction : l'ordinateur arrondit au plus petit entier possible.

Pour donner un exemple, voici ce que cela donne avec des entiers codés sur 4 bits, qui codent des nombres de 0 à 15. Si je fais le calcul 8 + 9, le résultat normal vaut 17, ce qui ne rentre pas dans l'intervalle. Le résultat est alors arrondi à 15. Inversement, si je fais le calcul 8 - 9, le résultat sera de -1, ce qui ne rentre pas dans l'intervalle : le résultat est alors arrondi à 0.

Exemple de débordement d'entier sur un compteur mécanique quelconque. L'image montre clairement que le compteur revient à zéro une fois la valeur maximale dépassée.

L'arithmétique modulaire est plus compliquée et c'est elle qui va nous intéresser dans ce qui suit. Pour simplifier, imaginons que l'on décompte à partir de zéro. Quand on arrive à la valeur haute de débordement, on recommence à compter à partir de zéro. L'arithmétique modulaire n'est pas si contre-intuitive et vous l'utilisez sans doute au quotidien. Après tout, c'est comme cela que l'on compte les heures, les minutes et les secondes. Quand on compte les minutes, on revient à 0 au bout de 60 minutes. Pareil pour les heures : on revient à zéro quand on arrive à 24 heures. Divers compteurs mécaniques fonctionnent sur le même principe et reviennent à zéro quand ils dépassent la plus grande valeur possible, l'image ci-contre en montrant un exemple.

Mathématiquement, l'arithmétique modulaire implique des divisions euclidiennes, celles qui donnent un quotient et un reste. Lors d'un débordement, le résultat s'obtient comme suit : on divise le nombre qui déborde par la valeur haute de débordement, et que l'on conserve le reste de la division. Au passage, l'opération qui consiste à faire une division et à garder le reste au lieu du quotient est appelée le modulo. Prenons l'exemple où l'ordinateur peut coder tous les nombres entre 0 et 1023, soit une valeur haute de débordement de 1024. Pour coder le nombre 4563, on fait le calcul 4563 / 1024. On obtient : . Le reste de la division est de 467 et c'est lui qui sera utilisé pour coder la valeur de départ, 4563. Le nombre 4563 est donc codé par la valeur 467 dans un tel ordinateur en arithmétique modulaire. Au passage, la valeur haute de débordement est toujours codée par un zéro dans ce genre d'arithmétique.

Les ordinateurs utilisent le plus souvent une valeur haute de débordement de , avec n le nombre de bits utilisé pour coder les nombres entiers positifs. En faisant cela, l'opération modulo devient très simple et revient à éliminer les bits de poids forts au-delà du énième bit. Concrètement, l'ordinateur ne conserve que les n bits de poids faible du résultat : les autres bits sont oubliés. Par exemple, reprenons l'exemple d'un ordinateur qui code ses nombres sur 4 bits. Imaginons qu'il fasse le calcul , soit 1101 + 0011 = 1 0000 en binaire. Le résultat entraîne un débordement d'entier et l'ordinateur ne conserve que les 4 bits de poids faible. Cela donne : 1101 + 0011 = 0000. Comme autre exemple l'addition 1111 + 0010 ne donnera pas 17 (1 0001), mais 1 (0001).

Les nombres entiers négatifs modifier

Passons maintenant aux entiers négatifs en binaire : comment représenter le signe moins ("-") avec des 0 et des 1 ? Eh bien, il existe plusieurs méthodes :

  • la représentation en signe-valeur absolue ;
  • la représentation en complément à un ;
  • la représentation en complément à deux;
  • la représentation par excès ;
  • la représentation dans une base négative ;
  • d'autres représentations encore moins utilisées que les autres.

La représentation en signe-valeur absolue modifier

La solution la plus simple pour représenter un entier négatif consiste à coder sa valeur absolue en binaire, et rajouter un bit qui précise si c'est un entier positif ou un entier négatif. Par convention, ce bit de signe vaut 0 si le nombre est positif et 1 s'il est négatif. Avec cette technique, l'intervalle des entiers représentables sur N bits est symétrique : pour chaque nombre représentable en représentation signe-valeur absolue, son inverse l'est aussi. Ce qui fait qu'avec cette méthode, le zéro est codé deux fois : on a un -0, et un +0. Cela pose des problèmes lorsqu'on demande à notre ordinateur d'effectuer des calculs ou des comparaisons avec zéro.

Codage sur 4 bits en signe-valeur absolue

La représentation par excès modifier

La représentation par excès consiste à ajouter un biais aux nombres à encoder afin de les encoder par un entier positif. Pour encoder tous les nombres compris entre -X et Y en représentation par excès, il suffit de prendre la valeur du nombre à encoder, et de lui ajouter un biais égal à X. Ainsi, la valeur -X sera encodée par zéro, et toutes les autres valeurs le seront par un entier positif. Par exemple, prenons des nombres compris entre -127 et 128. On va devoir ajouter un biais égal à 127, ce qui donne :

Valeur avant encodage Valeur après encodage
-127 0
-126 1
-125 2
0 127
127 254
128 255

Les représentations en complément à un et en complément à deux modifier

Les représentations en complément à un et en complément à deux sont deux représentations similaires. Leur idée est de remplacer chaque nombre négatif par un nombre positif équivalent, appelé le complément. Par équivalent, on veut dire que les résultats de tout calcul reste le même si on remplace le nombre négatif initial par son complément. Et pour faire cela, elles se basent sur les débordements d'entier pour fonctionner, et plus précisément sur l'arithmétique modulaire abordée plus haut. Si on fait les calculs avec le complément, les résultats du calcul entraînent un débordement d'entier, qui sera résolu par un modulo. Et le résultat après modulo sera identique au résultat qu'on aurait obtenu avec le nombre négatif sans modulo. En clair, c'est la gestion des débordements qui permet de corriger le résultat de manière à ce que l'opération avec le complément donne le même résultat qu'avec le nombre négatif voulu. Ainsi, on peut coder un nombre négatif en utilisant son complément positif.

Cela ressemble beaucoup à la méthode de soustraction basée sur un complément à 9 pour ceux qui connaissent, sauf que c'est une version binaire qui nous intéresse ici.

La différence entre complément à un et à deux tient dans la valeur haute de débordement. Pour des nombres codés sur bits, la valeur haute de débordement est égale à en complément à deux, alors qu'elle est de en complément à un. De ce fait, la gestion des débordements est plus simple en complément à deux. Concrètement, lors d'un débordement d'entier, l'ordinateur ne conserve que les bits de poids faible du résultat : les autres bits sont oubliés. Par exemple, reprenons l'addition 13 + 3 vue précédemment. En binaire, cela donne : 1101 + 0011 = 1 0000. En ne gardant que les 4 bits de poids faible, on obtient : 1101 + 0011 = 0000. Comme autre exemple l'addition 1111 + 0010 ne donnera pas 17 (10001), mais 1 (0001).

Le calcul du complément modifier

Voyons maintenant comment convertir un nombre décimal en représentation en complément. Précisons que la procédure de conversion est différente suivant que le nombre est positif ou négatif. Pour les nombres positifs, il suffit de les traduire en binaire, sans faire quoi que ce soit de plus. C'est pour les nombres négatifs que la procédure est différente et qu'il faut calculer le complément. La méthode pour déterminer le complément est assez simple. Prenons par exemple un codage sur 4 bits dont la valeur haute de débordement est de 16. Prenons l'addition de 13 + 3 = 16. Avec l'arithmétique modulaire, 16 est équivalent à 0, ce qui donne : 13 + 3 = 0 ! On peut aussi reformuler en disant que 13 = -3, ou encore que 3 = -13. Dit autrement, 3 est le complément de -13 pour ce codage. Et ne croyez pas que ça marche uniquement dans cet exemple : cela se généralise assez rapidement à tout nombre négatif.

Prenons un nombre négatif N et son complément à deux K. Dans ce qui suit, les additions modulo n seront notées comme suit : . La notation se généralise aux autres opérations. Dans le cas général, on a :

On peut donc calculer le complément d'un nombre comme suit :

Ce qui donne, respectivement en complément à deux et à un :

On se retrouve alors avec des soustractions à faire, mais nous ne sommes pas encore armés pour. On pourrait étudier en détail la soustraction en binaire dans ce chapitre, mais il vaut mieux laisser cela à un chapitre ultérieur. Pour passer outre ce problème, nous allons voir que l'on peut simplifier le calcul dans le cas du complément à un. Puis, nous déduirons le cas du complément à deux à partir de celui du complément à un. Pour le moment, l'on a juste besoin de savoir que la soustraction en binaire se fait comme en décimal, à savoir colonne par colonne et avec une propagation de retenue. La seule différence est que la table de soustraction est différente (en clair, il faut juste savoir soustraire deux bits).

Pour le complément à un, la valeur est par définition un nombre dont tous les bits sont à 1. À cette valeur, on soustrait un nombre dont certains bits sont à 1 et d'autres à 0. En clair, pour chaque colonne, on a deux possibilités : soit on doit faire la soustraction , soit la soustraction . Or, les règles de l'arithmétique binaire disent que et . En regardant attentivement, on se rend compte que le bit du résultat est l'inverse du bit de départ. De plus, les deux cas ne donnent pas de retenue : le calcul pour chaque bit n'influence pas les bits voisins. En clair, le complément à un s'obtient en codant la valeur absolue en binaire, puis en inversant tous les bits du résultat (les 0 se transforment en 1 et réciproquement). On a donc :

Pour le complément à deux, on peut utiliser le même raisonnement. Pour cela, reprenons l'équation précédente :

Elle peut se réécrire comme suit :

Or, : n'est autre que le complément à un, ce qui donne :

Pour résumer, on a :

En clair, le complément à deux s'obtient en prenant le complément à un et en ajoutant 1. Dit autrement, il faut prendre le nombre N, en inverser tous les bits et ajouter 1.

Une autre manière équivalente consiste à faire le calcul suivant :

On prend le nombre dont on veut le complément, on soustrait 1 et on inverse les bits.

Comparaison entre complément à un et à deux modifier

Avec le complément à un, le zéro est codé deux fois, avec un zéro positif et un zéro négatif. Les calculs sont cependant légèrement plus simples qu'en complément à deux, notamment quand il faut calculer le complément. Remarquez qu'en complément à un, la valeur haute de débordement est le zéro négatif. Traduire en décimal un nombre en complément à un est assez facile : il suffit de le traduire comme on le ferait avec du binaire non-signé, mais en ne tenant pas compte du bit de poids fort. Le signe du résultat est indiqué par le bit de poids fort : 1 pour un nombre négatif et 0 pour un nombre positif.

Codage sur 4 bits en complément à 1.

Avec le complément à deux, le zéro n'est codé que par un seul nombre binaire. Le zéro négatif est remplacé par une valeur négative, par la plus petite valeur négative pour être précis. C'est un avantage suffisant pour que tous les ordinateurs utilisent cette représentation. Pour savoir quelle est la valeur décimale d'un entier en complément à deux, on utilise la même procédure que pour traduire un nombre du binaire vers le décimal, à un détail près : le bit de poids fort (celui le plus à gauche) doit être multiplié par la puissance de deux associée, mais le résultat doit être soustrait au lieu d'être additionné.

Codage sur 4 bits en complément à 2.
Avec les deux représentations, le bit de poids fort vaut 0 pour les nombres positifs et 1 pour les négatifs : il agit comme un bit de signe.

L'extension de signe modifier

Dans nos ordinateurs, tous les nombres sont codés sur un nombre fixé et constant de bits. Ainsi, les circuits d'un ordinateur ne peuvent manipuler que des nombres de 4, 8, 12, 16, 32, 48, 64 bits, suivant la machine. Si l'on veut utiliser un entier codé sur 16 bits et que l'ordinateur ne peut manipuler que des nombres de 32 bits, il faut bien trouver un moyen de convertir notre nombre de 16 bits en un nombre de 32 bits, sans changer sa valeur et en conservant son signe. Cette conversion d'un entier en un entier plus grand, qui conserve valeur et signe s'appelle l'extension de signe. L'extension de signe des nombres positifs consiste à remplir les bits de poids fort avec des 0 jusqu’à arriver à la taille voulue : c'est la même chose qu'en décimal, où rajouter des zéros à gauche d'un nombre positif ne changera pas sa valeur. Pour les nombres négatifs, il faut remplir les bits à gauche du nombre à convertir avec des 1, jusqu'à obtenir le bon nombre de bits : par exemple, 1000 0000 (-128 codé sur 8 bits) donnera 1111 1111 1000 000 après extension de signe sur 16 bits. L'extension de signe d'un nombre codé en complément à 2 se résume donc en une phrase : il faut recopier le bit de poids fort de notre nombre à convertir à gauche de celui-ci jusqu’à atteindre le nombre de bits voulu.

La représentation négabinaire modifier

Enfin, il existe une dernière méthode, assez simple à comprendre, appelée représentation négabinaire. Dans cette méthode, les nombres sont codés non en base 2, mais en base -2. Oui, vous avez bien lu : la base est un nombre négatif. Dans les faits, la base -2 est similaire à la base 2 : il y a toujours deux chiffres (0 et 1), et la position dans un chiffre indique toujours par quelle puissance de 2 il faut multiplier, sauf qu'il faudra ajouter un signe moins une fois sur 2. Concrètement, les puissances de -2 sont les suivantes : 1, -2, 4, -8, 16, -32, 64, etc. En effet, un nombre négatif multiplié par un nombre négatif donne un nombre positif, ce qui fait qu'une puissance sur deux est négative, alors que les autres sont positives. Ainsi, on peut représenter des nombres négatifs, mais aussi des nombres positifs dans une puissance négative.

Par exemple, la valeur du nombre noté 11011 en base -2 s'obtient comme suit :

-32 16 -8 4 -2 1
1 1 1 0 1 1

Sa valeur est ainsi de (−32×1)+(16×1)+(−8×1)+(4×0)+(−2×1)+(1×1)=−32+16−8−2+1=−25.

Les nombres à virgule modifier

On sait donc comment sont stockés nos nombres entiers dans un ordinateur. Néanmoins, les nombres entiers ne sont pas les seuls nombres que l'on utilise au quotidien : il nous arrive d'en utiliser à virgule. Notre ordinateur n'est pas en reste : il est lui aussi capable de manipuler de tels nombres. Dans les grandes lignes, il peut utiliser deux méthodes pour coder des nombres à virgule en binaire : La virgule fixe et la virgule flottante.

Les nombres à virgule fixe modifier

La méthode de la virgule fixe consiste à émuler les nombres à virgule à partir de nombres entiers. Un nombre à virgule fixe est codé par un nombre entier proportionnel au nombre à virgule fixe. Pour obtenir la valeur de notre nombre à virgule fixe, il suffit de diviser l'entier servant à le représenter par le facteur de proportionnalité. Par exemple, pour coder 1,23 en virgule fixe, on peut choisir comme « facteur de conversion » 1000, ce qui donne l'entier 1230.

Généralement, les informaticiens utilisent une puissance de deux comme facteur de conversion, pour simplifier les calculs. En faisant cela, on peut écrire les nombres en binaire et les traduire en décimal facilement. Pour l'exemple, cela permet d'écrire des nombres à virgule en binaire comme ceci : 1011101,1011001. Et ces nombres peuvent se traduire en décimal avec la même méthode que des nombres entier, modulo une petite différence. Comme pour les chiffres situés à gauche de la virgule, chaque bit situé à droite de la virgule doit être multiplié par la puissance de deux adéquate. La différence, c'est que les chiffres situés à droite de la virgule sont multipliés par une puissance négative de deux, c'est à dire par , , , , , ...

Cette méthode est assez peu utilisée de nos jours, quoiqu'elle puisse avoir quelques rares applications relativement connue. Un bon exemple est celui des banques : les sommes d'argent déposées sur les comptes ou transférées sont codés en virgule fixe. Les sommes manipulées par les ordinateurs ne sont pas exprimées en euros, mais en centimes d'euros. Et c'est une forme de codage en virgule fixe dont le facteur de conversion est égal à 100. La raison de ce choix est que les autres méthodes de codage des nombres à virgule peuvent donner des résultats imprécis : il se peut que les résultats doivent être tronqués ou arrondis, suivant les opérandes. Cela n'arrive jamais en virgule fixe, du moins quand on se limite aux additions et soustractions.

Les nombres flottants modifier

Les nombres à virgule fixe ont aujourd'hui été remplacés par les nombres à virgule flottante, où le nombre de chiffres après la virgule est variable. Le codage d'un nombre flottant est basée sur son écriture scientifique. Pour rappel, en décimal, l’écriture scientifique d'un nombre consiste à écrire celui-ci comme un produit entre un nombre et une puissance de 10. Ce qui donne :

, avec

Le nombre est appelé le significande et il est compris entre 1 (inclus) et 10 (exclu). Cette contrainte garantit que l'écriture scientifique d'un nombre est unique, qu'il n'y a qu'une seule façon d'écrire un nombre en notation scientifique. Pour cela, on impose le nombre de chiffre à gauche de la virgule et le plus simple est que celui-ci soit égal à 1. Mais il faut aussi que celui-ci ne soit pas nul. En effet, si on autorise de mettre un 0 à gauche de la virgule, il y a plusieurs manières équivalentes d'écrire un nombre. Ces deux contraintes font que le significande doit être égal ou plus grand que 1, mais strictement inférieur à 10. Par contre, on peut mettre autant de décimales que l'on veut.

En binaire, c'est la même chose, mais avec une puissance de deux. Cela implique de modifier la puissance utilisée : au lieu d'utiliser une puissance de 10, on utilise une puissance de 2.

, avec

Le significande est aussi altéré, au même titre que la puissance, même si les contraintes sont similaires à celles en base 10. En effet, le nombre ne possède toujours qu'un seul chiffre à gauche de la virgule, comme en base 10. Vu que seuls deux chiffres sont possibles (0 et 1) en binaire, on s'attend à ce que le chiffre situé à gauche de la virgule soit un zéro ou un 1. Mais rappelons que le chiffre à gauche doit être non-nul, pour les mêmes raisons qu'en décimal. En clair, le significande a forcément un bit à 1 à gauche de la virgule. Pour récapituler, l'écriture scientifique binaire d'un nombre consiste à écrire celui-ci sous la forme :

, avec

La partie fractionnaire du nombre , qu'on appelle la mantisse.

Écriture scientifique (anglais).

Traduire un nombre en écriture scientifique binaire modifier

Pour déterminer l'écriture scientifique en binaire d'un nombre quelconque, la procédure dépend de la valeur du nombre en question. Tout dépend s'il est dans l'intervalle , au-delà de 2 ou en-dessous de 1.

  • Pour un nombre entre 1 (inclus) et 2 (exclu), il suffit de le traduire en binaire. Son exposant est 0.
  • Pour un nombre au-delà de 2, il faut le diviser par 2 autant de fois qu'il faut pour qu'il rentre dans l’intervalle . L'exposant est alors le nombre de fois qu'il a fallu diviser par 2.
  • Pour un nombre plus petit que 1, il faut le multiplier par 2 autant de fois qu'il faut pour qu'il rentre dans l’intervalle . L'exposant se calcule en prenant le nombre de fois qu'il a fallu multiplier par 2, et en prenant l'opposé (en mettant un signe - devant le résultat).

Le codage des nombres flottants et la norme IEEE 754 modifier

Pour coder cette écriture scientifique avec des nombres, l'idée la plus simple est d'utiliser trois nombres, pour coder respectivement la mantisse, l'exposant et un bit de signe. Coder la mantisse implique que le bit à gauche de la virgule vaut toujours 1, mais nous verrons qu'il y a quelques rares exceptions à cette règle. Quelques nombres flottants spécialisés, les dénormaux, ne sont pas codés en respectant les règles pour le significande et ont un 0 à gauche de la virgule. Un bon exemple est tout simplement la valeur zéro, que l'on peut coder en virgule flottante, mais seulement en passant outre les règles sur le significande. Toujours est-il que le bit à gauche de la virgule n'est pas codé, que ce soit pour les flottants normaux ou les fameux dénormaux qui font exception. On verra que ce bit peut se déduire en fonction de l'exposant utilisé pour encoder le nombre à virgule, ce qui lui vaut le nom de bit implicite. L'exposant peut être aussi bien positif que négatif (pour permettre de coder des nombres très petits), et est encodé en représentation par excès sur n bits avec un biais égal à .

IEEE754 Format Général

Le standard pour le codage des nombres à virgule flottante est la norme IEEE 754. Cette norme va (entre autres) définir quatre types de flottants différents, qui pourront stocker plus ou moins de valeurs différentes.

Classe de nombre flottant Nombre de bits utilisés pour coder un flottant Nombre de bits de l'exposant Nombre de bits pour la mantisse Décalage
Simple précision 32 8 23 127
Double précision 64 11 52 1023
Double précision étendue 80 ou plus 15 ou plus 64 ou plus 16383 ou plus

IEEE754 impose aussi le support de certains nombres flottants spéciaux qui servent notamment à stocker des valeurs comme l'infini. Commençons notre revue des flottants spéciaux par les dénormaux, aussi appelés flottants dénormalisés. Ces flottants ont une particularité : leur bit implicite vaut 0. Ces dénormaux sont des nombres flottants où l'exposant est le plus petit possible. Le zéro est un dénormal particulier dont la mantisse est nulle. Au fait, remarquez que le zéro est codé deux fois à cause du bit de signe : on se retrouve avec un -0 et un +0.

Bit de signe Exposant Mantisse
0 ou 1 Valeur minimale (0 en binaire) Mantisse différente de zéro (dénormal strict) ou égale à zéro (zéro)

Fait étrange, la norme IEEE754 permet de représenter l'infini, aussi bien en positif qu'en négatif. Celui-ci est codé en mettant l'exposant à sa valeur maximale et la mantisse à zéro. Et le pire, c'est qu'on peut effectuer des calculs sur ces flottants infinis. Mais cela a peu d'utilité.

Bit de signe Exposant Mantisse
0 ou 1 Valeur maximale Mantisse égale à zéro

Mais malheureusement, l'invention des flottants infinis n'a pas réglé tous les problèmes. Par exemple, quel est le résultat de  ? Ou encore  ? Autant prévenir tout de suite : mathématiquement, on ne peut pas savoir quel est le résultat de ces opérations. Pour pouvoir résoudre ces calculs, il a fallu inventer un nombre flottant qui signifie « je ne sais pas quel est le résultat de ton calcul pourri ». Ce nombre, c'est NaN. NaN est l'abréviation de Not A Number, ce qui signifie : n'est pas un nombre. Ce NaN a un exposant dont la valeur est maximale, mais une mantisse différente de zéro. Pour être plus précis, il existe différents types de NaN, qui diffèrent par la valeur de leur mantisse, ainsi que par les effets qu'ils peuvent avoir. Malgré son nom explicite, on peut faire des opérations avec NaN, mais cela ne sert pas vraiment à grand chose : une opération arithmétique appliquée avec un NaN aura un résultat toujours égal à NaN.

Bit de signe Exposant Mantisse
0 ou 1 Valeur maximale Mantisse différente de zéro

Les arrondis et exceptions modifier

La norme impose aussi une gestion des arrondis ou erreurs, qui arrivent lors de calculs particuliers. En voici la liste :

Nom de l’exception Description
Invalid operation Opération qui produit un NAN. Elle est levée dans le cas de calculs ayant un résultat qui est un nombre complexe, ou quand le calcul est une forme indéterminée. Pour ceux qui ne savent pas ce que sont les formes indéterminées, voici en exclusivité la liste des calculs qui retournent NaN : , , , , .
Overflow Résultat trop grand pour être stocké dans un flottant. Le plus souvent, on traite l'erreur en arrondissant le résultat vers Image non disponible ;
Underflow Pareil que le précédent, mais avec un résultat trop petit. Le plus souvent, on traite l'erreur en arrondissant le résultat vers 0.
Division par zéro Le nom parle de lui-même. La réponse la plus courante est de répondre + ou - l'infini.
Inexact Le résultat ne peut être représenté par un flottant et on doit l'arrondir.

La gestion des arrondis pose souvent problème. Pour donner un exemple, on va prendre le nombre 0,1. En binaire, ce nombre s'écrit comme ceci : 0,1100110011001100... et ainsi de suite jusqu'à l'infini. Notre nombre utilise une infinité de décimales. Bien évidemment, on ne peut pas utiliser une infinité de bits pour stocker notre nombre et on doit impérativement l'arrondir. Comme vous le voyez avec la dernière exception, le codage des nombres flottants peut parfois poser problème : dans un ordinateur, il se peut qu'une opération sur deux nombres flottants donne un résultat qui ne peut être codé par un flottant. On est alors obligé d'arrondir ou de tronquer le résultat de façon à le faire rentrer dans un flottant. Pour éviter que des ordinateurs différents utilisent des méthodes d'arrondis différentes, on a décidé de normaliser les calculs sur les nombres flottants et les méthodes d'arrondis. Pour cela, la norme impose le support de quatre modes d'arrondis :

  • Arrondir vers + l'infini ;
  • vers - l'infini ;
  • vers zéro ;
  • vers le nombre flottant le plus proche.

Les nombres flottants logarithmiques modifier

Les nombres flottants logarithmiques sont une spécialisation des nombres flottants IEEE754, ou tout du moins une spécialisation des flottants écrits en écriture scientifique. Avec ces nombres logarithmiques, la mantisse est totalement implicite : tous les flottants logarithmiques ont la même mantisse, qui vaut 1. Seul reste l'exposant, qui varie suivant le nombre flottant. On peut noter que cet exposant est tout simplement le logarithme en base 2 du nombre encodé, d'où le nombre de codage flottant logarithmique donné à cette méthode. Un nombre logarithmique est donc composé d'un bit de signe et d'un exposant, sans mantisse. Attention toutefois : l'exposant est ici un nombre fractionnaire, ce qui signifie qu'il est codé en virgule fixe. Le choix d'un exposant fractionnaire permet de représenter pas mal de nombres de taille diverses.

Bit de signe Exposant
Représentation binaire 0 01110010101111
Représentation décimale + 1040,13245464

L'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. En effet, les mathématiques de base nous disent que le logarithme d'un produit est égal à la somme des logarithmes : . Or, il se trouve que les ordinateurs sont plus rapides pour faire des additions/soustractions que pour faire des multiplications/divisions. On verra dans quelques chapitres que les circuits électroniques d'addition/soustraction sont beaucoup plus simples que les circuits pour multiplier et/ou diviser. Dans ces conditions, la représentation logarithmique permet de remplacer les multiplications/divisions par des opérations additives plus simples et plus rapides pour l'ordinateur. Évidemment, les applications des flottants logarithmiques sont rares, limitées à quelques situations bien précises (traitement d'image, calcul scientifique spécifiques).

Les encodages binaires alternatifs modifier

Outre le binaire usuel que nous venons de voir, il existe d'autres manières de coder des nombres en binaires. Et nous allons les aborder dans cette section. Parmi celle-ci, nous parlerons du code Gray, mais aussi du fameux Binary Coded Decimal. Nous en parlons car elles seront utiles dans la suite du cours, bien que de manière assez limitée. Autant nous passerons notre temps à parler du binaire normal, autant les représentations que nous allons voir sont aujourd'hui assez peu utilisées. Le Binary Coded Decimal a eu son heure de gloire au début de l'informatique grand public, mais il est aujourd'hui obsolète. Les autres représentations sont utilisées dans des cas assez spécifiques, mais certaines sont plus courantes que vous ne le pensez.

Le Binary Coded Decimal modifier

Le Binary Coded Decimal, abrévié BCD, est une représentation qui mixe binaire et décimal. Avec cette représentation, les nombres sont écrits en décimal, comme nous en avons l'habitude dans la vie courante, sauf que chaque chiffre décimal est directement traduit en binaire sur 4 bits. Prenons l'exemple du nombre 624 : le 6, le 2 et le 4 sont codés en binaire séparément, ce qui donne 0110 0010 0100.

Codage BCD
Nombre encodé (décimal) BCD
0 0 0 0 0
1 0 0 0 1
2 0 0 1 0
3 0 0 1 1
4 0 1 0 0
5 0 1 0 1
6 0 1 1 0
7 0 1 1 1
8 1 0 0 0
9 1 0 0 1

On peut remarquer que 4 bits permettent de coder 16 valeurs, là où il n'y a que 10 chiffres. Dans le BCD proprement dit, les combinaisons de bits qui correspondent à 10, 11, 12, 13, 14 ou 15 n'ont aucune utilité et ne sont tout simplement pas prises en compte. Sur beaucoup ordinateurs, ces combinaisons servaient à coder des chiffres décimaux en double : certains chiffres pouvaient être codés de deux manières différentes. Mais certaines variantes du BCD les utilisaient pour représenter d'autres symboles, comme un signe + ou - afin de gérer les entiers relatifs. De tels nombres relatifs étaient alors codés en utilisant la représentation en signe-magnitude. Une autre possibilité, complémentaire de la précédente, était d'utiliser ces valeurs en trop pour coder une virgule, afin de gérer les nombres non-entiers. En faisant cela, on utilise une technique spécifique au BCD, qui n'était ni la technique de la virgule fixe, ni l'encodage des nombres flottants vu plus haut.

Les variantes du BCD sont assez nombreuses. L'une d'entre elle est la représentation Excess-3. Avec elle, la conversion d'un chiffre décimal se fait comme suit : on prend le chiffre décimal, on ajoute 3, puis on traduit en binaire. L'avantage de cette représentation est que l'on peut facilement calculer les soustractions en utilisant une méthode bien précise (celle du complément à 10, je ne détaille pas plus). Le défaut est que le calcul des additions est légèrement plus complexe.

Codage en Excess-3
Nombre encodé (décimal) BCD
0 0 0 1 1
1 0 1 0 0
2 0 1 0 1
3 0 1 1 0
4 0 1 1 1
5 1 0 0 0
6 1 0 0 1
7 1 0 1 0
8 1 0 1 1
9 1 1 0 0

D'autres variantes du BCD visent à réduire le nombre de bits utilisés pour encoder un nombre décimal. Avec certaines variantes, on peut utiliser seulement 7 bits pour coder deux chiffres décimaux, au lieu de 8 bits en BCD normal. Idem pour les nombres à 3 chiffres décimaux, qui prennent 10 bits au lieu de 12. Cette économie est réalisée par une variante du BCD assez compliquée, appelée l'encodage Chen-Ho. Une alternative, appelée le Densely Packed Decimal, arrive à une compression identique, mais avec quelques avantages au niveau de l'encodage. Ces encodages sont cependant assez compliqués à expliquer, surtout à ce niveau du cours, aussi je me contente de simplement mentionner leur existence.

Le BCD et ses variantes étaient utilisés sur les tous premiers ordinateurs pour faciliter le travail des programmeurs, mais aussi sur les premières calculettes. Le BCD est utile dans les applications où on doit manipuler chaque chiffre décimal séparément des autres. Un exemple classique est celui d'une horloge digitale. Il a aussi l'avantage de bien se marier avec les nombres à virgule. Comme nous le verrons plus tard, coder des nombres à virgule en binaire est loin d'être une chose facile et plusieurs représentations existent. Toutes ont le défaut que des arrondis peuvent survenir. Par exemple, la valeur 0,2 est codée comme suit en binaire normal : 0.00110011001100... et ainsi de suite jusqu’à l'infini. Avec le BCD, on n'a pas ce problème, 0,2 étant codé 0000 , 0010.

Le code Gray modifier

Le code Gray est un encodage binaire qui a une particularité très intéressante : deux nombres consécutifs n'ont qu'un seul bit de différence. Pour exemple, voici ce que donne le codage des 8 premiers entiers sur 3 bits :

Décimal Binaire naturel Codage Gray
0 000 000
1 001 001
2 010 011
3 011 010
4 100 110
5 101 111
6 110 101
7 111 100

Le code Gray très utile dans certains circuits appelés les compteurs, qui mémorisent un nombre et l'incrémentent (+1) ou le décrémentent (-1) suivant les besoins de l'utilisateur. Imaginez par exemple un circuit qui compte le nombre de voitures dans un parking dans la journée. Pour cela, vous allez prendre un circuit qui détecte l'entrée d'une voiture, un autre pour détecter les sorties, et un compteur. Le compteur est initialisé à 0 quand le parking est vide, puis est incrémenté à chaque entrée de voiture, décrémenté à chaque sortie. A chaque instant, le compteur contient le nombre de voitures dans le parking. C'est là un exemple d'un compteur, mais les exemples où on a besoin d'un circuit pour compter quelque chose sont nombreux.

L'avantage du code Gray pour les compteurs est l'absence d'état transitoires douteux. En binaire normal, lorsqu'on passe d'un nombre au suivant, plusieurs bits changent. Il se passe la même chose avec un circuit compteur, qui change l'état de plusieurs bits. Le problème est que le circuit compteur met un certain temps avant de changer tous les bits, un temps généralement très court. Et pendant ce temps, tous les bits à modifier ne le sont pas en même temps. Typiquement, les bits de poids faibles sont modifiés avant les autres. Évidemment, à la fin du calcul, on obtient le résultat final, correct. Mais pendant le temps de calcul, le compteur peut se retrouver dans un état transitoire, où certains bits ont été modifiés mais pas les autres. Et c'est parfois un problème si le contenu de ce compteur est relié à des circuits assez rapides, qui peuvent, mais ne doivent pas voir cet état transitoire sous peine de dysfonctionner. L'usage de compteurs en code Gray permet d'éviter ce problème : vu que seul un bit est modifié lors d'une incrémentation/décrémentation, les états transitoires n'existent tout simplement pas.

Un autre détail est que ce code sera absolument nécessaire quand nous parlerons des tables de Karnaugh, un concept important pour la conception de circuits électroniques. Ne passez pas à côté de cette section.

Pour construire ce code Gray, on peut procéder en suivant plusieurs méthodes, les deux plus connues étant la méthode du miroir et la méthode de l'inversion.

Construction d'un code gray par la méthode du miroir.

La méthode du miroir est relativement simple. Pour connaître le code Gray des nombres codés sur n bits, il faut :

  • partir du code Gray sur n-1 bits ;
  • symétriser verticalement les nombres déjà obtenus (comme une réflexion dans un miroir) ;
  • rajouter un 0 au début des anciens nombres, et un 1 au début des nouveaux nombres.

Il suffit de connaître le code Gray sur 1 bit pour appliquer la méthode : 0 est codé par le bit 0 et 1 par le bit 1.

Une autre méthode pour construire la suite des nombres en code Gray sur n bits est la méthode de l'inversion. Celle-ci permet de connaître le codage du nombre n à partir du codage du nombre n-1, comme la méthode du dessus. On part du nombre 0, systématiquement codé avec uniquement des zéros. Par la suite, on décide quel est le bit à inverser pour obtenir le nombre suivant, avec la règle suivante :

  • si le nombre de 1 est pair, il faut inverser le dernier chiffre.
  • si le nombre de 1 est impair, il faut localiser le 1 le plus à droite et inverser le chiffre situé à sa gauche.

Pour vous entraîner, essayez par vous-même avec 2, 3, voire 5.

La représentation one-hot modifier

Dans le représentation one-hot, les nombres sont codés de manière à ce que un seul bit est à 1 pendant que les autres sont à 0. Cela laisse peu de valeurs possibles : pour N bits, on peut encoder seulement N valeurs. Les entiers sont codés de la manière suivante : le nombre N est encodé en mettant le énième bit à 1, avec la condition que l'on commence à compteur à partir de zéro. Dit autrement, le nombre encodé est égal au poids du bit à 1. Par exemple, si le bit de poids faible (celui de poids 0) est à 1, alors on code la valeur 0. Si le bit de poids numéro 1 est à 1, alors on code la valeur 1. Et ainsi de suite.

Décimal Binaire One-hot
0 000 00000001
1 001 00000010
2 010 00000100
3 011 00001000
4 100 00010000
5 101 00100000
6 110 01000000
7 111 10000000
Il est important de remarquer que dans cette représentation, le zéro est n'est PAS codé en mettant tous les bits à 0. Le zéro est codé en mettant le bit de poids faible à 1.

L'utilité de cette représentation n'est pas évidente. Mais sachez qu'elle le deviendra quand nous parlerons des circuits appelés les "compteurs", tout comme ce sera le cas pour le code Gray. Elle est très utilisée dans des circuits appelés des machines à état, qui doivent incorporer des circuits compteurs efficients. Et cette représentation permet d'avoir des circuits pour compter qui sont très simples, efficaces, rapides et économes en circuits électroniques.


Les circuits électroniques modifier

Grâce au chapitre précédent, on sait enfin comment sont représentées nos données les plus simples avec des bits. On n'est pas encore allés bien loin : on ne sait pas comment représenter des bits dans notre ordinateur ou les modifier, les manipuler, ni faire quoi que ce soit avec. On sait juste transformer nos données en paquets de bits (et encore, on ne sait vraiment le faire que pour des nombres entiers, des nombres à virgule et du texte...). C'est pas mal, mais il reste du chemin à parcourir ! Rassurez-vous, ce chapitre est là pour corriger ce petit défaut. On va vous expliquer quels traitements élémentaires notre ordinateur va effectuer sur nos bits.

Les portes logiques de base modifier

Les portes logiques sont des circuits qui possèdent des sorties et des entrées sur lesquelles on va placer ou récupérer des bits. Les entrées ne sont rien d'autre que des morceaux de « fil » conducteur sur lesquels on envoie un bit (une tension). À partir de là, le circuit électronique va réagir et déduire le bit à placer sur chaque sortie. Tous les composants d'un ordinateur sont fabriqués avec ce genre de circuits.

Sur les schémas qui vont suivre, les entrées des portes logiques seront à gauche et les sorties à droite !

Les portes logiques ont différent symboles selon le pays et l'organisme de normalisation :

  • Commission électrotechnique internationale (CEI) ou International Electrotechnical Commission (IEC),
  • Deutsches Institut für Normung (DIN, Institut allemand de normalisation),
  • American National Standards Institute (ANSI).

La porte OUI/BUFFER modifier

La première porte fondamentale est la porte OUI, qui agit sur un seul bit : sa sortie est exactement égale à l'entrée. En clair, elle recopie le bit en entrée sur sa sortie. Pour simplifier la compréhension, je vais rassembler les états de sortie en fonction des entrées pour chaque porte logique dans un tableau que l'on appelle table de vérité.

Entrée Sortie
0 0
1 1
Symboles d'une porte OUI(BUFFER).
CEI DIN ANSI

Mine de rien, la porte OUI est parfois utile. Elle sert surtout pour recopier un signal électrique qui risque de se dissiper dans un fil trop long. On place alors une porte OUI au beau milieu du fil, pour éviter tout problème, la porte logique régénérant le signal électrique, comme on le verra dans le chapitre suivant. Cela lui vaut parfois le nom de porte BUFFER, ce qui veut dire tampon. Les portes OUI sont aussi utilisées dans certaines mémoires RAM (les mémoires SRAM), comme nous le verrons dans quelques chapitres.

La porte NON modifier

La seconde porte fondamentale est la porte NON, qui agit sur un seul bit : la sortie d'une porte NON est exactement le contraire de l'entrée. Son symbole ressemble beaucoup au symbole d'une porte OUI, la seule différence étant le petit rond au bout du triangle.

Entrée Sortie
0 1
1 0
Symboles d'une porte NON (NOT).
CEI DIN ANSI

La porte ET modifier

La porte ET possède plusieurs entrées, mais une seule sortie. Cette porte logique met sa sortie à 1 quand toutes ses entrées valent 1.

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 0
1 0 0
1 1 1
Symboles d'une porte ET (AND).
CEI DIN ANSI

La porte NAND modifier

La porte NAND donne l'exact inverse de la sortie d'une porte ET. En clair, sa sortie ne vaut 1 que si au moins une entrée est nulle. Dans le cas contraire, si toutes les entrées sont à 1, la sortie vaut 0.

Entrée 1 Entrée 2 Sortie
0 0 1
0 1 1
1 0 1
1 1 0
Symboles d'une porte NON-ET (NAND).
CEI DIN ANSI

Au fait, si vous regardez le schéma de la porte NAND, vous verrez que son symbole est presque identique à celui d'une porte ET : seul un petit rond (blanc pour ANSI, noir pour DIN) ou une barre (CEI) sur la sortie de la porte a été rajouté. Il s'agit d'une sorte de raccourci pour schématiser une porte NON.

La porte OU modifier

La porte OU est une porte dont la sortie vaut 1 si et seulement si au moins une entrée vaut 1. Dit autrement, sa sortie est à 0 si toutes les entrées sont à 0.

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 1
1 0 1
1 1 1
Symboles d'une porte OU (OR).
CEI DIN ANSI

La porte NOR modifier

La porte NOR donne l'exact inverse de la sortie d'une porte OU.

Entrée 1 Entrée 2 Sortie
0 0 1
0 1 0
1 0 0
1 1 0
Symboles d'une porte NON-OU (NOR).
CEI DIN ANSI

La porte XOR modifier

Avec une porte OU, deux ET et deux portes NON, on peut créer une porte nommée XOR. Cette porte est souvent appelée porte OU exclusif. Sa sortie est à 1 quand les deux bits placés sur ses entrées sont différents, et vaut 0 sinon.

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 1
1 0 1
1 1 0
Symboles d'une porte OU-exclusif (XOR).
CEI DIN ANSI

La porte XNOR modifier

La porte XOR possède une petite sœur : la XNOR. Sa sortie est à 1 quand les deux entrées sont identiques, et vaut 0 sinon (elle est équivalente à une porte XOR suivie d'une porte NON).

Entrée 1 Entrée 2 Sortie
0 0 1
0 1 0
1 0 0
1 1 1
Symboles d'une porte NON-OU-exclusif (XNOR).
CEI DIN ANSI

Les autres portes logiques modifier

Les portes logiques que nous venons de voir ne sont pas les seules. En fait, il existe un grand nombre de portes logiques différentes, certaines ayant plus d'intérêt que d'autres. Mais avant toute chose, nous allons parler d'un point important : combien y a-t-il de portes logiques en tout ? La question a une réponse très claire, pour peu qu'on précise la question. Les portes que nous avons vu précédemment ont respectivement 1 et 2 bits d'entrée, mais il existe aussi des portes à 3, 4, 5, bits d’entrée, voire plus. Il faut donc se demander combien il existe de portes logiques, dont les entrées font N bits. Par exemple, combien y a-t-il de portes logiques avec un bit d'entrée ? Avec deux bits d'entrée ? Avec 3 bits ?

Pour cela, un petit raisonnement peut nous donner la réponse. Vous avez vu plus haut qu'une porte logique est définie par une table de vérité, qui liste la valeur de la sortie pour toutes les combinaisons possibles de l'entrée. Pour une entrée de N bits, on a combinaisons possibles. Ensuite, calculons combien de portes logiques en tout on peut créer avec ces combinaisons. Là encore, le raisonnement est simple : chaque combinaison peut donner deux résultats en sortie : 0 et 1, le résultat de chaque combinaison est indépendant des autres, on en a X. Le résultat est donc . Pour les portes logiques à 1 bit d’entrée, cela fait 4 portes logiques. Pour les portes logiques à 2 bits d’entrée, cela fait 16 portes logiques. Voici ce que cela donne avec les portes logiques de 2 bits d'entrée.

Les 16 portes logiques à deux entrées possibles.

Dans cette section, nous allons étudier toutes les portes logiques à une et deux entrées, et allons montrer que toutes peuvent se fabriquer en combinant d'autres portes logiques de base. Par exemple, certaines portes sont l'inverse l'une de l'autre. La porte ET et la porte NAND sont l'inverse l'une de l'autre : il suffit d'en combiner une avec une porte NON pour obtenir l'autre. Même chose pour les portes OU et NOR, ainsi que les portes XOR et NXOR. Mais d'autres possibilités existent et nous allons les voir dans le détail dans ce qui suit.

Porte ET AND from NAND and NOT
Porte OU OR from NOR and NOT

Les portes logiques à un bit d'entrée modifier

Il existe quatre portes logiques de 1 bit. Il est facile de toutes les trouver avec un petit peu de réflexion, en testant tous les cas possibles.

  • La première donne toujours un zéro en sortie, c'est la porte FALSE ;
  • La seconde recopie l'entrée sur sa sortie, c'est la porte OUI, aussi appelée la porte BUFFER ;
  • La troisième est la porte NON vue plus haut ;
  • La première donne toujours un 1 en sortie, c'est la porte TRUE.
Tables de vérité des portes logiques à une entrée
Entrée FALSE OUI NON TRUE
0 0 0 1 1
1 0 1 0 1

On peut fabriquer une porte OUI en faisant suivre deux portes NON l'une à la suite de l'autre. Inverser un bit deux fois redonne le bit original.

Porte OUI/Buffer fabriquée à partie de deux portes NON.

Les portes logiques TRUE et FALSE sont des portes logiques un peu à part, qu'on appelle des portes triviales. Elles sont absolument inutiles et n'ont même pas de symbole attitré. Il est possible de fabriquer une porte FALSE à partir d'une porte TRUE suivie d'une porte NON, et inversement, de créer une porte TRUE en inversant la sortie d'une porte FALSE. Pour résumer, toutes les portes à une entrée peuvent se fabriquer en prenant une porte NON, couplée avec soit une porte FALSE, soit une porte TRUE. C'est étrange que l'on doive faire un choix arbitraire, mais c'est comme ça et la même chose arrivera quand on parlera des portes à deux entrées.

Les portes logiques à deux bits d'entrée modifier

Les portes logiques à 2 bits d'entrée sont au nombre de 16. Nous avions déjà vu les portes OU, NOR, ET, NAND, XOR et NXOR. À part ces 6 là, peu de portes logiques sont réellement utiles. Il y a bien la porte IMPLY et la porte NIMPLY, que nous allons voir dans ce qui suit, qui peuvent servir dans des explications, mais assez marginalement. La liste complète des tables de vérité est regroupée dans le tableau ci-dessous. Dans ce tableau, on retrouve les deux portes logiques triviales, à savoir la porte FALSE et TRUE, qui donnent toujours respectivement 0 et 1 en sortie.

Entrée F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15
00 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
01 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1
10 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
11 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1

On peut classer les différentes portes logiques sur un point bien précis, que nous allons expliquer immédiatement. Vous remarquerez que la table de vérité a 4 lignes, qui correspondent aux 4 combinaisons possibles des bits d'entrée : 00, 01, 10, et 11. Ensuite, les portes logiques mettent leur sortie à 1 pour certaines de ces lignes, pour certaines de ces combinaisons. Par exemple, la porte ET a une sortie à 1 pour une seule combinaison, la porte OU pour 3 combinaisons, la porte TRUE pour les 4, etc. Si on omet les portes FALSE et TRUE, on peut classer les portes en fonction du nombre de combinaisons qui mettent la sortie à 1, ce qui donne 3 classes :

  • les portes où c'est 1 lignes/combinaison ;
  • les portes où c'est 2 lignes/combinaisons ;
  • les portes où c'est 3 lignes/combinaisons.

Cette classification est essentielle pour la suite du chapitre. Nous simplifier les explications, nous parlerons de portes 0, 1, 2, 3 et 4-combinaisons. Elles sont indiquées par les couleurs dans le tableau suivant : jaune pour les 2-combinaisons, rouge pour les 1-combinaisons, et vert pour les 3-combinaisons.

Entrée FALSE NOR NIMPLY NON A NCONVERSE NON (B) XOR NAND ET NXOR OUI (B) IMPLY OUI (A) CONVERSE OU TRUE
00 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
01 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1
10 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
11 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1

On peut compter le nombre de portes dans chaque catégorie, ce qui donne 4 portes logiques 1-combinaison, 6 portes logique 2-combinaisons, 4 portes logiques 3-combinaisons.

Les portes 1-combinaison sont des dérivées de la porte ET modifier

Les portes 1-combinaison sont au nombre de quatre. Elles regroupent les deux portes NOR et ET, ainsi que deux nouvelles portes que nous allons appeler NCONVERSE et NIMPLY.

La porte NCONVERSE a la table de vérité suivante :

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 1
1 0 0
1 1 0

La porte NIMPLY a la table de vérité suivante :

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 0
1 0 1
1 1 0

Vous pouvez le voir, elles ont une sortie à 1 à condition que l'une des entrées soit à 1, et l'autre entrée soit à 0. On devine rapidement que ces deux portes peuvent se fabriquer en prenant une porte ET et une porte NON. Il suffit de mettre la porte NON devant l'entrée devant être à 0, et de mettre un ET à la suite. Au passage, cela se ressent dans les symboles utilisés pour ces deux portes, qui sont les suivants :

Porte NCONVERSE.
Porte NIMPLY.

Vous vous demandez certainement ce qui se passe quand on inverse les deux entrées avant le ET. Pour cela, regardons ce que fait le circuit en étudiant sa table de vérité.

Porte NOR fabriquée avec des portes NON et ET.
Entrée 1 Entrée 2 Sortie
0 0 1
0 1 0
1 0 0
1 1 0

C'est la table de vérité d'une porte NOR. En clair, une porte NOR est équivalente à une porte ET dont on aurait inversé les deux entrées.

Porte NOR fabriquée avec une porte ET et deux portes NON.

Pour résumer, toutes les portes 1-combinaison sont équivalentes à une porte ET dont on a inversé 0, 1 ou 2 entrées.

Les portes 3-combinaison sont des dérivées de la porte OU modifier

Les portes 3-combinaison sont au nombre de quatre. Elles regroupent les deux portes OU et NAND, ainsi que deux nouvelles portes que nous allons appeler IMPLY et CONVERSE.

La porte CONVERSE a la table de vérité suivante :

Entrée 1 Entrée 2 Sortie
0 0 1
0 1 0
1 0 1
1 1 1

La porte IMPLY a la table de vérité suivante :

Entrée 1 Entrée 2 Sortie
0 0 1
0 1 1
1 0 0
1 1 1

Leur comportement se comprend facilement quand on sait qu'elles sont équivalentes à une porte OU dont on aurait inversé une des entrées. La porte CONVERSE met sa sortie à 1 soit quand l'entrée 1 est à 1, soit quand l'entrée 2 est à 0. La porte IMPLY fait la même chose, sauf qu'il faut que l'entrée 2 soit à 1 et l'entrée 1 à 0. Leurs symboles trahissent cet état de fait, jugez-en vous-même :

Porte CONVERSE.
Porte IMPLY.

Vous vous demandez certainement ce qui se passe quand on inverse les deux entrées avant le OU. Pour cela, regardons ce que fait le circuit en étudiant sa table de vérité.

Porte NAND fabriquée avec des portes NON et OU.
Entrée 1 Entrée 2 Sortie
0 0 1
0 1 1
1 0 1
1 1 0

C'est la table de vérité d'une porte NAND. En clair, une porte NAND est équivalente à une porte ET dont on aurait inversé les deux entrées.

Porte NOR fabriquée avec une porte ET et deux portes NON.

Pour résumer, toutes les portes 3-combinaison sont équivalentes à une porte OU dont on a inversé 0, 1 ou 2 entrées.

Les autres portes modifier

Les portes 2-combinaisons sont assez simples à comprendre. Deux portes d'entre elles sont familières : il s'agit des portes XOR et NXOR. Les autres portes correspondent à des portes qui mettent leur sortie à 1 quand, respectivement : la première entrée est à 1, quand elle est à 0, quand la seconde entrée est à 1, quand la seconde entrée est à 0. Il s'agit de portes à une entrée déguisées, la seconde entrée ne servant à rien. Elles se résument donc à une porte OUI ou une porte NON selon les cas. On peut remarquer que la moitié des portes 2-combinaison sont l'inverse des autres. Si on prend les portes A, B et XOR, on peut retrouver les trois portes restantes avec une simple porte NON. Voici leurs tables de vérités regroupées ensembles.

Entrée 1 Entrée 2 A NON A B NON B XOR NXOR
0 0 0 1 1 0 0 1
0 1 0 1 0 1 1 0
1 0 1 0 0 0 1 0
1 1 1 0 1 1 0 1

Si dans les deux sections précédentes, nous avons dérivé toutes les portes 1-combinaison à partir de la porte ET, puis les portes 3-combinaison à partir de la porte OU. Il vous est peut-être venu l'idée de dériver toutes les portes 2-combinaisons à partir de la porte XOR. Cependant, si vous avez l'idée d'ajouter des portes NON en entrée d'une porte XOR/NXOR, cela ne marchera pas. Pour cela, établissons la table de vérité des quatre possibilités :

Entrée 1 Entrée 2 A XOR B A XOR (NON B) (NON A) XOR B (NON A) XOR (NON B)
0 0 0 1 1 0
0 1 1 0 0 1
1 0 1 0 0 1
1 1 0 1 1 0

On retombe sur une porte XOR ou NXOR !

Fabriquer des portes logiques basiques en combinant d'autres portes basiques modifier

Nous avons vu dans la section précédente plusieurs choses importantes, qui nous ont permis de fabriquer une bonne partie des portes logiques à partir de portes logiques NON, ET, OU, XOR, NAND, NOR. Nous avions vu :

  • que toutes les portes logiques 1-combinaisons se fabriquent avec une porte ET dont on inverse 0, 1 ou 2 entrées;
  • que les portes 3-combinaisons se fabriquent avec une porte OU dont on inverse 0, 1 ou 2 entrées ;
  • que les portes 2-combinaisons sont soit des portes OUI, des portes NON ou des portes XOR/NXOR.

Pour résumer, beaucoup de portes logiques parmi les 16 possibles peuvent se fabriquer à partir d'autres. Voici la liste des 16 portes logiques possibles. Les couleurs servent à indiquer comment elles sont fabriquées à partir de portes logiques plus simples. En rouge, on trouve les portes logiques fabriquées avec une porte OU, en la combinant ou non avec une porte NON. En vert , on trouve les portes logiques fabriquées avec une porte ET, en la combinant ou non avec une porte NON. En bleu clair, on trouve les portes logiques fabriquées avec une porte OU, en la combinant ou non avec une porte XOR. En jaune, on trouve les portes logiques fabriquées avec des portes NON.

Entrée FALSE NOR NIMPLY NON A NCONVERSE NON (B) XOR NAND ET NXOR OUI (B) IMPLY OUI (A) CONVERSE OU TRUE
00 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
01 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1
10 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
11 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1
Notons que certaines portes peuvent se fabriquer de plusieurs manières, et devraient avoir plusieurs couleurs. En théorie, la porte FALSE peut se fabriquer avec une porte XOR, mais aussi avec une porte NIMPLY ou NCONVERSE, donc avec une porte ET et une porte NON. Idem avec la porte TRUE, qui peut se fabriquer avec une porte OU et une porte NON, ou encore avec une porte XOR et une porte NON. Il en est de même avec la porte NON, qui peut en théorie être dérivée d'une porte XOR seule.

Il semblerait donc que toute porte logique puisse de fabriquer à partir de quatre portes de base : NON, ET, OU, et XOR. Une autre possibilité, équivalente, est d'utiliser les quatre portes NON, NAND, NOR et NXOR. A vrai dire, il y a plein d'autres possibilités, comme NON, ET, NOR, XOR ; ou encore NON, NAND, NOR, XOR, etc. Mais passons cela sous silence. Dans cette section, nous allons voir que l'on peut aller encore plus loin et éliminer certaines de ces portes logiques. Mais la question est : faut-il garder les portes ET/OU/XOR ou les portes NOR/NAND/NXOR ? L'un de ces choix est plus pertinent que l'autre, mais nous ne savons pas lequel pour le moment. Intuitivement, on se doute que l'on devrait retirer les portes NOR, NAND et NXOR, mais n'allons pas trop vite en besogne, nous ne sommes pas à l'abri d'une surprise.

L'élimination des portes à 2, 3 et 4-combinaisons modifier

Pour commencer, nous allons montrer que les portes XOR/NXOR peuvent se fabriquer à partir de portes NON, ET/NAND et OU/NOR. Puis, nous allons voir comment les techniques utilisées pour cette élimination marchent pour toutes les portes logiques ou presque.

L'élimination de la porte XOR modifier

Pour la porte XOR, plusieurs combinaisons de portes logiques faisant l'affaire. Une première possibilité part du principe qu'un XOR est un OU, sauf dans le cas où les deux entrées sont à 1, cas qui peut se détecter avec une porte ET. L'idée est donc la suivante : on prend une porte OU, on prend une porte ET, et on combine le résultat pour donner une porte XOR. Voici ce que cela donne :

Entrée 1 Entrée 2 OU ET XOR
0 0 0 0 0
0 1 1 0 1
1 0 1 0 1
1 1 1 1 0

La porte à choisir pour combiner les deux résultats n'est pas évidente en regardant le tableau. Il faut dire qu'on n’a pas de véritable table de vérité bien évidente, car il manque le cas avec la sortie du OU à 0 et celle du ET à 1. En réfléchissant bien, on devine qu'une porte 1-combinaison fait l'affaire, vu que seule la combinaison 1,0 met la sortie à 1. Il s'agit de la porte NIMPLY ou NCONVERSE, selon comment on câble le circuit (on peut envoyer le résultat de la porte OU sur l'entrée 1 ou 2, peut importe). L'essentiel est que l'on a juste besoin d'une porte 1-combinaison qui inverse le résultat de la porte ET. En remplacant cette porte par une portet NON et une porte ET, on obtient le circuit suivant. Notez que la porte NON a été fusionnée avec la porte ET précédente, pour obtenir un circuit avec uniquement des portes ET/OU/NON.

Porte XOR fabriquée à partir de portes ET/OU/NON, alternative.
Notons que ce circuit nous donne une idée pour créer une porte NXOR : il suffit de remplacer la porte ET finale par une porte NAND.

Une autre possibilité vient d'un raisonnement assez simple. Pour cela, partons du tableau vu plus haut :

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 1
1 0 1
1 1 0

On voit que la sortie est à 1 dans deux situations : soit l'entrée n°1 est à 1 et l'entrée 2 à 0, soit c'est l'inverse. L'idée est de créer un circuit qui vérifie si on est dans la première situation, un circuit pour l'autre situation, et de quoi combiner la sortie de ces deux circuits. Les deux premiers circuits ne sont autres que les portes 1-combinaison NCONVERSE et NIMPLY, vue précédemment.

Entrée 1 Entrée 2 NCONVERSE NIMPLY (NCONVERSE) OU (NIMPLY) = XOR
0 0 0 0 0
0 1 0 1 1
1 0 1 0 1
1 1 0 0 0

Et il se trouve que ces deux portes sont composées d'une porte ET et d'une porte NON chacune. La sortie des deux circuits est combinée avec une porte OU, car une seule des deux situations rencontrées met la sortie à 1. Le circuit obtenu est le suivant :

Porte XOR fabriquée à partir de portes ET/OU/NON.
Notons que ce circuit nous donne une idée pour créer une porte NXOR : il suffit de remplacer la porte OU finale par une porte NOR.

L'élimination de la porte NXOR modifier

Il est possible de créer une porte NXOR en plaçant une porte NON en sortie d'une porte XOR, et en simplifiant le circuit, notamment en utilisant des portes NOR/NAND. Mais il est aussi possible d'utiliser la technique précédente, à savoir combiner deux portes 1-combinaison. La porte NXOR sort un 1 soit quand ses deux entrées sont à 1, soit quand elles sont toutes deux à 0. La porte ET a sa sortie à 1 dans le premier cas, quand les deux entrées sont à 1, alors que la porte NOR (une OU suivie d'une NOT) a sa sortie à 1 quand les deux entrées sont à 0. La porte finale combine le résultat de la porte NOR avec celui de la porte ET pour obtenir un résultat valide. Vu que la sortie doit être à 1 dans l'un des deux cas, c’est-à-dire quand l'une des deux portes ET/NOR est à 1, la porte finale est naturellement une porte OU.

Porte NXOR fabriquée à partir de portes ET/OU/NON, alternative.
Notons que ce circuit nous donne une troisième possibilité pour créer une porte XOR : il suffit de remplacer la porte OU finale par une porte NOR.
Entrée 1 Entrée 2 NOR ET (NOR) OU (ET) = NXOR
0 0 1 0 1
0 1 0 0 0
1 0 0 0 0
1 1 0 1 1

Les autres portes logiques à 2, 3 et 4 combinaisons modifier

On vient de voir que l'on peut créer une porte XOR/NXOR en combinant les résultats de plusieurs portes 1-combinaison avec une porte OU. Il s'agit là d'une technique qui marche au-delà des portes XOR, et qui marche pour toutes les portes logiques à 2, 3 ou 4 combinaisons.

Prenons comme exemple la porte logique qui met la sortie à 1 pour la seconde et quatrième ligne de la table de vérité. On peut la concevoir en prenant deux portes logiques 1-combinaison : celle qui a sa sortie à 1 pour la seconde ligne de la table de vérité, et celle pour la quatrième ligne. En faisant un OU entre ces deux portes, le résultat sera la porte 2-combinaison demandé. Et on peut appliquer le même raisonnement pour n'importe quelle porte logique 2, 3 ou même 4-combinaison. Il suffit alors d'utiliser un OU à 2, 3 ou 4 entrées.

Entrée 1 Entrée 2 NCONVERSE ET Porte voulue
0 0 0 0 0
0 1 1 0 1
1 0 0 0 0
1 1 0 1 1

Cette technique permet de fabriquer directement toutes les portes logiques à deux entrées, sauf la porte FALSE. C'est la seule qui ne puisse être fabriquée à partir des portes 1-combinaison seules et qui demande d'utiliser une porte NON pour. Pour cela, il faut créer une porte TRUE avec des portes 1-combinaisons et la faire suivre d'une porte NON. En clair, à partir de toutes les portes 1-combinaison et d'une porte NON, on peut créer toutes les portes logiques à deux entrées existantes.

L'élimination des portes ET et OU modifier

Nous venons de voir que les portes XOR et NXOR sont superflues, tout comme les autres portes à 2-combinaisons. Les portes qui restent sont donc les portes à 1 et 3 combinaisons. Or, nous avions vu dans la section précédente que celles-ci peuvent se fabriquer en prenant une porte ET (portes 1-combinaison) ou OU (3-combinaison), et d'ajouter si besoin des portes NON sur ses entrées. Il nous reste donc les portes NON, ET, OU. Dans cette section, nous allons montrer que toute porte peut être créée avec seulement des portes ET et NON, sans porte OU. Nous allons aussi voir qu'il est possible de faire de même, mais avec seulement des portes NON et des portes OU, sans porte ET.

Pour comprendre pourquoi, il faut remarquer que les portes 3-combinaisons font l'exact inverse des portes 1-combinaison : les premières mettent leur sortie à zéro pour une seule combinaison d'entrée, les secondes mettent leur sortie à 1 pour une seule combinaison d'entrée. En clair, on peut obtenir les quatre portes 1-combinaison en combinant une porte 3-combinaison avec une porte NON, et inversement. Les premières étant formées avec une porte ET et des portes NON, alors que les secondes étant faites avec des portes OU et NON. On devine donc que l'on peut éliminer au choix la porte OU, la porte ET.

L'élimination de la porte OU modifier

Nous avions vu plus haut qu'il est possible de créer une porte NOR avec des NON et des ET, comme illustré ci-dessous.

Porte NOR fabriquée avec des portes NON et ET.

On peut créer une porte OU en ajoutant une porte NON au bout du circuit précédent, pour inverser son résultat.

Porte OU fabriquée avec des portes NON et ET.

L'élimination de la porte ET modifier

Nous avions vu plus haut qu'il est possible de créer une porte NAND avec des NON et des OU, comme illustré ci-dessous.

Porte NAND fabriquée avec des portes NON et OU.

On peut aussi créer une porte ET en ajoutant une porte NON au bout du circuit précédent pour inverser son résultat.

Porte ET fabriquée avec des portes NON et OU.

L'élimination de la porte NON modifier

Dans la section précédente, nous avons vu qu'il existe deux possibilités : soit on supprime les portes ET/NAND et on garde les portes OU/NOR, soit on fait l'inverse. Les deux possibilités sont équivalentes et permettent chacune de fabriquer toutes les portes logiques restantes. Cependant, supposons que je conserve les portes ET/NAND : dois-je conserver la porte ET, ou la porte NAND ? Les deux solutions ne sont pas équivalentes, car l'une permet de se passer de porte NON et pas l'autre. En effet, il est possible de remplacer les portes NON par une porte NAND ou une porte NOR ! Ce qui permet de fabriquer tout circuit avec seulement un type de porte logique : soit on construit le circuit avec uniquement des NAND, soit avec uniquement des NOR. Pour donner un exemple, sachez que les ordinateurs chargés du pilotage et de la navigation des missions Appollo étaient intégralement conçus avec des portes NOR.

Pour comprendre pourquoi, il faut savoir qu'il est possible de créer une porte OUI en utilisant une porte ET ou encore une porte OU, comme illustré ci-dessous. La raison est que si on fait un ET/OU entre un bit et lui-même, on retrouve le bit initial. Il s'agit d'une propriété particulière de la porte XOR sur laquelle nous reviendrons rapidement dans le chapitre sur les circuits combinatoires, et qui sera très utile dans le chapitre sur les opérations bit à bit. De plus, elle sera très utile vers la fin du chapitre.

Porte Buffer faite à partir d'un OU.
Porte Buffer faite à partir d'un ET.

Mais que se passe-t-il si on remplace celles-ci par une porte NAND/NOR ? La réponse est simple : on obtient une porte NON ! Pour comprendre pourquoi cela marche, il faut imaginer que la porte NAND/NOR est composée d'une porte ET/OU suivie par une porte NON. Une porte NAND/NOR dont on relie les deux entrées donne donc un ET/OU dont on relie les deux entrées, c’est-à-dire une porte OUI, suivie par une porte NON. Le bit d'entrée va subir un ET/OU avec lui-même, avant d'être inversé. Mais le passage dans le ET/OU ne changera pas le bit (cette étape se comporte comme une porte OUI), alors que la porte NON l'inversera.

Porte NON fabriquée avec des portes NAND/NOR
Circuit équivalent avec des NAND Circuit équivalent avec des NOR
Porte NON
NOT from NAND
NOT from NAND
NOT from NOR
NOT from NOR
Vous vous demandez peut-être ce qu'il se passe quand on fait la même chose avec une porte XOR, en faisant un XOR entre un bit et lui-même. Et bien le résultat est une porte FALSE. En effet, la porte XOR fournit un zéro quand les deux bits d'entrée sont identiques, ce qui est le cas quand on XOR un bit avec lui-même. Et inversement, une porte TRUE peut se fabriquer en utilisant une porte NXOR. Il s'agit là d'une propriété particulière de la porte XOR/NXOR sur laquelle nous reviendrons rapidement dans le chapitre sur les circuits combinatoires, et qui sera très utile dans le chapitre sur les opérations bit à bit.

Créer les autres portes logiques est alors un jeu d'enfant avec ce qu'on a appris dans les sections précédentes. Il suffit de remplacer les portes NON et ET par leurs équivalents fabriqués avec des NAND.

Circuit équivalent avec des NAND Circuit équivalent avec des NOR
Porte ET
AND from NAND
AND from NAND
AND from NOR
AND from NOR
Porte OU
OR from NAND
OR from NAND
OR from NOR
OR from NOR
Porte NOR
NOR from NAND
NOR from NAND
Porte NAND
NAND from NOR
NAND from NOR
Porte XOR
XOR from NAND
XOR from NAND
XOR from NOR
XOR from NOR
XOR from NAND
XOR from NAND
XOR from NOR
XOR from NOR
Porte NXOR
NXOR from NAND
NXOR from NAND
NXOR from NOR
NXOR from NOR
NXOR from NAND
NXOR from NAND
NXOR from NOR
NXOR from NOR

Les portes logiques à plus de deux entrées modifier

En théorie, les portes logiques regroupent tous les circuits à une ou deux entrées, mais pas au-delà. Mais dans les faits, certains circuits assez simples sont considérés comme des portes logiques, même s'ils ont plus de deux entrées. En fait, une porte logique est un circuit simple, qui sert de brique de base pour d'autres circuits. Il doit être raisonnablement simple et doit se fabriquer sans recourir à des portes logiques plus simples. En clair, les portes logiques sont des circuits élémentaires, et sont aux circuits électroniques ce que les atomes sont aux molécules. Dans ce qui suit, nous allons voir des portes logiques qui ont plus de 2 entrées, et en ont 3, 4, 5, voire plus. S'il est difficile d'expliquer en quoi ce sont des portes logiques, tout deviendra plus évident dans le chapitre suivant, quand nous verrons comment sont fabriquées ces portes logiques avec des transistors. Nous verrons que les circuits que nous allons voir se fabriquent très simplement en quelques transistors, sans recourir à des portes ET/OU/NAND/NOR. De plus, beaucoup de ces circuits sont très utiles et reviendront régulièrement dans la suite du cours.

Les portes ET/OU/NAND/NOR à plusieurs entrées modifier

Les premières portes logiques à plusieurs entrées que nous allons voir sont les portes ET/OU/NAND/NOR à plus de 2 entrées.

Il existe des portes ET qui ont plus de deux entrées. Elles peuvent en avoir 3, 4, 5, 6, 7, etc. Comme pour une porte ET normale, leur sortie ne vaut 1 que si toutes les entrées valent 1 : dans le cas contraire, la sortie de la porte ET vaut 0. Dit autrement, si une seule entrée vaut 0, la sortie de la porte ET vaut 0.

Porte ET à trois entrées, symbole ANSI
Porte ET à trois entrées, symbole CEI

De même, il existe des portes OU/NOR à plus de deux entrées. Pour les portes OU à plusieurs entrées, leur sortie est à 1 quand au moins une de ses entrées vaut 1. Une autre manière de le dire est que leur sortie est à 0 si et seulement si toutes les entrées sont à 0.

Porte OU à trois entrées, symbole CEI
Porte ET à trois entrées, symbole DIN

Les versions NAND et NOR existent elles aussiet leur sortie/comportement est l'inverse de celle d'une porte ET/OU à plusieurs entrées. Pour les portes NAND, leur sortie ne vaut 1 que si au moins une entrée est nulle : dans le cas contraire, la sortie de la porte NAND vaut 0. Dit autrement, si toutes les entrées sont à 1, la sortie vaut 0.

Porte NOR à trois entrées, symbole CEI
Porte NAND à trois entrées, symbole CEI
Porte ET à trois entrées conçue à partir de portes ET à deux entrées.
Porte OU à quatre entrées conçue à partir de portes OU à deux entrées.

Bien sur, ces portes logiques peuvent se créer en combinant plusieurs portes ET/OU/NOR/NAND à deux entrées. Cependant, faire ainsi n'est pas la seule solution et nous verrons dans le chapitre suivant que l'on peut faire nettement mieux avec quelques transistors. Elles sont très utiles dans la conception de circuits électroniques, mais elles sont aussi fortement utiles au niveau pédagogique. Nous en ferons un grand usage dans la suite du cours, car elles permettent de simplifier fortement les schémas et les explications pour certains circuits complexes. Sans elles, certains circuits seraient plus compliqués à comprendre et certains schémas seraient trop chargés en portes ET/OU pour être lisibles.

Les portes ET-OU-NON modifier

Les portes ET/OU/NON sont des portes logiques qui combinent plusieurs portes ET et une porte NOR en une seule porte logique. Il en existe de nombreux types, mais nous allons voir les deux principaux : les portes 2-2 et 2-1.

La porte 2-1 est une porte à 3 entrées, que nous allons appeler A, B, C, D. Elle fait un ET entre les entrées A et B, puis fait un NOR entre le résultat et la troisième entrée. Et le tout peut encore une fois s'implémenter en une seule porte logique, pas forcément en enchainant deux ou trois portes à la suite.

Porte ET-OU-NON de type 2-1, symbole.

La porte 2-2 est une porte à 4 entrées, que nous allons appeler A, B, C, D. Elle fait un ET entre les entrées A et B, un autre ET entre C et D, puis fait un NOR entre le résultat des deux ET. Et le tout peut s'implémenter en une seule porte logique, pas forcément en enchainant deux ou trois portes à la suite.

Porte ET-OU-NON de type 2-2, symbole.

Et il s'agit là des versions les plus simples de la porte, mais on peut imaginer des versions plus complexes, où les ET et les OU sont à 3, 4, voire 5 entrées.

Exemple de porte ET-OU-NON à huit entrées.

Il existe aussi des portes OU-ET-NON, où la position des portes ET et OU sont inversées.

Nous verrons dans le chapitre sur les circuits combinatoires que ces portes logiques sont très utiles ! En effet, les situations où on a une couche de portes ET suivi d'une couche de portes OU est très fréquente. Dans le chapitre sur les circuits combinatoires, nous verrons des méthodes pour concevoir n'importe quel circuit électronique (sans mémoire). La première méthode, dite des minterms, donne toujours un circuit composé d'une couche de portes NON, suivie d'une couche de portes ET, suivie d'une couche de portes OU. A l'inverse, la seconde méthode, celle des maxterms, donne toujours une couche de portes NON, suivie d'une couche de portes OU, suivie d'une couche de portes ET. Autant dire que de nombreux circuits peuvent se fabriquer avec une porte ET-OU-NON, ou du moins en combinant la sortie de plusieurs portes de ce type.

La porte à majorité modifier

La porte à majorité est une porte à plusieurs entrées, qui met sa sortie à 1 quand une plus de la moitié des entrées sont à 1, et sort un 0 sinon. En général, le nombre d'entrée de cette porte est toujours impair, afin d'éviter une situation où exactement la moitié des entrées sont à 1 et l'autre à 0. Avec un nombre impair d'entrée, il y a toujours un déséquilibre des entrées, pas une égalité parfaite. Il existe cependant des portes logiques à 4, 6, 8 entrées, mais elles sont plus rares. Dans tous les cas, une porte à majorité est actuellement fabriquée à partir de portes logiques simples (ET, OU, NON, NAND, NOR). Mais on considère que c'est une porte logique car c'est un circuit simple et assez utile. De plus, il est possible de créer une grande partie des circuits électroniques possibles en utilisant seulement des portes à majorité ! C'est surtout cette possibilité qui fait que la porte à majorité est considérée comme une porte logique, pas comme un circuit simple et utile.

Une porte à majorité à trois entrées mettra sa sortie quand deux sorties sont à 1. Il existe plusieurs possibilités pour cela, qui sont presque toutes plus simples que le circuit précédent. La plus simple utilise une couche de portes ET suivie par une porte OU à plusieurs entrées. En voici une autre :

Porte à majorité à trois bits d'entrée.

Voici le circuit d'une porte à majorité à 4 bits d'entrées :

Porte à majorité à quatre bits d'entrée.

Les deux circuits précédents nous disent comment fabriquer une porte à majorité générale. Pour la porte à trois entrée, on prend toutes les paires d'entrées possibles, on fait un ET entre les bits de chaque paire, puis on fait un OU entre le résultat des ET. Pareil pour la porte à 4 entrées : on prend toutes les combinaisons de trois entrées possibles, on fait un ET par combinaison, et on fait un OU entre tout le reste. Pour une porte à 5 entrées, on devrait utiliser là encore les combinaisons de trois entrées possibles. En fait, la recette générale est la suivante : pour une porte à N entrées, on toutes les combinaisons de (N+1)/2 entrées, on fait un ET par combinaison, puis on fait un OU entre les résultats des ET.


Dans le chapitre précédent, nous avons abordé les portes logiques. Dans ce chapitre, nous allons voir qu'elles sont fabriquées avec des composants électroniques que l'on appelle des transistors. Ces derniers sont reliés entre eux pour former des circuits plus ou moins compliqués. Pour donner un exemple, sachez que les derniers modèles de processeurs peuvent utiliser près d'un milliard de transistors.

Les transistors MOS modifier

Un transistor est un morceau de conducteur, dont la conductivité est contrôlée par sa troisième broche/borne.

Les transistors possèdent trois broches, des pattes métalliques sur lesquelles on connecte des fils électriques. On peut appliquer une tension électrique sur ces broches, qui peut représenter soit 0 soit 1. Sur ces trois broches, il y en a deux entre lesquelles circule un courant, et une troisième qui commande le courant. Le transistor s'utilise le plus souvent comme un interrupteur commandé par sa troisième broche. Le courant qui traverse les deux premières broches passe ou ne passe pas selon ce qu'on met sur la troisième.

Il existe plusieurs types de transistors, mais les deux principaux sont les transistors bipolaires et les transistors MOS. De nos jours, les transistors utilisés dans les ordinateurs sont tous des transistors MOS. Les raisons à cela sont multiples, mais les plus importantes sont les suivantes. Premièrement, les transistors bipolaires sont plus difficiles à fabriquer et sont donc plus chers. Deuxièmement, ils consomment bien plus de courant que les transistors MOS. Et enfin, les transistors bipolaires sont plus gros, ce qui n'aide pas à miniaturiser les puces électroniques. Tout cela fait que les transistors bipolaires sont aujourd'hui tombés en désuétude et ne sont utilisés que dans une minorité de circuits.

Les types de transistors MOS : PMOS et NMOS modifier

Sur un transistor MOS, chaque broche a un nom, nom qui est indiqué sur le schéma ci-dessous.On distingue ainsi le drain, la source et la grille On l'utilise le plus souvent comme un interrupteur commandé par sa grille. Appliquez la tension adéquate et la liaison entre la source et le drain se comportera comme un interrupteur fermé. Mettez la grille à une autre valeur et cette liaison se comportera comme un interrupteur ouvert.

Il existe deux types de transistors CMOS, qui diffèrent entre autres par le bit qu'il faut mettre sur la grille pour les ouvrir/fermer :

  • les transistors NMOS qui s'ouvrent lorsqu'on envoie un zéro sur la grille et se ferment si la grille est à un ;
  • et les PMOS qui se ferment lorsque la grille est à zéro, et s'ouvrent si la grille est à un.
Illustration du fonctionnement des transistors NMOS et PMOS.

Voici les symboles de chaque transistor.

Transistor CMOS
Transistor MOS à canal N (NMOS).
Transistor MOS à canal P (PMOS).

L'anatomie d'un transistor MOS modifier

À l'intérieur du transistor, on trouve simplement une plaque en métal reliée à la grille appelée l'armature, un bout de semi-conducteur entre la source et le drain, et un morceau d'isolant entre les deux. Pour rappel, un semi-conducteur est un matériau qui se comporte soit comme un isolant, soit comme un conducteur, selon les conditions auxquelles on le soumet. Dans un transistor, son rôle est de laisser passer le courant, ou de ne pas le transmettre, quand il faut. C'est grâce à ce semi-conducteur que le transistor peut fonctionner en interrupteur : interrupteur fermé quand le semi-conducteur conduit, ouvert quand il bloque le courant. La commande de la résistance du semi-conducteur (le fait qu'il laisse passer ou non le courant) est réalisée par la grille, comme nous allons le voir ci-dessous.

Transistor CMOS

Suivant la tension que l'on place sur la grille, celle-ci va se remplir avec des charges négatives ou positives. Cela va entrainer une modification de la répartition des charges dans le semi-conducteur, ce qui modulera la résistance du conducteur. Prenons par exemple le cas d'un transistor NMOS et étudions ce qui se passe selon la tension placée sur la grille. Si on met un zéro, la grille sera vide de charges et le semi-conducteur se comportera comme un isolant : le courant ne passera pas. En clair, le transistor sera équivalent à un interrupteur ouvert. Si on met un 1 sur la grille, celle-ci va se remplir de charges. Le semi-conducteur va réagir et se mettre à conduire le courant. En clair, le transistor se comporte comme un interrupteur fermé.

Transistor NMOS fermé.
Transistor NMOS ouvert.

La technologie CMOS modifier

Les portes logiques que nous venons de voir sont actuellement fabriquées en utilisant des transistors. Il existe de nombreuses manières pour concevoir des circuits à base de transistors, qui portent les noms de DTL, RTL, TLL, CMOS et bien d'autres. Les techniques anciennes concevaient des portes logiques en utilisant des diodes, des transistors bipolaires et des résistances. Mais elles sont aujourd'hui tombées en désuétudes dans les circuits de haute performance.De nos jours, on n'utilise que des logiques MOS (Metal Oxyde Silicium), qui utilisent des transistors MOS vus plus haut dans ce chapitre, parfois couplés à des résistances. On distingue :

  • La logique NMOS, qui utilise des transistors NMOS associés à des résistances.
  • La logique PMOS, qui utilise des transistors PMOS associés à des résistances.
  • La logique CMOS, qui utilise des transistors PMOS et NMOS, sans résistances.

Dans cette section, nous allons montrer comment fabriquer des portes logiques en utilisant la technologie CMOS. Avec celle-ci, chaque porte logique est fabriquée à la fois avec des transistors NMOS et des transistors PMOS. On peut la voir comme un mélange entre la technologie PMOS et NMOS. Tout circuit CMOS est divisé en deux parties : une intégralement composée de transistors PMOS et une autre de transistors NMOS. Chacune relie la sortie du circuit soit à la masse, soit à la tension d'alimentation.

Principe de conception d'une porte logique/d'un circuit en technologie CMOS.

La première partie relie la tension d'alimentation à la sortie, mais uniquement quand la sortie doit être à 1. Si la sortie doit être à 1, des transistors PMOS vont se fermer et connecter tension et sortie. Dans le cas contraire, des transistors s'ouvrent et cela déconnecte la liaison entre sortie et tension d'alimentation. L'autre partie du circuit fonctionne de la même manière que la partie de PMOS, sauf qu'elle relie la sortie à la masse et qu'elle se ferme quand la sortie doit être mise à 0

Fonctionnement d'un circuit en logique CMOS.

Dans ce qui va suivre, nous allons étudier la porte NON, la porte NAND et la porte NOR. La porte de base de la technologie CMOS est la porte NON, les portes NAND et NOR ne sont que des versions altérées de la porte NON qui ajoutent des entrées et quelques transistors. Les autres portes, comme la porte ET et la porte OU, sont construites à partir de ces portes. Nous parlerons aussi de la porte XOR, qui est un peu particulière.

La porte NON modifier

Cette porte est fabriquée avec seulement deux transistors, comme indiqué ci-dessous.

Porte NON fabriquée avec des transistors CMOS.

Si on met un 1 en entrée de ce circuit, le transistor du haut va fonctionner comme un interrupteur ouvert, et celui du bas comme un interrupteur fermé : la sortie est reliée au zéro volt, et vaut donc 0. Inversement, si on met un 0 en entrée de ce petit montage électronique, le transistor du bas va fonctionner comme un interrupteur ouvert, et celui du haut comme un interrupteur fermé : la sortie est reliée à la tension d'alimentation, et vaut donc 1.

Porte NON fabriquée avec des transistors CMOS - fonctionnement.

Les portes logiques à deux entrées modifier

Passons maintenant aux portes logiques à deux entrées. Pour celles-ci, on devra utiliser plus de transistors, au moins deux par entrée. Il existe plusieurs manières de relier deux transistors, mais celle qui va nous intéresser en premier lieu est la suivante : deux transistors en série, c'est-à-dire l'un à la suite de l'autre. On peut mettre deux transistors PMOS en série ou deux transistors NMOS en série. Il y aura un transistor PMOS par entrée, un NMOS par entrée. Étudions plus en détail les deux cas.

On rappelle que les transistors se ferment si l'entrée vaut 0 pour des transistors PMOS, 1 pour des NMOS. Prenons deux transistors PMOS en série, chacun associé à son entrée. Pour que la connexion se fasse entre tension d'alimentation et sortie, il faudra que les deux transistors se ferment, ce qui demande que les deux entrées soient à 0. C'est la seule possibilité, et cela laisse de côté le cas où une seule entrée est à 0, ainsi que le cas où les deux entrées sont à 1. Pour gérer ces trois cas, il suffit d'inverser une entrée, voire les deux, avec des portes logiques NON.

Avec les transistors NMOS, c'est la même chose. Avec deux transistors NMOS, la connexion est fermée quand les deux entrées sont à 1. On peut gérer les autres cas avec des portes NON. Le tout est illustré ci-dessous.

Transistors CMOS en série

Mine de rien, avec ces 8 montages de base, on peut créer n'importe quelle porte logique à deux entrées. Nous allons notamment voir dans la section suivante comment faire une porte XOR avec. Il suffit de prendre les 4 montages adéquats par les 8 précédents pour obtenir la porte logique voulue. Rappelons que d'après les règles du CMOS, les deux transistors PMOS se placent entre la tension d'alimentation et la sortie, et servent à mettre la sortie à 1. Pour les deux transistors NMOS, ils sont reliés à la masse et mettent la sortie à 0. Pour mieux comprendre, prenons l'exemple d'une porte XOR.

Un exemple d'application avec la porte XOR modifier

Il est possible de créer une porte XOR en combinant d'autres portes logiques, mais la méthode que je vais expliquer donne un résultat plus économe en circuit, sans compter que la méthode en question marche pour toute porte logique à deux entrées. L'idée est très simple : on prend la table de vérité de la porte logique, et on associe deux transistors en série pour chaque ligne. Regardons d'abord la table de vérité ligne par ligne :

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 1
1 0 1
1 1 0

La première ligne a ses deux entrées à 0 et sort un 0. La sortie est à 0, ce qui signifie qu'il faut regarder sur la ligne des transistors NMOS, qui connectent la sortie à la masse. Le montage qui se ferme quand les deux entrées sont à 0 est celui tout en bas à droite du tableau précédent, à savoir deux transistors NMOS avec deux portes NON.

Les deux lignes du milieu ont une entrée à 0 et une à 1, et leur sortie à 1. La sortie à 1 signifie qu'il faut regarder sur la ligne des transistors PMOS, qui connectent la tension d'alimentation à la sortie. Les deux montages avec deux entrées différentes sont les deux situés au milieu, avec deux transistors PMOS et une porte logique.

La dernière ligne a ses deux entrées à 1 et sort un 0. La sortie est à 0, ce qui signifie qu'il faut regarder sur la ligne des transistors NMOS, qui connectent la sortie à la masse. Le montage qui se ferme quand les deux entrées sont à 1 est celui tout en bas à gauche du tableau précédent, à savoir deux transistors NMOS seuls.

En combinant ces quatre montages, on trouve le circuit suivant. Notons qu'il n'y a que deux portes NON marquées en vert et bleu : on a juste besoin d'inverser la première entrée et la seconde, pas besoin de portes en plus. Les portes NOn sont en quelque sorte partagées entre les transistors PMOS et NMOS.

Porte XOR en logique CMOS.

Si les deux entrées sont à 1, alors les deux transistors en bas à gauche vont se fermer et connecter la sortie au 0 volt, les trois autres groupes ayant au moins un transistor ouvert. Si les deux entrées sont à 0, alors les deux transistors en bas à droite vont se fermer et connecter la sortie au 0 volt, les autres quadrants ayant au moins un transistor ouvert. Et pareil quand les deux bits sont différents : un des deux quadrants aura ses deux transistors fermés, alors que les autres auront au moins un transistor ouvert, ce qui connecte la sortie à la tension d'alimentation.

On peut construire la porte NXOR sur la même logique. Et toutes les portes logiques peuvent se construire avec cette méthode. Le nombre de transistors est alors le même : on utilise 12 transistors au total : 4 paires de transistors en série, 4 transistors en plus pour les portes NON. Que ce soit pour la porte XOR ou NXOR, on économise beaucoup de transistors comparés à la solution naïve, qui consiste à utiliser plusieurs portes NON/ET/OU. Si on ne peut pas faire mieux dans le cas de la porte XOR/NXOR, sachez cependant que les autres portes construites avec cette méthode utilisent plus de transistors que nécessaire. De nombreuses simplifications sont possibles, comme on le verra plus bas.

Les portes NAND et NOR modifier

Pour simplifier une porte NOR/NAND en CMOS, on doit câbler les transistors d'une certaine façon. On retrouve des transistors en série (l'un après l'autre, sur le même fil), mais on trouve aussi des transistors en parallèle (sur des fils différents). Le tout est illustré ci-dessous. Les transistors en série ferment la connexion quand toutes les entrées sont à 1 (NMOS) ou 0 (PMOS). A l'inverse, pour les transistors en parallèle, il faut qu'une seule entrée soit à la bonne valeur pour que la connexion se fasse. L'usage de transistors PMOS/NMOS en parallèle permet de faire de nombreuses simplifications.

Transistors CMOS en série et en parallèle

Une porte NOR met sa sortie à 1 si toutes les entrées sont à 0, à 0 si une seule entrée vaut 1. Pour reformuler, il faut connecter la sortie à la tension d'alimentation si toutes les entrées sont à 0, ce qui demande d'utiliser des transistors PMOS en série. Le second cas (une seule entrée à 1) peut théoriquement se faire en combinant plusieurs transistors en séries : deux NMOS en série pour le cas avec les deux entrées à 1, deux pour le cas où la première entrée est à 1 et l'autre à 0, etc. Mais on peut faire autrement et directement utiliser deux transistors en parallèle. Il y a alors deux cas : soit la première entrée vaut 1, soit l'autre vaut 1. On place alors un transistor NMOS pour chaque possibilité, là encore entre la masse et la sortie.

Le circuit obtenu est donc celui obtenu dans le premier schéma. Le même raisonnement pour une porte NAND donne le second schéma.

Porte NOR fabriquée avec des transistors.
Porte NAND fabriquée avec des transistors.

Leur fonctionnement s'explique assez bien si on regarde ce qu'il se passe en fonction des entrées. Suivant la valeur de chaque entrée, les transistors vont se fermer ou s'ouvrir, ce qui va connecter la sortie soit à la tension d'alimentation, soit à la masse.

Voici ce que cela donne pour une porte NAND :

Porte NAND fabriquée avec des transistors.

Voici ce que cela donne pour une porte NOR :

Porte NOR fabriquée avec des transistors.

Les autres portes logiques : ET/OU/XOR/NXOR modifier

En logique CMOS, les portes logiques ET et OU sont construites en prenant une porte NAND/NOR et en mettant une porte NON sur sa sortie. Il est théoriquement possible d'utiliser uniquement des transistors en série et en parallèle, mais cette solution utilise plus de transistors.

Porte ET en CMOS
Porte OU en CMOS

Avec ces portes, il est possible de créer d'autres portes. Par exemple, on peut construire une porte XOR avec seulement 10 transistors, et non 12 avec la méthode de base.

Porte XOR en CMOS en 10 transistors.

Les portes logiques à trois entrées ou plus modifier

Tout ce qui a été dit plus haut sur les transistors en série et en parallèle vaut aussi pour les portes logiques à trois entrées ou plus. Il est possible de créer des portes logiques à 3 entrées avec uniquement des paquets de 3 transistors en série, mais c'est rarement une bonne idée. La quasi-totalité de ces portes gagnent à utiliser des transistors en parallèle.

Les portes NAND/NOR/ET/OU à plusieurs entrées modifier

Les portes NOR/NAND à plusieurs entrées sont construites comme les portes ET et OU à deux entrées, mais en rajoutant des transistors. Il y a autant de transistors en série que d'entrée, pareil pour les transistors en parallèle. Leur fonctionnement est similaire à leurs cousines à deux entrées. Les portes ET et OU à plusieurs entrées sont construites à partie de NAND/NOR suivies d'une porte NON.

NAND plusieurs entrées
NOR plusieurs entrées

En théorie, on pourrait créer des portes avec un nombre arbitraire d'entrées avec cette méthode. Cependant, au-delà d'un certain nombre de transistors en série/parallèle, les performances s'effondrent rapidement. Le circuit devient alors trop lent, sans compter que des problèmes purement électriques surviennent. En pratique, difficile de dépasser la dizaine d'entrées. Dans ce cas, les portes sont construites en assemblant plusieurs portes NAND/NOR ensemble. Et faire ainsi marche nettement mieux pour fabriquer des portes ET/OU que pour des portes NAND/NOR.

On s'attend à ce qu'une porte ET/OU avec beaucoup d'entrées soit construite en combinant plusieurs portes ET/OU. Il existe cependant une alternative qui se marie nettement mieux avec la logique CMOS. Rappelons qu'en logique CMOS, les portes NAND et NOR sont les portes à plusieurs entrées les plus simples à fabriquer. L'idée est alors de combiner des portes NAND/NOR pour créer une porte ET/OU.

Voici la comparaison entre les deux solutions pour une porte ET :

ET plusieurs entrées
ET plusieurs entrées

Voici la comparaison entre les deux solutions pour une porte OU :

OU plusieurs entrées
OU plusieurs entrées

D'autres portes mélangent transistors en série et en parallèle d'une manière différente. Les portes ET-OU-NON et OU-ET-NON en sont un bon exemple.

Les portes ET-OU-NON et OU-ET-NON modifier

Il est possible de créer des portes ET-OU-NON et OU-ET-NON assez simplement en CMOS. La solution la plus simple est de combiner des portes ET et une porte NOR, mais il est possible de faire beaucoup plus simple, comme indiqué dans le schéma ci-dessous.

3-1-OAI

Le schéma ci-contre montre l'implémentation d'une porte OU-ET-NON, où l'on fait un OU entre les trois premières entrées, avant de faire un ET avec la quatrième, puis un NON sur le résultat. On voit qu'on arrive à se débrouiller avec seulement quatre transistors, ce qui est une sacrée économie comparé à une implémentation naïve avec trois portes logiques.

Le schéma suivant compare l'implémentation d'une porte ET-OU-NON de type 2-1, à savoir qu'elle fait un ET entre les deux premières entrées, puis un NOR entre le résultat du ET et la troisième entrée. L'implémentation à droite du schéma avec une porte ET et une porte NOR prend 10 transistors. L'implémentation la plus simple, à gauche du schéma, prend seulement 6 transistors.

Porte ET-OU-NON à trois entrées (de type 2-1) à gauche, contre la combinaison de plusieurs portes à droite.

La logique pass transistor logic modifier

La pass transistor logic est une forme particulière de technologie CMOS, une version non-conventionnelle. Avec le CMOS normal, la porte de base est la porte NON. En modifiant celle-ci, on arrive à fabriquer des portes NAND, NOR, puis les autres portes logiques. Les transistors sont conçus de manière à connecter la sortie, soit la tension d'alimentation, soit la masse. Avec la pass transistor logic, le montage de base est un circuit interrupteur, qui fonctionne autrement. Cette version du CMOS a été utilisée dans des processeurs commerciaux, comme dans l'ARM1. Dans la suite du cours, nous verrons quelques circuits qui utilisent cette technologie, mais ils seront rares. Nous l'utiliserons quand nous parlerons des additionneurs, ou les multiplexeurs, guère plus. Mais il est sympathique de savoir que cette technologie existe.

La porte à transmission modifier

Le circuit de base est un interrupteur construit avec deux transistors. Pourquoi ne pas utiliser un seul transistor par interrupteur ? C'est parce que la logique CMOS fait que tout transistor PMOS doit être associé à un transistor NMOS et réciproquement. Donc, deux transistors. Le montage interrupteur de base est appelé une porte à transmission. C'est un petit circuit avec trois entrées : une entrée de commande, une entrée et une sortie. Le circuit peut soit connecter l'entrée et la sortie, soit déconnecter la sortie de l'entrée. Le choix entre les deux dépend de l’entrée de commande. Le montage de base est le suivant :

CMOS Transmission gate
Les deux entrées A et /A sont l'inverse l'une de l'autre, ce qui fait qu'il faut en théorie rajouter une porte NON CMOS normale, pour obtenir le circuit complet. Mais dans les faits, on arrive souvent à s'en passer. Ce qui fait que la porte à transmission est définie comme étant le circuit à deux transistors précédents.

Le schéma ci-dessous nous permet de comprendre quels sont les défauts de la pass transistor logic. Il n'y a ni tension d'alimentation, ni masse (O Volts). Par contre, la sortie d'une porte à transmission est alimentée par la tension d'entrée, ce qui fait qu'il n'y a pas d'amplification de la tension d'entrée. Et vu que les transistors ne sont pas parfaits, on a toujours une petite perte de tension en sortie d'une porte à transmission. Le résultat est que si on enchaine les portes à transmission, la tension de sortie a tendance à diminuer, et ce d'autant plus vite qu'on a enchainé de portes à transmission. le résultat est qu'il faut souvent rajouter des portes amplificatrices pour restaurer les tensions adéquates, à divers endroits du circuit. Ces portes amplificatrices sont composées d'une ou de deux portes NON en CMOS normal. La pass transistor logic mélange donc porte NON CMOS normales avec des portes à transmission. De plus, afin de faire des économies de circuit, on n'utilise souvent qu'une seule porte NON CMOS comme amplificateur, ce qui fait que de nombreux signaux sont inversés dans le circuit.

Par contre, ce défaut entraine aussi des avantages. Notamment, la consommation d'énergie est fortement diminuée. Seules les portes amplificatrices, les portes NON CMOS, sont alimentées en tension/courant. Le reste des circuits n'est pas alimenté, car il n'y a pas de connexion à la tension d'alimentation et la masse.

Les portes à transmission sont très utilisés dans certains circuits très communs, que nous aborderons dans quelques chapitres, comme les multiplexeurs ou les démultiplexeurs.

La porte XOR en pass transistor logic modifier

Il est facile d'implémenter une porte XOR avec des portes à transmission. Cela demande deux portes à transmission, plus quelques portes NON, pas plus.

Porte XOR implémentée avec une porte à transmission.

La version précédente est une porte XOR où les signaux d'entrée sont doublés : on a le bit d'entrée original, et son inverse. C'est quelque chose de fréquent dans les circuits en pass transistor logic, où les signaux/bits sont doublés. Mais il est possible de créer des versions normales, sans duplication des bits d'entrée. La solution la plus simple de rajouter deux portes NON, pour inverser les deux entrées. Le circuit passe donc de 4 à 8 transistors, ce qui reste peu. Mais on peut ruser, ce qui donne le circuit ci-dessous. Comme vous pouvez les voir, il mélange porte à transmission et portes NON CMOS normales.

XOR en pass transistor logic

Dans les deux cas, l'économie en transistors est drastique comparé au CMOS normal. Plus haut, nous avons illustré plusieurs versions possibles d'une porte XOR en CMOS normal, toutes de 12 transistors. Ici, on va de 6 transistors maximum, à seulement 4 ou 5 pour les versions plus simples. Le gain est clairement significatif, suffisamment pour que les circuits avec beaucoup de portes XOR gagnent à être implémentés avec la pass transistor logic.

Les technologies PMOS et NMOS modifier

Dans ce qui va suivre, nous allons voir la technologie NMOS et POMS. Pour simplifier, la technologie NMOS est équivalente aux circuits CMOS, sauf que les transistors PMOS sont remplacés par une résistance. Pareil avec la technologie PMOS, sauf que c'est les transistors NMOS qui sont remplacés par une résistance. Les deux technologies étaient utilisées avant l'invention de la technologie CMOS, quand on ne savait pas comment faire pour avoir à la fois des transistors PMOS et NMOS sur la même puce électronique, mais sont aujourd'hui révolues. Nous en parlons ici, car nous évoquerons quelques circuits en PMOS/NMOS dans le chapitre sur les cellules mémoire, mais vous pouvez considérer que cette section est facultative.

Le fonctionnement des logiques NMOS et PMOS modifier

Avec la technologie NMOS, les portes logiques sont fabriqués avec des transistors NMOS intercalés avec une résistance.

Circuit en logique NMOS.

Leur fonctionnement est assez facile à expliquer. Quand la sortie doit être à 1, tous les transistors sont ouverts. La sortie est connectée à la tension d'alimentation et déconnectée de la masse, ce qui fait qu'elle est mise à 1. La résistance est là pour éviter que le courant qui arrive dans la sortie soit trop fort. Quand au moins un transistor NMOS qui se ferme, il connecte l'alimentation à la masse, les choses changent. Les lois compliquées de l'électricité nous disent alors que la sortie est connectée à la masse, elle est donc mise à 0.

Fonctionnement d'un circuit en technologie NMOS.

Les circuits PMOS sont construits d'une manière assez similaire aux circuits CMOS, si ce n'est que les transistors NMOS sont remplacés par une résistance qui relie ici la masse à la sortie. Rien d'étonnant à cela, les deux types de transistors, PMOS et NMOS, ayant un fonctionnement inverse.

Les portes logiques en NMOS et PMOS modifier

Que ce soit en logique PMOS ou NMOS, les portes de base sont les portes NON, NAND et NOR. Les autres portes sont fabriquées en combinant des portes de base. Voici les circuits obtenus en NMOS et PMOS:

NMOS
Porte NON NMOS. NMOS-NAND NMOS-NOR NMOS AND NMOS OR
PMOS
PMOS NOT PMOS NAND PMOS NOR PMOS OR

Les portes logiques de base en NMOS modifier

Le circuit d'une porte NON en technologie NMOS est illustré ci-dessous. Le principe de ce circuit est similaire au CMOS, avec quelques petites différences. Si on envoie un 0 sur la grille du transistor, il s'ouvre et connecte la sortie à la tension d'alimentation à travers la résistance. À l'inverse, quand on met un 1 sur la grille, le transistor se ferme et la sortie est reliée à la masse, donc mise à 0. Le résultat est bien un circuit inverseur.

Porte NON NMOS. Porte NON NMOS : fonctionnement.

La porte NOR est similaire à la porte NON, si ce n'est qu'il y a maintenant deux transistors en parallèle. Si l'une des grilles est mise à 1, son transistor se fermera et la sortie sera mise à 0. Par contre, quand les deux entrées sont à 0, les transistors sont tous les deux ouverts, et la sortie est mise à 1. Le comportement obtenu est bien celui d'une porte NOR.

NMOS-NOR-gate Fonctionnement d'une porte NOR NMOS.

La porte NAND fonctionne sur un principe similaire au précédent, si ce n'est qu'il faut que les deux grilles soient à zéro pour obtenir une sortie à 1. Pour mettre la sortie à 0 quand seulement les deux transistors sont ouverts, il suffit de les mettre en série, comme dans le schéma ci-dessous. Le circuit obtenu est bien une porte NAND.

NMOS-NAND-gate
NMOS-NAND-gate
Funktionsprinzip eines NAND-Gatters
Funktionsprinzip eines NAND-Gatters

Les avantages et inconvénients des technologies CMOS, PMOS et NMOS modifier

La technologie PMOS et NMOS ne sont pas totalement équivalentes, niveau performances. Ces technologies se distinguent sur plusieurs points : la vitesse des transistors et leur consommation énergétique.

La vitesse des circuits NMOS/PMOS/CMOS dépend des transistors eux-mêmes. Les transistors PMOS sont plus lents que les transistors NMOS, ce qui fait que les circuits NMOS sont plus rapides que les circuits PMOS. Les circuits CMOS ont une vitesse intermédiaire, car ils contiennent à la fois des transistors NMOS et PMOS.

Pour la consommation électrique, les résistances sont plus goumandes que les transistors. En PMOS et NMOS, la résistance est traversée par du courant en permanence, peu importe l'état des transistors. Et résistance traversée par du courant signifie consommation d'énergie, dissipée sous forme de chaleur par la résistance. Il s'agit d'une perte sèche d'énergie, une consommation d'énergie inutile. En CMOS, l'absence de résistance fait que la consommation d'énergie est liée aux transistors, et celle-ci est beaucoup plus faible que pour une résistance.

Les transistors PMOS sont plus simples à fabriquer que les NMOS, ils sont plus simples à sortir d'usine. Les premiers processeurs étaient fabriqués en logique PMOS, plus simple à fabriquer. Puis, une fois la fabrication des circuits NMOS maitrisée, les processeurs sont tous passés en logique NMOS du fait de sa rapidité. La logique CMOS a mis du temps à remplacer les logiques PMOS et NMOS, car il a fallu maitriser les techniques pour mettre à la fois des transistors NMOS et PMOS sur la même puce. Les premières puces électroniques étaient fabriquées en PMOS ou en NMOS, parce qu'on n’avait pas le choix. Mais une fois la technologie CMOS maitrisée, elle s'est imposée en raison de deux gros avantages : une meilleure fiabilité (une meilleure tolérance au bruit électrique), et une consommation électrique plus faible.

La logique TTL : quelques remarques superficielles modifier

Tous ce que nous avons vu depuis le début de ce chapitre porte sur les transistors MOS et les technologies associées. Mais les transistors MOS n'ont pas été les premiers inventés. Ils ont été précédés par les transistors bipolaires. Nous ne parlerons pas en détail du fonctionnement d'un transistor bipolaire, car celui-ci est extraordinairement compliqué. Cependant, nous devons parler rapidement de la logique TTL, qui permet de fabriquer des portes logiques avec ces transistors bipolaires. Là encore, rassurez-vous, nous n'allons pas voir comment fabriquer des portes logiques en logique TTL, cela serait trop compliqué, sans compter que le but n'est pas de faire un cours d'électronique. Mais nous devons fait quelques remarques et donner quelques explications superficielles.

La raison à cela est double. La première raison est que certains circuits présents dans les mémoires RAM sont fabriqués avec des transistors bipolaires. C'est notamment le cas des amplificateurs de lecture ou d'autres circuits de ce genre. De tels circuits ne peuvent pas être implémentés facilement avec des transistors CMOS et nous expliquerons rapidement pourquoi dans ce qui suit. La seconde raison est que ce cours parlera occasionnellement de circuits anciens et qu'il faut quelques bases sur le TTL pour en parler.

Dans la suite du cours, nous verrons occasionnellement quelques circuits anciens, pour la raison suivante : ils sont très simples, très pédagogiques, et permettent d'expliquer simplement certains concepts du cours. Rien de mieux que d'étudier des circuits réels pour donner un peu de chair à des explications abstraites. Par exemple, pour expliquer comment fabriquer une unité logique de calcul bit à bit, je pourrais utiliser l'exemple du Motorola MC14500B, un processeur 1 bit qui est justement une unité logique sous stéroïdes. Ou encore, dans le chapitre sur les circuits additionneurs, je parlerais du circuit additionneur présent dans l'Intel 8008 et dans l'Intel 4004, les deux premiers microprocesseurs commerciaux. Malheureusement, malgré leurs aspects pédagogiques indéniables, ces circuits ont le défaut d'être des circuits TTL. Ce qui est intuitif : les circuits les plus simples ont été inventés en premier et utilisent du TTL plus ancien. Beaucoup de ces circuits ont été inventés avant même que le CMOS ou même les transistors MOS existent. D'où le fait que nous devons faire quelques explications mineures sur le TTL.

Les transistors bipolaires modifier

Les transistors bipolaires ressemblent beaucoup aux transistors MOS. Les transistors bipolaires ont trois broches, appelées le collecteur, la base et l'émetteur. Notez que ces trois termes sont différents de ceux utilisés pour les transistors MOS, où on parle de la grille, du drain et de la source.

Là encore, comme pour les transistors PMOS et NMOS, il existe deux types de transistors bipolaires : les NPN et les PNP. Là encore, il est possible de fabriquer une puce en utilisant seulement des NPN, seulement des PNP, ou en mixant les deux. Mais les ressemblances s'arrêtent là. La différence entre PNP et NPN tient dans la manière dont les courants entrent ou sortent du transistor. La flèche des symboles ci-dessous indique si le courant rentre ou sort par l'émetteur : il rentre pour un PNP, sort pour un NPN. Dans la suite du cours, nous n'utiliserons que des transistors NPN, les plus couramment utilisés.

BJT PNP
BJT NPN

Plus haut nous avons dit que les transistors CMOS sont des interrupteurs. La réalité est que tout transistor peut être utilisé de deux manières : soit comme interrupteur, soit comme amplificateur de tension/courant. Pour simplifier, le transistor bipolaire NPN prend en entrée un courant sur sa base et fournit un courant amplifié sur l'émetteur. Pour s'en servir comme amplificateur, il faut fournir une source de courant sur le collecteur. Le fonctionnement exact est cependant plus compliqué.

Transistor bipolaire, explication simplifiée de son fonctionnement

Les transistors bipolaires sont de bons amplificateurs, mais de piètres interrupteurs. A l'inverse, les transistors CMOS sont généralement de bons interrupteurs, mais de moyens amplificateurs. Pour des circuits numériques, la fonction d'interrupteur est clairement plus adaptée, car elle-même binaire (un transistor est fermé ou ouvert : deux choix possibles). Aussi, les circuits modernes privilégient des transistors CMOS aux transistors bipolaires. A l'inverse, la fonction d'amplification est adaptée aux circuits analogiques.

C'est pour ça que nous rencontrerons les transistors bipolaires soit dans des portions de l'ordinateur qui sont au contact de circuits analogiques. Pensez par exemple aux cartes sons ou au vieux écrans cathodiques, qui gèrent des signaux analogiques (le son pour la carte son, les signaux vidéo analogique pour les vieux écrans). On les croisera aussi dans les mémoires DRAM, dont la conception est un mix entre circuits analogiques et numériques. Nous les croiserons aussi dans de vieux circuits antérieurs aux transistors MOS. Les anciens circuits faisaient avec les transistors bipolaires car ils n'avaient pas le choix, mais ils ont été partiellement remplacés dès l'apparition des transistors CMOS.

Les portes logiques complexes en TTL modifier

Le détail le plus important qui nous concernera dans la suite du cours est le suivant : on peut créer des portes logiques exceptionnellement complexes en TTL. Pour comprendre pourquoi, sachez qu'il existe des transistors bipolaires qui possèdent plusieurs émetteurs. Ils sont très utilisés pour fabriquer des portes logiques à plusieurs entrées. Les émetteurs correspondent alors à des entrées de la porte logique. Ainsi, une porte logique à plusieurs entrées se fait non pas en ajoutant des transistors, comme c'est le cas avec les transistors MOS, mais en ajoutant un émetteur sur un transistor. Cela permet à une porte NAND à trois entrées de n'utiliser que deux transistors bipolaires, au lieu de quatre transistors MOS.

Transistor bipolaire avec plusieurs émetteurs.

De plus, là où les logiques PMOS/NMOS/CMOS permettent de fabriquer les portes de base que nous avons précédemment, elles ne peuvent pas faire plus. Au pire, on peut implémenter des portes ET/OU/NAND/NOR à plusieurs entrées, mais pas plus. En TTL, on peut parfaitement créer des portes de type ET/OU/NON ou OU/ET/NON, avec seulement quatre transistors. Par exemple, une porte ET/OU/NON de type 2-2 entrées (pour rappel, qui effectue un ET par paire d’entrée puis fait un NOR entre le résultat des deux ET) est bien implémenté en une seule porte logique, pas en enchainant deux ou trois portes à la suite.

TTL AND-OR-INVERT 1961

Les désavantages et avantages des circuits TTL modifier

Pour résumer, le TTL à l'avantage de pouvoir fabriquer des portes logiques avec peu de transistors comparé au CMOS, surtout pour les portes logiques complexes. Et autant vous dire que les concepteurs de puce électroniques ne se gênaient pas pour utiliser ces portes complexes, capables de fusionner 3 à 5 portes en une seule : les économies de transistors étaient conséquentes.

Et pourtant, les circuits TTL étaient beaucoup plus gros que leurs équivalents CMOS. La raison est qu'un transistor bipolaire prend beaucoup de place : il est environ 10 fois plus gros qu'un transistor MOS. Autant dire que les économies réalisées avec des portes logiques complexes ne faisaient que compenser la taille énorme des transistors bipolaires. Et encore, cette compensation n'était que partielle, ce qui fait que les circuits PMOS/NMOS/CMOS se miniaturisent beaucoup plus facilement. Un avantage pour le transistor MOS !

De plus, les schémas précédents montrent que les portes logiques en TTL utilisent une résistance, elle aussi difficile à miniaturiser. Et cette résistance est parcourue en permanence par un courant, ce qui fait qu'elle consomme de l'énergie et chauffe. C'est la même chose en logique NMOS et PMOS, ce qui explique leur forte consommation d'énergie. Les circuits TTL ont donc le même problème.

TTL voltage.

Un autre défaut est lié à la une tension d'alimentation. Les circuits TTL utilisent une tension d'alimentation de 5 volts, alors que les circuits CMOS ont une tension d'alimentation beaucoup plus variable. Les circuits CMOS vont de 3 volts à 18 volts pour les circuits commerciaux, avec des tensions de 1 à 3 volts pour les circuits optimisés. Les circuits CMOS sont généralement bien optimisés et utilisent une tension d'alimentation plus basse que les circuits TTL, ce qui fait qu'ils consomment moins d'énergie et de courant.

De plus, rappelons que coder un zéro demande que la tension soit sous un seuil, alors que coder un 1 demande qu'elle dépasse un autre seuil, avec une petite marge de sécurité entre les deux. Les seuils en question sont indiqués dans le diagramme ci-dessous. Il s'agit des seuils VIH et VIL. On voit que sur les circuits TTL, la marge de sécurité est plus faible qu'avec les circuits CMOS. De plus, les marges sont bien équilibrées en CMOS, à savoir que la marge de sécurité est en plein milieu entre la tension max et le zéro volt. Avec le TTL normal, la marge de sécurité est très proche du zéro volt. Un 1 est codé par une tension entre 2 et 5 volts en TTL ! Une version améliorée du TTL, le LVTTL, corrige ce défaut. Elle baisse la tension d'alimentation à 3,3 Volts, mais elle demande des efforts de fabrication conséquents.

Niveaux logiques CMOS-TTL-LVTTL


Broches du processeur MOS6502.

De nos jours, les portes logiques et/ou transistors sont rassemblés dans des circuits intégrés. Les circuits intégrés se présentent le plus souvent sous la forme de boitiers rectangulaires, comme illustré ci-contre. D'autres ont des boitiers de forme carrées, comme ceux que l'on peut trouver sur les barrettes de mémoire RAM, ou à l'intérieur des clés USB/ disques SSD. Enfin, certains circuits intégrés un peu à part ont des formes bien différentes, comme les processeurs ou les mémoires RAM.

À l'intérieur de ces circuits intégrés, on trouve un grand nombre de transistors et de portes logiques qui sont reliés entre eux et/ou connectés aux broches d'entrée/sortie. Les circuits intégrés contiennent un grand nombre de transistors. Par exemple, les derniers modèles de processeurs peuvent utiliser près d'un milliard de transistors. Cette orgie de transistors permet d'ajouter des fonctionnalités aux composants électroniques. C'est notamment ce qui permet aux processeurs récents d'intégrer plusieurs cœurs, une carte graphique, etc.

L'interface d'un circuit intégré modifier

Les circuits intégrés ont, comme les portes logiques, des broches métalliques sur lesquelles on envoie des tensions, chaque broche pouvant servir soit d'entrée, soit de sortie, soit les deux. Certaines de ces broches vont recevoir la tension d'alimentation (broche VCC), d'autres vont être reliées à la masse (broche GND), d'autres vont porter des bits de données ou de contrôle.

Les broches modifier

La plupart des circuits actuels, processeurs et mémoires, comprennent un grand nombre de broches : plusieurs centaines ! Si on prend l'exemple du processeur MC68000, un vieux processeur inventé en 1979 présent dans les calculatrices TI-89 et TI-92, celui-ci contient 68000 transistors (d'où son nom : MC68000). Il s'agit d'un vieux processeur complètement obsolète et particulièrement simple. Et pourtant, celui-ci contient pas mal de broches : 37 au total ! Pour comparer, sachez que les processeurs actuels utilisent entre 700 et 1300 broches d'entrée et de sortie. À ce jeu là, notre pauvre petit MC68000 passe pour un gringalet !

Pour être plus précis, le nombre de broches (entrées et sorties) d'un processeur dépend du socket de la carte mère. Par exemple, un socket LGA775 est conçu pour les processeurs comportant 775 broches d'entrée et de sortie, tandis qu'un socket AM2 est conçu pour des processeurs de 640 broches. Certains sockets peuvent carrément utiliser 2000 broches (c'est le cas du socket G34 utilisé pour certains processeurs AMD Opteron). Pour la mémoire, le nombre de broches dépend du format utilisé pour la barrette de mémoire (il existe trois formats différents), ainsi que du type de mémoire. Certaines mémoires obsolètes (les mémoires FPM-RAM et EDO-RAM) se contentaient de 30 broches, tandis que la mémoire DDR2 utilise entre 204 et 244 broches.

Les sorties et leurs types modifier

Les sorties des circuits intégrés peuvent se classer en plusieurs types, selon leur fonctionnement. Pour les sorties basées sur des transistors, on distingue principalement les sorties totem-pole, les sorties à drain ouvert et les sorties trois-état.

Les sorties totem-pole sont les plus communes pour les circuits CMOS. Ce sont des sorties qui sont connectées à deux transistors : un qui relie la sortie à la masse, et un autre qui la relie à la tension d'alimentation. En technologie CMOS, elles sont équivalentes à des sorties connectées à une porte logique. Elles sont toujours connectées soit à la masse, soit à la tension d'alimentation.

Sortie à collecteur ouvert, équivalent en technologie TTL d'une sortie à drain ouvert.

Les sorties à drain/collecteur ouvert sont soit connectées à la masse, soit connectées à rien. La sortie peut être mise à 0 par le circuit intégré, mais elle ne peut pas être mise à 1 sans intervention extérieure. Pour utiliser une sortie à drain ouvert, il faut relier la sortie à la tension d'alimentation à travers une résistance, appelée résistance de rappel.

Il existe aussi une variante, où la sortie peut être mise à 1 par le circuit intégré, ou être déconnectée, mais ne peut pas être mise à 0 sans intervention extérieure. Ici on connecte la sortie à la masse, et non à la tension d'alimentation.

Les sorties trois-état peuvent prendre trois états, comme leur nom l'indique. Soit elles sont connectées à la masse, soit elles sont reliées à la tension d'alimentation, soit elles ne sont connectées ni à l'une ni à l'autre. Si les deux premiers cas correspondent à un 0 et à un 1, l'état déconnecté ne correspond à aucun des deux. Il s'agit d'un état utilisé quand on souhaite déconnecter ou connecter à la demande certains composants dans un circuit. Vous comprendrez en quoi ces sorties sont utiles quand nous parlerons des mémoires et des bus de communication.

Les sorties à drain ouvert et les sorties trois-états sont très utilisés quand il s'agit de connecter plusieurs circuits intégrés entre eux, et nous en reparlerons longuement dans le chapitre sur les bus électroniques. Nous verrons que de nombreux bus exigent que les circuits branchés dessus aient des entrées-sorties trois-états, ou en drain/collecteur ouvert.

Transformer une sortie totem-pole en sortie trois états modifier

Il est possible de fabriquer une sortie trois-états à partir d'une sortie totem-pole normale. Pour cela, il faut placer une porte logique modifiée juste avant la sortie totem-pole. Cette porte logique est une porte OUi améliorée appelée tampon trois-état. Elle possède une entrée de donnée, une entrée de commande, et une sortie : suivant ce qui est mis sur l'entrée de commande, la sortie est soit en état de haute impédance (déconnectée du bus), soit dans l'état normal (0 ou 1).

Commande Entrée Sortie
0 0 Haute impédance/Déconnexion
0 1 Haute impédance/Déconnexion
1 0 0
1 1 1

Pour simplifier, on peut voir ceux-ci comme des interrupteurs :

  • si on envoie un 0 sur l'entrée de commande, ces circuits trois états se comportent comme un interrupteur ouvert ;
  • si on envoie un 1 sur l'entrée de commande, ces circuits trois états se comportent comme une porte OUI.
Tampon trois-états.

Un tampon trois-état est parfois implémenté avec le circuit ci-dessous. Son fonctionnement est simple à expliquer. Si le bit de commande vaut 0, la sortie des deux portes vaut 0 et les deux transistors sont ouverts. Si le bit de commande vaut 1, les deux sorties des portes ET sont l'inverse l'une de l'autre. Si le bit d'entrée est à 1, le transistor du haut se ferme et met un 1 en sortie, alors que le transistor du bas s'ouvre. Si le bit d'entrée est à 0, c'est l'inverse, la sortie est reliée à la masse et sort un 0. Si le bit de commande est à 0, la sortie des deux portes sort un 0, les deux transistors se ferment.

Circuit trois état, implémentation possible

Transformer une sortie totem-pole en sortie à collecteur ouvert modifier

Il est possible de fabriquer une sortie à collecteur ouvert à partir d'une sortie totem-pole normale. Pour cela, il faut placer un transistor en aval de la sortie normale. Les sorties à drain ouvert utilisent un transistor MOS, les sorties à collecteur ouvert utilisent un transistor bipolaire au lieu d'un transistor MOS. Le tout est illustré ci-dessous.

La sortie est mise à 0 ou 1 selon que le transistor est ouvert ou fermé. Si le transistor est ouvert, la sortie est connectée à la tension d'alimentation, ce qui fait que la sortie est à 1. Si le transistor est fermé, la tension d'alimentation est reliée à la masse, la tension d'alimentation est alors aux bornes de la résistance, et la sortie est donc au niveau de la masse : elle est à 0.

Implémentation d'une sortie à collecteur ouvert, équivalent en technologie TTL d'une sortie à drain ouvert.

Pour la variante où la sortie est soit à 1 ou déconnectée, on peut procéder de la même manière, en plaçant un transistor en aval de la sortie. Mais il est aussi possible d'utiliser un autre composant que le transistor : une diode. Une diode est un composant qui ne laisse passer le courant que dans un sens : de l'entrée vers la sortie, mais pas dans l'autre sens. La diode est dite bloquée quand elle ne laisse pas passer le courant, passante quand le courant passe. La diode est passante si on met une tension suffisante sur l'entrée, bloquée sinon. En clair, la diode recopie un 1 présenté sur l'entrée, mais déconnecte la sortie quand on présente un 0 sur l'entrée.

Le ET câblé et le OU câblé avec des sorties à drain ouvert modifier

Les sorties à drain ouvert ont une particularité assez sympathique, qui permet d'implémenter une porte ET simplement en croisant des fils. Il suffit de connecter ces sorties au même fil et de relier celui-ci à la tension d'alimentation à travers une résistance. On obtient alors un ET câblé, qui fait un ET entre plusieurs sorties d'un circuit intégré. Il est illustré ci-dessous.

La tension d'alimentation est reliée au fil à travers une résistance, ce qui permet d'imposer un 1 sur la sortie, à condition que les sorties en collecteur ouvert soient coopératives. Si toutes les sorties sont à 1, elles sont déconnectées, et la sortie est connectée à la résistance de rappel : le circuit sort un 1. Par contre, si une seule sortie sort un 0, elle connectera la tension d'alimentation à la masse et mettra la sortie à 0. C'est le comportement attendu d'une porte ET.

Et câblé.

Le OU câblé fonctionne sur le même principe, avec cependant deux grosses différences. Premièrement, les sorties en collecteur ouvert doivent soit imposer un 1 sur la sortie, soit la déconnecter. C'est le fonctionnement inverse à celui vu précédemment. Deuxièmement, la résistance est reliée à la masse, ce qui permet d'imposer un 0 sur la sortie si les sorties en collecteur ouvert soient coopératives. Si toutes les sorties sont à 0, elles sont déconnectées, et la sortie est connectée à masse à travers la résistance de rappel : le circuit sort un 0. Par contre, si une seule sortie sort un 1, elle impose le 1 sur la sortie. C'est le comportement attendu d'un OU.

OU câblé.

En théorie, beaucoup de circuits peuvent se simplifier en utilisant des OU/ET câblés. C'en est au point où de nombreux circuits que nous allons voir dans la suite de ce cours pourraient se simplifier grâce à ces montages. Mais ils sont peu utilisés en pratique, surtout sur les circuits CMOS.

La miniaturisation des circuits intégrés et la loi de Moore modifier

En 1965, le cofondateur de la société Intel, spécialisée dans la conception de mémoires et de processeurs, a affirmé que la quantité de transistors présents dans un circuit intégré doublait tous les 18 mois : c'est la première loi de Moore. En 1975, il réévalua cette affirmation : ce n'est pas tous les 18 mois que le nombre de transistors d'un circuit intégré double, mais tous les 2 ans. Elle est respectée sur la plupart des circuits intégrés, mais surtout par les processeurs et les cartes graphiques, les mémoires RAM et ROM, bref : tout ce qui est majoritairement constitué de transistors.

Nombre de transistors en fonction de l'année.

La miniaturisation des transistors modifier

L'augmentation du nombre de transistors n'aurait pas été possible sans la miniaturisation, à savoir le fait de rendre les transistors plus petits. Il faut savoir que les circuits imprimés sont fabriqués à partir d'une plaque de silicium pur, un wafer, sur laquelle on vient graver le circuit imprimé. Les transistors sont donc répartis sur une surface plane. Ils ont souvent une largeur et une longueur qui sont très proches. Pour simplifier, la taille des transistors est aussi appelée la finesse de gravure. Celle-ci s'exprime le plus souvent en nanomètres.

La loi de Moore nous donne des indications sur l'évolution de la finesse de gravure dans le temps. Doubler le nombre de transistors signifie qu'on peut mettre deux fois plus de transistors sur une même surface : la surface occupée par un transistor a été divisée par deux. Ainsi, la finesse de gravure est divisée par la racine carrée de deux, environ 1,4, tous les deux ans. Une autre formulation consiste à dire que la finesse de gravure est multipliée par 0,7 tous les deux ans, soit une diminution de 30 % tous les deux ans. En clair, la taille des transistors décroit de manière exponentielle avec le temps !

Évolution de la finesse de gravure au cours du temps pour les mémoires FLASH de type NAND.

La fin de la loi de Moore modifier

Néanmoins, la loi de Moore n'est pas vraiment une loi gravée dans le marbre. Si celle-ci a été respectée jusqu'à présent, c'est avant tout grâce aux efforts des fabricants de processeurs, qui ont tenté de la respecter pour des raisons commerciales. Vendre des processeurs toujours plus puissants, avec de plus en plus de transistors est en effet gage de progression technologique autant que de nouvelles ventes.

Il arrivera un moment où les transistors ne pourront plus être miniaturisés, et ce moment approche ! Quand on songe qu'en 2016 certains transistors ont une taille proche d'une vingtaine ou d'une trentaine d'atomes, on se doute que la loi de Moore n'en a plus pour très longtemps. Et la progression de la miniaturisation commence déjà à montrer des signes de faiblesses. Le 23 mars 2016, Intel a annoncé que pour ses prochains processeurs, le doublement du nombre de transistors n'aurait plus lieu tous les deux ans, mais tous les deux ans et demi. Cet acte de décès de la loi de Moore n'a semble-t-il pas fait grand bruit, et les conséquences ne se sont pas encore faites sentir dans l'industrie. Au niveau technique, on peut facilement prédire que la course au nombre de cœurs a ses jours comptés.

On estime que la limite en terme de finesse de gravure sera proche des 5 à 7 nanomètres : à cette échelle, le comportement des électrons suit les lois de la physique quantique et leur mouvement devient aléatoire, perturbant fortement le fonctionnement des transistors au point de les rendre inutilisables. Et cette limite est proche : des finesses de gravure de 10 nanomètres sont déjà disponibles chez certaines fondeurs comme TSMC. Autant dire que si la loi de Moore est respectée, la limite des 5 nanomètres sera atteinte dans quelques années, à peu-près vers l'année 2020. Ainsi, nous pourrons vivre la fin d'une ère technologique, et en voir les conséquences. Les conséquences économiques sur le secteur du matériel promettent d'être assez drastiques, que ce soit en terme de concurrence ou en terme de réduction de l'innovation.

Quant cette limite sera atteinte, l'industrie sera face à une impasse. Le nombre de cœurs ou la micro-architecture des processeurs ne pourra plus profiter d'une augmentation du nombre de transistors. Et les recherches en terme d'amélioration des micro-architectures de processeurs sont au point mort depuis quelques années. La majeure partie des optimisations matérielles récemment introduites dans les processeurs sont en effet connues depuis fort longtemps (par exemple, le premier processeur superscalaire à exécution dans le désordre date des années 1960), et ne sont améliorables qu'à la marge. Quelques équipes de recherche travaillent cependant sur des architectures capables de révolutionner l'informatique. Le calcul quantique ou les réseaux de neurones matériels sont une première piste, mais qui ne donneront certainement de résultats que dans des marchés de niche. Pas de quoi rendre un processeur de PC plus rapide.

L'invention du microprocesseur modifier

Un processeur est un circuit assez complexe et qui utilise beaucoup de transistors. Avant les années 1970, il n'était pas possible de produire un processeur en un seul morceau. Impossible de mettre un processeur dans un seul boitier, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés : l'Intel 3001 est le séquenceur, l'Intel 3002 est le chemin de données (ALU et registres), le 3003 est un circuit d'anticipation de retenue censé être combiné avec l'ALU, le 3212 est une mémoire tampon, le 3214 est une unité de gestion des interruptions, les 3216/3226 sont des interfaces de bus mémoire. On pourrait aussi citer la famille de circuits intégrés AMD Am2900.

L'intel 4004 : le premier microprocesseur modifier

Par la suite, les progrès de la miniaturisation ont permis de mettre un processeur entier dans un seul circuit intégré. C'est ainsi que sont nés les microprocesseurs, à savoir des processeurs qui tiennent tout entier sur une seule puce de silicium. Les tout premiers microprocesseurs étaient des processeurs à application militaire, comme le processeur du F-14 CADC ou celui de l'Air data computer.

Le tout premier microprocesseur commercialisé au grand public est le 4004 d'Intel, sorti en 1971. L'intel 4004 comprenait environ 2300 transistors, avait une fréquence de 740 MHz, pouvait faire 46 opérations différentes, et manipulait des entiers de 4 bits. De plus, le processeur manipulait des entiers en BCD, ce qui fait qu'il pouvait manipuler un chiffre BCD à la fois (un chiffre BCD est codé sur 4 bits). Il était au départ un processeur de commande, prévu pour être intégré dans la calculatrice Busicom calculator 141-P, mais il fut utilisé pour d'autres applications quelque temps plus tard. Son successeur, l'Intel 4040, garda ces caractéristiques et n'apportait que quelques améliorations mineures : plus de registres, plus d'opérations, etc.

Le 4004 était commercialisé dans un boitier DIP simple, fort différent des boitiers et sockets des processeurs actuels. Le boitier du 4004 avait seulement 16 broches, ce qui était permis par le fait qu'il s'agissait d'un processeur 4 bits. On trouve 4 broches pour échanger des données avec le reste de l'ordinateur, 5 broches pour communiquer avec la mémoire (4 broches d'adresse, une pour indiquer s'il faut faire une lecture ou écriture), le reste est composé de broches pour la tension d'alimentation VDD, la masse VSS et pour le signal d'horloge (celui qui décide de la fréquence).

Intel 4004
Broches du 4004.

Immédiatement après le 4004, les premiers microprocesseurs 8 bits furent commercialisés. Le 4004 fut suivi par le 8008 et quelques autres processeurs 8 bits extrêmement connus, comme le 8080 d'Intel, le 68000 de Motorola, le 6502 ou le Z80. Ces processeurs utilisaient là encore des boitiers similaires au 4004, mais avec plus de broches, vu qu'ils étaient passés de 4 à 8 bits. Par exemple, le 8008 utilisait 18 broches, le 8080 était une version améliorée du 8008 avec 40 broches. Le 8086 fut le premier processeur 16 bits.

Le passage des boitiers aux slots et sockets modifier

La forme des processeurs a changé au cours du temps. Ils sont devenus plats et carrés. Les raisons qui expliquent la forme des boitiers des processeurs actuels sont assez nombreuses. La première est que les techniques de fabrications des puces électroniques actuelles font qu'il est plus simple d'avoir un circuit planaire, relativement peu épais. De plus, la forme carrée s'explique par la fabrication des puces de silicium, où un cristal de silicium est coupé en tranches, elles-mêmes découpées en puces carrées identiques, ce qui facilite la conception. Un autre avantage de cette forme est que la dissipation de la chaleur est meilleure. Les processeurs actuels sont devenus plus puissants que ceux d'antan, mais au prix d'une dissipation thermique augmentée. Dissiper cette chaleur est devenu un vrai défi sur les processeurs actuels, et la forme des microprocesseurs actuels aide à cela, couplé à des radiateurs et ventilateurs.

Un autre changement tient dans la manière dont le processeur est relié à la carte mère. Les premiers processeurs 8 et 16 bits étaient soudés à la carte mère. Les retirer demandait de les dé-souder, ce qui n'était pas très pratique, mais ne posait pas vraiment de problèmes à l'époque. Il faut noter que certains processeurs assez anciens étaient placés sur des cartes intégrées, elles-mêmes connectées à la carte mère par un slot d'extension, similaire à celui des cartes graphiques.

Circuit du Pentium 2..
Slot 1-8626, utilisé pour connecter les processeurs Pentium 2 sur la carte mère.

De nos jours, les processeurs n'utilisent plus les boitiers soudés d'antan. Les processeurs sont clipsés dans un connecteur spécial sur la carte mère, appelé le socket. Grâce à ce système, il est plus simple d'ajouter ou de retirer un processeur de la carte mère. L'upgrade d'un processeur est ainsi fortement facilitée. Les broches sont composées de billes ou de pins métalliques qui font contact avec le connecteur.

XC68020 bottom p1160085
Kl Intel Pentium MMX embedded BGA Bottom


Les circuits combinatoires modifier

Dans ce chapitre, nous allons aborder les circuits combinatoires. Ces circuits font comme tous les autres circuits : ils prennent des données sur leurs entrées, et fournissent un résultat en sortie. Le truc, c'est que ce qui est fourni en sortie ne dépend que du résultat sur les entrées, et de rien d'autre (ce n'est pas le cas pour tous les circuits). Pour donner quelques exemples, on peut citer les circuits qui effectuent des additions, des multiplications, ou d'autres opérations arithmétiques du genre.

Quelle que soit la complexité du circuit combinatoire à créer, celui-ci peut être construit en reliant des portes logiques entre elles. La conception d'un circuit combinatoire demande cependant de respecter quelques contraintes. La première est qu'il n'y ait pas de boucles dans le circuit : impossible de relier la sortie d'une porte logique sur son entrée, ou de faire la même chose avec un morceau de circuit. Si une boucle est présente dans un circuit, celui-ci n'est pas un circuit combinatoire, mais appartient à la classe des circuits séquentiels, que nous verrons dans le prochain chapitre.

Dans ce qui va suivre, nous allons voir comment concevoir ce genre de circuits. Il existe des méthodes et procédures assez simples qui permettent à n'importe qui de créer n'importe quel circuit combinatoire. Nous allons voir comment créer des circuits combinatoires à plusieurs entrées, mais à une seule sortie. Pour simplifier, on peut considérer que les bits envoyés en entrée sont un nombre, et que le circuit calcule un bit à partir du nombre envoyé en entrée.

Exemple d'un circuit électronique à une seule sortie.

C'est à partir de circuits de ce genre que l'on peut créer des circuits à plusieurs sorties : il suffit d'assembler plusieurs circuits à une sortie. La méthode pour ce faire est très simple : chaque sortie est calculée indépendamment des autres, uniquement à partir des entrées. Ainsi, pour chaque sortie du circuit, on crée un circuit à plusieurs entrées et une sortie : ce circuit déduit quoi mettre sur cette sortie à partir des entrées. En assemblant ces circuits à plusieurs entrées et une sortie, on peut ainsi calculer toutes les sorties.

Comment créer un circuit à plusieurs sorties avec des sous-circuits à une sortie.

Décrire un circuit : tables de vérité et équations logiques modifier

Dans ce qui va suivre, nous aurons besoin de décrire un circuit électronique, le plus souvent un circuit que l'on souhaite concevoir ou utiliser. Et pour cela, il existe plusieurs grandes méthodes : la table de vérité, les équations logiques et un schéma du circuit. Les schémas de circuits électroniques ne sont rien de plus que les schémas avec des portes logiques, que nous avons déjà utilisé dans les chapitres précédents. Reste à voir la table de vérité et les équations logiques. La différence entre les deux est que la table de vérité décrit ce que fait un circuit, alors qu'une équation logique décrit la manière dont il est câblé. D'un côté la table de vérité considère le circuit comme une boite noire dont elle décrit le fonctionnement, de l'autre les équations décrivent ce qu'il y a à l'intérieur.

La table de vérité modifier

La table de vérité décrit ce que fait le circuit, mais ne se préoccupe pas de dire comment. Elle ne dit pas quelles sont les portes logiques utilisées pour fabriquer le circuit, ni comment celles-ci sont reliées. Il s'agit d'une description du comportement du circuit, pas du circuit lui-même. En effet, elle se borne à donner la valeur de la sortie pour chaque entrée. Pour l'obtenir, il suffit de lister la valeur de chaque sortie pour toute valeur possible en entrée : on obtient alors la table de vérité du circuit. Pour créer cette table de vérité, il faut commencer par lister toutes les valeurs possibles des entrées dans un tableau, et écrire à côté les valeurs des sorties qui correspondent à ces entrées. Cela peut être assez long : pour un circuit ayant entrées, ce tableau aura lignes. Mais c'est la méthode la plus simple, la plus facile à appliquer.

Un premier exemple modifier

Le premier exemple sera très simple. Le circuit que l'on va créer sera un inverseur commandable, qui fonctionnera soit comme une porte NON, soit se contentera de recopier le bit fournit en entrée. Pour faire le choix du mode de fonctionnement (inverseur ou non), un bit de commande dira s'il faut que le circuit inverse ou non l'autre bit d'entrée :

  • quand le bit de commande vaut zéro, l'autre bit est recopié sur la sortie ;
  • quand il vaut 1, le bit de sortie est égal à l'inverse du bit d'entrée (pas le bit de commande, l'autre).

La table de vérité obtenue est celle d'une porte XOR :

Entrées Sortie
00 0
01 1
10 1
11 0

Un second exemple modifier

Pour donner un autre exemple, on va prendre un circuit calculant le bit de parité d'un nombre. Ce bit de parité est un bit qu'on ajoute aux données à stocker afin de détecter des erreurs de transmission ou d’éventuelles corruptions de données. Le but d'un bit de parité est que le nombre de bits à 1 dans le nombre à stocker, bit de parité inclus, soit toujours un nombre pair. Ce bit de parité vaut : zéro si le nombre de bits à 1 dans le nombre à stocker (bit de parité exclu) est pair et 1 si ce nombre est impair. Détecter une erreur demande de compter le nombre de 1 dans le nombre stocké, bit de parité inclus : si ce nombre est impair, on sait qu'un nombre impair de bits a été modifié. Dans notre cas, on va créer un circuit qui calcule le bit de parité d'un nombre de 3 bits.

Entrées Sortie
000 0
001 1
010 1
011 0
100 1
101 0
110 0
111 1

Un troisième exemple modifier

Pour le dernier exemple, nous allons prendre en entrée un nombre de 3 bits. Le but du circuit à concevoir sera de déterminer le bit majoritaire dans ce nombre : celui-ci contient-il plus de 1 ou de 0 ? Par exemple :

  • le nombre 010 contient deux 0 et un seul 1 : le bit majoritaire est 0 ;
  • le nombre 011 contient deux 1 et un seul 0 : le bit majoritaire est 1 ;
  • le nombre 000 contient trois 0 et aucun 1 : le bit majoritaire est 0 ;
  • le nombre 110 contient deux 1 et un seul 0 : le bit majoritaire est 1 ;
  • etc.
Entrées Sortie
000 0
001 0
010 0
011 1
100 0
101 1
110 1
111 1

Les équations logiques modifier

Il peut être utile d'écrire un circuit non sous forme d'une table de vérité, ou d'un schéma, mais sous forme d'équations logiques. Mais attention : il ne s'agit pas des équations auxquelles vous êtes habitués. Ces équations logiques ne font que travailler avec des 1 et des 0, et n'effectuent pas d'opérations arithmétiques mais seulement des ET, des OU, et des NON. Dans le détail, les variables sont des bits (les entrées du circuit considéré), alors que les opérations sont des ET, OU et NON. Voici résumé dans ce tableau les différentes opérations, ainsi que leur notation. a et b sont des bits.

Opérateur Notation 1 Notation 2
NON a
a ET b a.b
a OU b a+b
a XOR b

Avec ce petit tableau, vous savez comment écrire des équations logiques… Enfin presque, il ne faut pas oublier le plus important : les parenthèses, pour éviter quelques ambiguïtés. C'est un peu comme avec des équations normales : donne un résultat différent de . Avec nos équations logiques, on peut trouver des situations similaires : par exemple, est différent de .

Les formes normales conjonctives et disjonctives modifier

Dans la jungle des équations logiques, deux types se démarquent des autres. Ces deux catégories portent les noms barbares de formes normales conjonctives et de formes normales disjonctives. Derrière ces termes se cache cependant deux concepts assez simples. Il s'agit d'équations qui impliquent uniquement des portes ET, OU et NON. Pour simplifier, ces équations décrivent des circuits composés de trois couches de portes logiques : une couche de portes NON, une couche de portes ET et une couche de portes OU. La couche de portes NON est placée immédiatement à la suite des entrées, dont elle inverse certains bits. Les couches de portes ET et OU sont placées après la couche de portes NON, et deux cas sont alors possibles : soit on met la couche de portes ET avant la couche de OU, soit on fait l'inverse. Le premier cas, avec les portes ET avant les portes OU, donne une forme normale disjonctive. La forme normale conjonctive est l'exact inverse, à savoir celui où la couche de portes OU est placée avant la couche de portes ET.

Les équations obtenues ont une forme similaire aux exemples qui vont suivre. Ces exemples sont donnés pour un circuit à trois entrées nommées a, b et c, et une sortie s. On voit que certaines entrées sont inversées avec une porte NON, et que le résultat de l'inversion est ensuite combiné avec des portes ET et OU.

  • Exemple de forme normale conjonctive : .
  • Exemple de forme normale disjonctive : .

Il faut savoir que tout circuit combinatoire peut se décrire avec une forme normale conjonctive et avec une forme normale disjonctive. Mais l'équation obtenue n'est pas forcément la manière idéale de concevoir un circuit. Celui-ci peut par exemple être simplifié en utilisant des portes XOR, ou en remaniant le circuit. Mais obtenir une forme normale est très souvent utile, quitte à simplifier celle-ci par la suite. D'ailleurs, les méthodes que nous allons voir plus bas ne font que cela : elles traduisent une table de vérité en forme normale conjonctive ou disjonctive, avant de la simplifier et de traduire le tout en circuit.

Conversions entre équation, circuit et table de vérité modifier

Conversion d'un schéma de circuit en équation logique.

Une équation logique se traduit en circuit assez facilement : il suffit de substituer chaque terme de l'équation avec la porte logique qui correspond. Les parenthèses et priorités opératoires indiquent l'ordre dans lequel relier les différentes portes logiques. Elles donnent une idée de comment doit être faite cette substitution. Les schémas ci-dessous montrent un exemple d'équation logique et le circuit qui correspond, tout en montrant les différentes substitutions intermédiaires. À ce propos, concevoir un circuit demande simplement d'établir son équation logique : il suffit de traduire l'équation obtenue en circuit, et le tour est joué !

La signification symboles sur les exemples est donnée dans la section « Les équations logiques » ci-dessus.

Premier exemple. Second exemple.
Troisième exemple.
Quatrième exemple.
Quatrième exemple.

Il est aussi intéressant de parler des liens entre tables de vérité et équation logique. Il faut savoir qu'il est possible de trouver l'équation d'un circuit à partir de sa table de vérité, et réciproquement. C'est d'ailleurs ce que font les méthodes de conception de circuit que nous allons voir plus bas : elles traduisent la table de vérité d'un circuit en équation logique. On commence par établir la table de vérité, ce qui est assez simple, avant d'établir une équation logique et de la traduire en circuit. On pourrait croire qu'à chaque table de vérité correspond une seule équation logique, mais ce n'est pas le cas. En réalité, il existe plusieurs équations logiques différentes pour chaque table de vérité. La raison à cela est que des équations différentes peuvent donner des circuits qui se comportent de la même manière. Après tout, on peut concevoir un circuit de différente manières et des circuits câblés différemment peuvent parfaitement faire la même chose. Des équations logiques qui décrivent la même table de vérité sont dites équivalentes. Par équivalente, on veut dire qu'elles décrivent des circuits différents, mais qui ont la même table de vérité - ils font la même chose. À ce propos, il faut savoir qu'il est possible de convertir une équation logique en une autre équation équivalente., chose que nous apprendrons à faire dans la suite de ce chapitre.

Concevoir un circuit combinatoire avec la méthode des minterms modifier

Comme dit plus haut, créer un circuit demande d'établir sa table de vérité, avant de la traduire en équation logique, puis en circuit. Nous allons maintenant voir la première étape, celle de la conversion entre table de vérité et équation. Il existe deux grandes méthodes de ce type, pour concevoir un circuit intégré, qui portent les noms de méthode des minterms et de méthode des maxterms. La différence entre les deux est que la première donne une forme normale disjonctive, alors que la seconde donne une forme normale conjonctive. Dans cette section, nous allons voir la méthode des minterms, avant de voir la méthode des maxterms. Pour chaque méthode, nous allons commencer par montrer comment appliquer ces méthodes sans rentrer dans le formalisme, avant de montrer le formalisme en question. Précisons cependant que ces deux méthodes font la même chose : elles traduisent une table de vérité en équation logique. La première étape de ces deux méthodes est donc d'établir la table de vérité. Voyons un peu de quoi il retourne.

La méthode des minterms, expliquée sans formalisme modifier

La méthode des minterms est de loin la plus simple à comprendre. Ses principes sont en effet assez intuitifs et elle est assez facile à appliquer, pour qui connaît ses principes sous-jacents. Pour l'expliquer, nous allons commencer par voir un circuit, qui compare son entrée avec une constante, dépendante du circuit. Par la suite, nous allons voir comment combiner ce circuit avec des portes logiques pour obtenir le circuit désiré.

Les minterms (comparateurs avec une constante) modifier

Nous allons maintenant étudier un comparateur qui vérifie si le nombre d'entrée est égal à une certaine constante (2, 3, 5, 8, ou tout autre nombre) qui dépend du circuit et renvoie un 1 seulement si c’est le cas. Ainsi, on peut créer un circuit qui mettra sa sortie à 1 uniquement si on envoie le nombre 5 sur ses entrées. Ou encore, créer un circuit qui met sa sortie à 1 uniquement quand l'entrée vaut 126. Et ainsi de suite : tout nombre peut servir de constante à vérifier. Le circuit possède plusieurs entrées, sur lesquelles on place les bits du nombre à comparer. Sa sortie est un simple bit, qui vaut 1 si le nombre en entrée est égal à la constante et 0 sinon. Nous allons voir qu'il y en a deux types, qui ressemblent aux deux types de comparateurs avec zéro. Le premier type est basé sur une porte NOR, à laquelle on ajoute des portes NON. Le second est basé sur une porte ET précédée de portes NON.

Le premier circuit de ce type est composé d'une couche de portes NON et d'une porte ET à plusieurs entrées. Créer un tel circuit se fait en trois étapes. En premier lieu, il faut convertir la constante à vérifier en binaire : dans ce qui suit, nous nommerons cette constante k. En second lieu, il faut créer la couche de portes NON. Pour cela, rien de plus simple : on place des portes NON pour les entrées de la constante k qui sont à 0, et on ne met rien pour les bits à 1. Par la suite, on place une porte ET à plusieurs entrées à la suite de la couche de portes NON.

Exemples de comparateurs (la constante est indiquée au-dessus du circuit).

Pour comprendre pourquoi on procède ainsi, il faut simplement regarder ce que l'on trouve en sortie de la couche de portes NON :

  • si on envoie la constante, tous les bits à 0 seront inversés alors que les autres resteront à 1 : on se retrouve avec un nombre dont tous les bits sont à 1 ;
  • si on envoie un autre nombre, soit certains 0 du nombre en entrée ne seront pas inversés, ou alors des bits à 1 le seront : il y aura au moins un bit à 0 en sortie de la couche de portes NON.

Ainsi, on sait que le nombre envoyé en entrée est égal à la constante k si et seulement si tous les bits sont à 1 en sortie de la couche de portes NON. Dit autrement, la sortie du circuit doit être à 1 si et seulement si tous les bits en sortie des portes NON sont à 1 : il ne reste plus qu'à trouver un circuit qui prenne ces bits en entrée et ne mette sa sortie à 1 que si tous les bits d'entrée sont à 1. Il existe une porte logique qui fonctionne ainsi : il s'agit de la porte ET à plusieurs entrées.

Fonctionnement d'un comparateur avec une constante.

Le second type de comparateur avec une constante est fabriqué avec une porte NOR précédée de portes NON. Il se fabrique comme le comparateur précédent, sauf que cette fois-ci, il faut mettre une porte NON pour chaque bit à 1 de l'opérande, et non chaque bit à zéro. En clair, la couche de portes NON est l'exact inverse de celle du circuit précédent. Le tout est suivi par une porte NOR.

Exemples de comparateurs de type NON-NOR.

Pour comprendre pourquoi on procède ainsi, il faut simplement regarder ce que l'on trouve en sortie de la couche de portes NON :

  • si on envoie la constante, tous les bits à 0 seront inversés alors que les autres resteront à 1 : on se retrouve avec un zéro en sortie ;
  • si on envoie un autre nombre, soit certains 1 du nombre en entrée ne seront pas inversés, ou alors des bits à 0 le seront : il y aura au moins un bit à 1 en sortie de la couche de portes NON.

La porte NOR, quant à elle est un comparateur avec zéro, comme on l'a vu plus haut. Pour résumer, la première couche de portes NON transforme l'opérande et que la porte NOR vérifie si l'opérande transformée vaut zéro. La transformation de l'opérande est telle que son résultat est nul seulement si l’opérande est égale à la constante testée.

Fonctionnement des comparateurs de type NON-NOR.

Combiner les comparateurs avec une constante modifier

On peut créer n'importe quel circuit à une seule sortie avec ces comparateurs, en les couplant avec une porte OU à plusieurs entrées. Pour comprendre pourquoi, rappelons que les entrées du circuit peuvent prendre plusieurs valeurs : pour une entrée de bits, on peut placer valeurs différentes sur l'entrée. Mais seules certaines valeurs doivent mettre la sortie à 1, les autres la laissant à 0. Les valeurs d'entrée qui mettent la sortie 1 sont aussi appelées des minterms. Ainsi, pour savoir s’il faut mettre un 1 en sortie, il suffit de vérifier que l'entrée est égale à un minterm. Pour savoir si l'entrée est égale à un minterm, on doit utiliser un comparateur avec une constante pour chaque minterm. Par exemple, pour un circuit dont la sortie est à 1 si son entrée vaut 0000, 0010, 0111 ou 1111, il suffit d'utiliser :

  • un comparateur qui vérifie si l'entrée vaut 0000 ;
  • un comparateur qui vérifie si l'entrée vaut 0010 ;
  • un comparateur qui vérifie si l'entrée vaut 0111 ;
  • et un comparateur qui vérifie si l'entrée vaut 1111.

Reste à combiner les sorties de ces comparateurs pour obtenir une seule sortie, ce qui est fait en utilisant un circuit relativement simple. On peut remarquer que la sortie du circuit est à 1 si un seul comparateur a sa sortie à 1. Or, on connaît un circuit qui fonctionne comme cela : la porte OU à plusieurs entrées. En clair, on peut créer tout circuit avec seulement des comparateurs et une porte OU à plusieurs entrées.

Conception d'un circuit à partir de minterms

Méthode des minterms, version formalisée modifier

On peut formaliser la méthode précédente, ce qui donne la méthode des minterms. Celle-ci permet d'obtenir un circuit à partir d'une description basique du circuit. Mais le circuit n'est pas vraiment optimisé et peut être fortement simplifié. Nous verrons plus tard comment simplifier des circuits obtenus avec la méthode que nous allons exposer.

Lister les entrées de la table de vérité qui valident l'entrée modifier

La première étape demande d'établir la table de vérité du circuit, afin de déterminer ce que fait le circuit voulu. Maintenant que l'on a la table de vérité, il faut lister les valeurs en entrée pour lesquelles la sortie vaut 1. On rappelle que ces valeurs sont appelées des minterms. Il faudra utiliser un comparateur avec une constante pour chaque minterm afin d'obtenir le circuit final. Pour l'exemple, nous allons reprendre le circuit de calcul d'inverseur commandable, vu plus haut.

Entrées Sortie
00 0
01 1
10 1
11 0

Listons les lignes de la table où la sortie vaut 1.

Entrées Sortie
01 1
10 1

Pour ce circuit, la sortie vaut 1 si et seulement si l'entrée du circuit vaut 01 ou 10. Dans ce cas, on doit créer deux comparateurs qui vérifient si leur entrée vaut respectivement 01 et 10. Une fois ces deux comparateurs crée, il faut ajouter la porte OU.

Établir l'équation du circuit modifier

Les deux étapes précédentes sont les seules réellement nécessaires : quelqu'un qui sait créer un comparateur avec une constante (ce qu'on a vu plus haut), devrait pouvoir s'en sortir. Reste à savoir comment transformer une table de vérité en équations logiques, et enfin en circuit. Pour cela, il n'y a pas trente-six solutions : on va écrire une équation logique qui permettra de calculer la valeur (0 ou 1) d'une sortie en fonction de toutes les entrées du circuit. Et on fera cela pour toutes les sorties du circuit que l'on veut concevoir. Pour ce faire, on peut utiliser ce qu'on appelle la méthode des minterms, qui est strictement équivalente à la méthode vue au-dessus. Elle permet de créer un circuit en quelques étapes simples :

  • lister les lignes de la table de vérité pour lesquelles la sortie vaut 1 (comme avant) ;
  • écrire l'équation logique pour chacune de ces lignes (qui est celle d'un comparateur) ;
  • faire un OU entre toutes ces équations logiques, en n'oubliant pas de les entourer par des parenthèses.

Pour écrire l'équation logique d'une ligne, il faut simplement :

  • lister toutes les entrées de la ligne ;
  • faire un NON sur chaque entrée à 0 ;
  • et faire un ET avec le tout.

Vous remarquerez que la succession d'étapes précédente permet de créer un comparateur qui vérifie que l'entrée est égale à la valeur sur la ligne sélectionnée.

Pour illustrer le tout, on va reprendre notre exemple avec le bit de parité. La première étape consiste donc à lister les lignes de la table de vérité dont la sortie est à 1.

Entrées Sortie
001 1
010 1
100 1
111 1

On a alors :

  • la première ligne où l'entrée vaut 001 : son équation logique vaut  ;
  • la seconde ligne où l'entrée vaut 010 : son équation logique vaut  ;
  • la troisième ligne où l'entrée vaut 100 : son équation logique vaut  ;
  • la quatrième ligne où l'entrée vaut 111 : son équation logique vaut .

On a alors obtenu nos équations logiques. Reste à faire un OU entre toutes ces équations, et le tour est joué !

Nous allons maintenant montrer un deuxième exemple, avec le circuit de calcul du bit majoritaire vu juste au-dessus. Première étape, lister les lignes de la table de vérité dont la sortie vaut 1 :

Entrées Sortie
011 1
101 1
110 1
111 1

Seconde étape, écrire les équations de chaque ligne. Essayez par vous-même, avant de voir la solution ci-dessous.

  • Pour la première ligne, l'équation obtenue est : .
  • Pour la seconde ligne, l'équation obtenue est : .
  • Pour la troisième ligne, l'équation obtenue est : .
  • Pour la quatrième ligne, l'équation obtenue est : .

Il suffit ensuite de faire un OU entre les équations obtenues au-dessus.

Traduire l'équation en circuit modifier

Enfin, il est temps de traduire l'équation obtenue en circuit, en remplaçant chaque terme de l'équation par le circuit équivalent. Notons que les parenthèses donnent une idée de comment doit être faite cette substitution.

Concevoir un circuit avec la méthode des maxterms modifier

La méthode des minterms, vue précédemment, n'est pas la seule qui permet de traduire une table de vérité en équation logique. Elle est secondée par une méthode assez similaire : la méthode des maxterms. Les deux donnent des équations logiques, et donc des circuits, différents. Les deux commencent par une couche de portes NON, suivie par deux couches de portes ET et OU, mais l'ordre des portes ET et OU est inversé. Dit autrement, la méthode des minterms donne une forme normale disjonctive, alors que celle des maxterms donnera une forme normale conjonctive.

La méthode des maxterms : formalisme modifier

La méthode des maxterms fonctionne sur un principe assez tordu, mais qui fonctionne cependant. Avec celle-ci, on effectue trois étapes, chacune correspondant à l'exact inverse de l'étape équivalente avec les minterms. Les 0 sont remplacés par des 1 et les portes ET par des portes OU.

  • Premièrement on doit lister les lignes de la table de vérité qui mettent la sortie à 0, ce qui est l'exact inverse de l'étape équivalente avec les minterms.
  • Ensuite, on traduit chaque ligne en équation logique. La traduction de chaque ligne en équation logique est aussi inversée par rapport à la méthode des minterms : on doit inverser les bits à 1 avec une porte NON et faire un OU entre chaque bit.
  • Et enfin, on doit faire un ET entre tous les résultats précédents.

Un exemple d'application modifier

Par exemple, prenons la table de vérité suivante :

Entrée a Entrée b Entrée c Sortie S
0 0 0 0
0 0 1 1
0 1 0 0
0 1 1 1
1 0 0 1
1 0 1 1
1 1 0 1
1 1 1 0

La première étape est de lister les entrées associées à une sortie à 0. Ce qui donne :

Entrée a Entrée b Entrée c Sortie S
0 0 0 0
0 1 0 0
1 1 1 0

Vient ensuite la traduction de chaque ligne en équation logique. Cette fois-ci, les bits à 1 dans l'entrée sont ceux qui doivent être inversés, les autres restants tels quels. De plus, on doit faire un OU entre ces bits. On a donc :

  • pour la première ligne ;
  • pour la seconde ligne ;
  • pour la troisième ligne.

Et enfin, il faut faire un ET entre ces maxterms. Ce qui donne l'équation suivante :

Quelle méthode choisir ? modifier

On peut se demander quelle méthode choisir entre minterms et maxterms. L'exemple précédent nous donne un indice. Si on applique la méthode des minterms sur l'exemple précédent, vous allez prendre du temps et obtenir une équation logique bien plus compliquée, avec beaucoup de minterms. Alors que c'est plus rapide avec les maxterms, l'équation obtenue étant beaucoup plus simple. Cela vient du fait que dans l'exemple précédent, il y a beaucoup de lignes associées à une sortie à 1. On a donc plus de minterms que de maxterms, ce qui rend la méthode des minterms plus longue. Par contre, on pourrait trouver des exemples où c'est l'inverse. Si un circuit a plus de lignes où la sortie est à 0, alors la méthode des minterms sera plus rapide. Bref, tout dépend du nombre de minterms/maxterms dans la table de vérité. En comptant le nombre de cas où la sortie est à 1 ou à 0, on peut savoir quelle méthode est la plus rapide : minterm si on a plus de cas avec une sortie à 0, maxterm sinon.

Le principe caché derrière la méthode des maxterms modifier

La méthode des maxterms fonctionne sur un principe un peu différent de la méthode des minterms. Rappelons que chaque valeur d'entrée qui met une sortie à 0 est appelée un maxterm, alors que celles qui la mettent à 1 sont des minterms. Un circuit conçu selon avec des minterms vérifie si l'entrée met la sortie à 1, alors qu'un circuit maxterm vérifie si l'entrée ne met pas la sortie à 0. Dit autrement, ils vérifient soit que l'entrée est un minterm, soit que l'entrée n'est pas un maxterm.

L’aperçu complet de la méthode modifier

Pour cela, un circuit conçu avec la méthode des maxterms procède en deux étapes : il compare l'entrée avec chaque maxterm possible, et combine les résultats avec une porte à plusieurs entrées.

  • Pour commencer, le circuit vérifie si l'entrée est un maxterm avec plusieurs comparateurs avec une constante modifiée : un pour chaque maxterm. Chaque comparateur dit si l'entrée est différente du maxterm associé : il renvoie un 1 si l'entrée ne correspond pas au maxterm et 0 sinon.
  • La seconde étape combine les résultats de tous les maxterms pour déduire la sortie. Si tous les comparateurs renvoient un 1, cela signifie que l'entrée est différente de tous les maxterms : ce n'en est pas un. La sortie doit alors être mise à 1. Si l'entrée correspond à un maxterm, alors le comparateur associé au maxterm donnera un 0 en sortie : il y aura au moins un comparateur qui donnera un 0. Dans ce cas, la sortie doit être mise à 0. On remarque rapidement que ce comportement est celui d'une porte ET à plusieurs entrées.
Conception d'un circuit à partir de maxterms.

Le circuit de comparaison modifier

Le circuit de comparaison fonctionne sur le principe suivant : il compare l'entrée avec le maxterm bit par bit, chaque bit étant comparé indépendamment des autres, en parallèle. Les résultats des comparaisons sont ensuite combinées pour donner le bit de résultat.

Le circuit de comparaison donne un 1 quand les bits sont différents et un 0 s'ils sont égaux. La comparaison bit à bit est effectuée par une simple porte logique, qui n'est autre que la porte NON en entrée du circuit (ou son absence). Pour comprendre pourquoi, regardons la table de vérité du circuit de comparaison, illustré ci-dessous. On voit que si le bit du maxterm est 0, alors la sortie est égale au bit d'entrée. Mais si le bit du maxterm est à 1, alors la sortie est l'inverse du bit d'entrée.

Bit du maxterm Bit d'entrée Bit de sortie
0 0 0
0 1 1
1 0 1
1 1 0

Maintenant, passons à la combinaison des résultats. Si au moins un bit d'entrée est différent du bit du maxterm, alors l'entrée ne correspond pas au maxterm. On devine donc qu'on doit combiner les résultats avec une porte OU.

Simplifier un circuit modifier

Comme on l'a vu, la méthode précédente donne une équation logique qui décrit un circuit. Mais quelle équation : on se retrouve avec un gros paquet de ET et de OU ! heureusement, il est possible de simplifier cette équation. Pour donner un exemple, sachez que cette équation :  ; peut se simplifier en : avec les règles de simplifications que nous allons voir. Dans cet exemple, on passe donc de 17 portes logiques à seulement 3 ! Bien sûr, on peut simplifier cette équation juste pour se simplifier la vie lors de la traduction de cette équation en circuit, mais cela sert aussi à autre chose : cela permet d'obtenir un circuit plus rapide et/ou utilisant moins de portes logiques. Autant vous dire qu'apprendre à simplifier ces équations est quelque chose de crucial, particulièrement si vous voulez concevoir des circuits un tant soit peu rapides.

L'algèbre de Boole modifier

Pour simplifier une équation logique, on peut utiliser certaines propriétés mathématiques simples pour factoriser ou développer comme on le ferait avec une équation mathématique normale. Ces propriétés forment ce qu'on appelle l’algèbre de Boole. En utilisant ces règles algébriques, on peut factoriser ou développer certaines expressions, comme on le ferait avec une équation normale, ce qui permet de simplifier une équation logique assez intuitivement. Le tout est de bien faire ces simplifications en appliquant correctement ces règles, ce qui peut demander un peu de réflexion.

Les théorèmes de base de l’algèbre de Boole peuvent se classer en plusieurs types séparés, qui sont les suivantes :

  • l'associativité, la commutativité et la distributivité ;
  • la double négation et les lois de de Morgan ;
  • les autres règles, appelées règles bit à bit.

L'associativité, la distributivité et la commutativité des opérateurs logiques modifier

L'associativité, la commutativité et la distributivité ressemblent beaucoup aux règles arithmétiques usuelles, ce qui fait qu'on ne les détaillera pas ici.

Associativité, commutativité et distributivité
Commutativité



Associativité



Distributivité


Les autres règles sont par contre plus importantes.

Les règles de type bit à bit modifier

Les règles bit à bit ne sont utiles que dans le cas où certaines entrées d'un circuit sont fixées, ou lors de la simplification de certaines équations logiques. Elles regroupent plusieurs cas distincts :

  • soit on fait un ET/OU/XOR entre un bit et lui-même ;
  • soit on fait un ET/OU/XOR entre un bit et son inverse ;
  • soit on fait un ET/OU/XOR entre un bit et 1 ;
  • soit on fait un ET/OU/XOR entre un bit et 0.

Le premier cas regroupe les trois formules suivantes :

Le second cas regroupe les trois formules suivantes :

,
.

Le troisième cas regroupe les trois formules suivantes :

,
.

Le dernier cas regroupe les trois formules suivantes :

,
.

Voici la liste de ces règles, classées par leur nom mathématique :

Règles bit à bit
Idempotence


Élément nul


Élément Neutre



Complémentarité





Ces relations permettent de simplifier des équations logiques, mais peuvent avoir des utilisations totalement différentes.

Nous avons déjà utilisé implicitement les formules du premier cas, à savoir et dans le chapitre sur les portes logiques. En effet, nous avions vu qu'il est possible de fabriquer une porte NON à partir d'une porte NAND ou d'une porte NOR. L'idée était d'envoyer le bit à inverser sur les deux entrées d'une NOR/NAND, le ET/OU recopiant le bit sur sa sortie et le NON l'inversant. Pour retrouver ce résultat, il suffit d'ajouter une porte NON dans les formules et , ce qui donne :

Au passage, la formule nous dit pourquoi cela ne marcherait pas du tout avec une porte XOR.

Pour la formule , elle sert dans certaines situations particulières, où l'on veut initialiser un nombre à zéro, peu importe que ce nombre soit une variable, la sortie d'un circuit combinatoire ou un registre. La formule nous dit que le résultat d'un XOR entre un bit et lui-même est toujours zéro. Et cela s'applique aussi à des nombres : si on XOR un nombre avec lui-même, chacun de ses bits est XORé avec lui-même et est donc mis à zéro. Conséquence : un nombre XOR lui-même donnera toujours zéro. Cette propriété est utilisée pour mettre à zéro un registre, pour le calcul des bits de parité ou pour échanger une valeur entre deux registres. Les formules du second cas, à savoir , et , permettent de faire quelque chose de similaire. La première formule dit que faire un ET entre un nombre et son inverse donnera toujours zéro, comme un XOR entre un nombre et lui-même. Pour les deux autres formules, elles disent que le résultat sera toujours un bit à 1. Cela sert cette fois-ci si on veut initialiser une variable, un registre ou une sortie de circuit à une valeur où tous les bits sont à 1.

Les formules du troisième et quatrième cas seront utilisées dans le chapitre sur les circuits de calcul logique et bit à bit, dans la section sur les masques. C'est dans cette section que nous verrons en quoi ces formules sont utiles en dehors du cas d'une simplification de circuit. Pour le moment, nous ne pouvons pas en dire plus.

Les lois de de Morgan et la double négation modifier

Parmi les règles de l’algèbre de Boole, les lois de de Morgan et la double négation sont de loin les plus importantes à retenir. Elles sont les seules à impliquer les négations. Voici ces deux règles :

Règles sur les négations
Double négation
Loi de De Morgan


La première loi de de Morgan nous dit simplement qu'une porte NAND peut se fabriquer avec une porte OU précédée de deux portes NON, comme nous l'avions vu au chapitre précédent.

Porte NAND fabriquée avec des portes NON et OU

La seconde loi, quant à elle, dit qu'une porte NOR peut se fabriquer avec une porte ET précédée de deux portes NON, comme nous l'avions vu au chapitre précédent.

Porte NOR fabriquée avec des portes NON et ET

Les règles de Morgan pour deux entrées sont résumées dans le tableau ci-dessous.

Illustration des lois de De Morgan
1. Theorem.svg
2. Theorem.svg

Les lois de de Morgan peut se généraliser pour plus de deux entrées.

1ère loi de de Morgan :
2nd loi de de Morgan :

En les combinant avec la loi de la double négation, les lois de de Morgan permettent de transformer une équation écrite sous forme normale conjonctive en une équation équivalente sous forme normale disjonctive, et réciproquement. Elles permettent de passer d'une équation obtenue avec les minterms à l'équation obtenue avec les maxterms.

La formule équivalente de la porte XOR et de la porte NXOR modifier

Avec les règles précédentes, il est possible de démontrer que les portes XOR et NXOR peuvent se construire avec uniquement des portes ET/OU/NON. Nous l'avions vu dans le chapitre précédent, et montré quelques exemples de circuits équivalents.

En utilisant la méthode des minterms, on arrive à l'expression suivante pour la porte XOR et la porte NXOR :

XOR :
NXOR :

La formule obtenue avec les minterms pour la porte XOR donne ce circuit :

Porte XOR fabriquée à partir de portes ET/OU/NON.

La formule obtenue avec les minterms pour la porte NXOR donne ce circuit :

Porte NXOR fabriquée à partir de portes ET/OU/NON, alternative.

Il est possible d'obtenir une formule équivalente pour la porte XOR, en utilisant l’algèbre de Boole sur les formules précédentes. Pour cela, partons de l'équation de la porte NXOR obtenue avec la méthode des minterms :

Appliquons une porte NON pour obtenir la porte XOR :

Appliquons la loi de de Morgan entre les parenthèses :

Appliquons la loi de de Morgan dans les parenthèses :

Simplifions les doubles négations :

Porte XOR obtenue avec la méthode des maxterms.

Il est possible de faire la même chose, mais pour la porte NXOR. Pour cela, partons de l'équation de la porte XOR obtenue avec la méthode des minterms :

Une porte NXOR est, par définition, l'inverse d'une porte XOR. En appliquant un NON sur l'équation précédente, on trouve donc :

On applique la loi de de Morgan pour le OU entre les parenthèses :

On applique la loi de de Morgan, mais cette fois-ci à l'intérieur des parenthèses :

On simplifie les doubles inversions :

La formule est similaire à celle d'un XOR, la seule différence étant qu'il faut inverser de place les ET et les OU : le ET est dans les parenthèses pour le XOR et entre pour le NXOR, et inversement pour le OU.

Dans les deux exemples précédents, on voit que l'on a pu passer d'une forme normale conjonctive à une forme normale disjonctive et réciproquement, en utilisant la loi de de Morgan. C'est un principe assez général qui se retrouve souvent dans les démonstrations d'équations logiques.

Exemples complets modifier

Comme premier exemple, nous allons travailler sur cette équation : . On peut la simplifier en trois étapes :

  • Appliquer la règle de distributivité du ET sur le OU pour factoriser le facteur e1.e0, ce qui donne  ;
  • Appliquer la règle de complémentarité sur le terme entre parenthèses , ce qui donne 1.e1.e0 ;
  • Et enfin, utiliser la règle de l’élément neutre du ET, qui nous dit que a.1=a, ce qui donne : e1.e0.

En guise de second exemple, nous allons simplifier . Cela se fait en suivant les étapes suivantes :

  • Factoriser e0, ce qui donne : ;
  • Utiliser la règle du XOR qui dit que , ce qui donne .

Les tableaux de Karnaugh modifier

Il existe d'autres méthodes pour simplifier nos circuits. Les plus connues étant les tableaux de Karnaugh et l'algorithme de Quine Mc Cluskey. On ne parlera pas de la dernière méthode, trop complexe pour ce cours. Ces deux méthodes possèdent quelques défauts qui nous empêchent de créer de très gros circuits avec. Pour le dire franchement, elles sont trop longues à utiliser quand le nombre d'entrée du circuit dépasse 5 ou 6. Nous allons cependant aborder la méthode du tableau de Karnaugh, qui peut être assez utile pour des circuits simples. La simplification des équations avec un tableau de Karnaugh demande plusieurs étapes, que nous allons maintenant décrire.

Première étape : créer le tableau de Karnaugh modifier

Tableau de Karnaugh à quatre variables.

D'abord, il faut créer une table de vérité pour chaque bit de sortie du circuit à simplifier, qu'on utilise pour construire ce tableau. La première étape consiste à obtenir un tableau plus ou moins carré à partir d'une table de vérité, organisé en lignes et colonnes. Si on a n variables, on crée deux paquets avec le même nombre de variables (à une variable près pour un nombre impair de variables). Par exemple, supposons que j'aie quatre variables : a, b, c et d. Je peux créer deux paquets en regroupant les quatre variables comme ceci : ab et cd. Ou encore comme ceci : ac et bd. Il arrive que le nombre de variables soit impair : dans ce cas, il y a aura un paquet qui aura une variable de plus.

Seconde étape : remplir ce tableau modifier

Ensuite, pour le premier paquet, on place les valeurs que peut prendre ce paquet sur la première ligne. Pour faire simple, considérez ce paquet de variables comme un nombre, et écrivez toutes les valeurs que peut prendre ce paquet en binaire. Rien de bien compliqué, mais ces variables doivent être encodées en code Gray : on ne doit changer qu'un seul bit en passant d'une ligne à sa voisine. Pour le second paquet, faites pareil, mais avec les colonnes. Là encore, les valeurs doivent être codées en code Gray.

Pour chaque ligne et chaque colonne, on prend les deux paquets : ces deux paquets sont avant tout des rassemblements de variables, dans lesquels chacune a une valeur bien précise. Ces deux paquets précisent ainsi les valeurs de toutes les entrées, et correspondent donc à une ligne dans la table de vérité. Sur cette ligne, on prend le bit de la sortie, et on le place à l'intersection de la ligne et de la colonne. On fait cela pour chaque case du tableau, et on le remplit totalement.

Troisième étape : faire des regroupements modifier

Troisième étape de l'algorithme : faire des regroupements. Par regroupement, on veut dire que les 1 dans le tableau doivent être regroupés en paquets de 1, 2, 4, 8, 16, 32, etc. Le nombre de 1 dans un paquet doit TOUJOURS être une puissance de deux. De plus, ces regroupements doivent obligatoirement former des rectangles dans le tableau de Karnaugh. De manière générale, il vaut mieux faire des paquets les plus gros possible, afin de simplifier l'équation au maximum.

Exemple de regroupement valide.
Exemple de regroupement invalide.
Regroupements par les bords du tableau de Karnaugh, avec recouvrement.

Il faut noter que les regroupements peuvent se recouvrir. Non seulement c'est possible, mais c'est même conseillé : cela permet d'obtenir des regroupements plus gros. De plus, ces regroupements peuvent passer au travers des bords du tableau : il suffit de les faire revenir de l'autre côté. Et c'est possible aussi bien pour les bords horizontaux (gauche et droite) que pour les bords verticaux (haut et bas). Le même principe peut s'appliquer aux coins.

Quatrième étape : convertir chaque regroupement en équation logique modifier

Trouver l'équation qui correspond à un regroupement est un processus en plusieurs étapes, que nous illustrerons dans ce qui va suivre. Ce processus demande de :

  • trouver la variable qui ne varie pas dans les lignes et colonnes attribuées au regroupement ;
  • inverser la variable si celle-ci vaut toujours zéro dans le regroupement ;
  • faire un ET entre les variables qui ne varient pas.
  • faire un OU entre les équations de chaque regroupement, et on obtient l'équation finale de la sortie.


Dans ce chapitre, nous allons voir des applications des portes ET/OU/XOR. Nous allons voir dans le détail les opérations logiques, des opérations qui effectuent un ET/OU/XOR entre deux nombres. A savoir qu'elles font un ET/OU/XOR entre les deux bits de même poids des deux opérandes.

Les opérations de masquage modifier

Leur utilité n'est pas très utile, mais elles servent souvent pour effectuer des opérations de masquage. Leur but est de modifier certains bits d'un opérande, mais de laisser certains intouchés. Les bits modifiés peuvent être forcés à 1, forcés à 0, ou inversés. Pour cela, on combine l'opérande avec un second opérande, qui est appelée le masque. Les bits à modifier sont indiqués par le masque : chaque bit du masque indique s'il faut modifier ou laisser intact le bit correspondant dans l'opérande.

Les exemples de masquage modifier

Pour donner un exemple d'utilisation, parlons des droits d'accès à un fichier. Ceux-ci sont regroupés dans une suite de bits : un des bits indique s'il est accessible en écriture, un autre pour les accès en lecture, un autre s'il est exécutable, etc. Bref, modifier les droits en écriture de ce fichier demande de modifier le bit associé à 1 ou à 0, sans toucher aux autres. Cela peut se faire facilement en utilisant une instruction bit à bit avec un masque bien choisie.

Un autre cas typique est celui où un développeur compacte plusieurs données dans un seul entier. Par exemple, prenons le cas d'une date, exprimée sous la forme jour/mois/année. Un développeur normal stockera cette date dans trois entiers : un pour le jour, un pour le mois, et un pour la date. Mais un programmeur plus pointilleux sera capable d'utiliser un seul entier pour stocker le jour, le mois et l'année. Pour cela, il raisonnera comme suit :

  • un mois comporte maximum 31 jours : on doit donc encoder tous les nombres compris entre 1 et 31, ce qui peut se faire en 5 bits ;
  • une année comporte 12 mois, ce qui tient dans 4 bits ;
  • et enfin, en supposant que l'on doive gérer les années depuis la naissance de Jésus jusqu'à l'année 2047, 11 bits peuvent suffire.

Dans ces conditions, notre développeur décidera d'utiliser un entier de 32 bits pour le stockage des dates :

  • les 5 bits de poids forts serviront à stocker le jour ;
  • les 4 bits suivants stockeront le mois ;
  • et les bits qui restent stockeront l'année.

Le développeur qui souhaite modifier le jour ou le mois d'une date devra modifier une partie des bits, tout en laissant les autres intacts. Encore une fois, cela peut se faire facilement en utilisant une instruction bit à bit avec un masque bien choisi.

Le résultat d'une opération de masquage modifier

Maintenant, regardons ce que l'on peut faire avec une opération bit à bit entre un opérande et un masque. Le résultat dépend suivant que l'opération est un ET, un OU ou un XOR. Nous allons vous demander d'accepter les résultats sans les comprendre, vous allez comprendre comment cela fonctionne dans la section suivante.

Faire un ET entre l'opérande et le masque va mettre certains bits de l’opérande à 0 et va recopier les autres. Les bits mis à 0 sont ceux où le bit du masque correspondant est à 0, tandis que les autres sont recopiés tels quels.

La même chose a lieu avec l'opération OU, sauf que cette fois-ci, les bits de l'opérande sont soit recopiés, soit mis à 1. Les bits mis à 1 sont ceux pour lesquels le bit du masque correspondant est un 1.

Dans le cas d'un XOR, les bits sont inversés. Les bits inversés sont ceux pour lesquels le bit du masque correspondant est un 1.

Masquage des n bits de poids faible

Les circuits de masquage total modifier

Les circuits qui vont suivre appliquent la même opération entre tous les bits d'un opérande et un bit d'entrée. Ils permettent de mettre à zéro tous les bits si une condition est réunie, ou de les mettre tous à 1, voire de les inverser. Cela peut paraître assez simpliste, mais c'est quelque chose de très utile. On peut les voir comme des circuits qui ont un masque implicite dont tous les bits sont à 1.

Le circuit de mise à zéro modifier

Dans cette section, nous allons voir un circuit qui prend en entrée un nombre et le met à zéro si une condition est respectée. Pour le dire autrement, le circuit va soit recopier l'entrée telle quelle sur sa sortie, soit la mettre à zéro. Le choix entre les deux situations est réalisé par une entrée Reset de 1 bit : un 0 sur cette entrée met la sortie à zéro, un 1 signifie que l'entrée est recopiée en sortie. La porte ET est tout indiquée pour cela. La mise à zéro d'un bit d'entrée demande de faire un ET de celui-ci avec un 0, alors que recopier un bit d'entrée demande de faire un ET de celui-ci avec un 1. Il suffit d'envoyer le bit d'entrée sur les portes ET, comme illustré ci-dessous.

Circuit de mise à zéro

Il est possible de modifier ce circuit de manière à ce que le signal Reset fasse ce qu'il faut non pas quand il est à 0, mais quand il est à 1. Pour cela, une simple porte NON suffit.

Le circuit de mise à la valeur maximale modifier

Dans cette section, nous allons voir un circuit qui prend en entrée un nombre et met sa sortie à la valeur maximale si une condition est respectée. Pour le dire autrement, le circuit va soit recopier l'entrée telle quelle sur sa sortie, soit la mettre à 11111...111. Le choix entre les deux situations est réalisé par une entrée Set de 1 bit : un 1 sur cette entrée met la sortie à la valeur maximale, un 0 signifie que l'entrée est recopiée en sortie. La porte OU est toute indiquée pour cela. La mise à 1 d'un bit d'entrée demande de faire un OU de celui-ci avec un 1, alors que recopier un bit d'entrée demande de faire un OU de celui-ci avec un 0. Il suffit d'envoyer le bit d'entrée sur les portes ET, comme illustré ci-dessous.

Circuit de mise à 1111111...11

Ce circuit est utilisé pour gérer les débordements d'entier dans les circuits de calculs qui utilise l'arithmétique saturée (voir le chapitre sur le codage des entiers pour plus d'explications). Les circuits de calculs sont souvent suivis par ce circuit de mise à 111111...111, pour gérer le cas où le calcul déborde, afin de mettre la sortie à la valeur maximale. Évidemment, le circuit de calcul doit non seulement faire le calcul, mais aussi détecter les débordements d'entiers, afin de fournir le bit pour l'entrée Set. Mais nous verrons cela dans le chapitre sur les circuits de calcul entier.

L'inverseur commandable modifier

Dans cette section, nous allons voir un inverseur commandable, un circuit qui, comme son nom l'indique, inverse les bits d'un nombre passé en entrée. Ce circuit inverse un nombre quand on lui demande et ne fait rien sinon. On précise au circuit s'il doit inverser ou non l'opérande d'entrée avec un bit de commande, souvent nommé Invert. Ce dernier vaut 1 si le circuit doit inverser l'opérande et 0 sinon. La porte XOR est toute indiquée pour, ce qui fait que le circuit d'inversion commandable est composé d'une couche de portes XOR, chaque porte ayant une entrée connectée au bit de commande.

Inverseur commandable par un bit.

Dans les chapitres précédents, nous avons vu comment fabriquer des circuits relativement généraux. Il est maintenant temps de voir quelques circuits relativement simples, très utilisés. Ces circuits simples sont utilisés pour construire des circuits plus complexes, comme des processeurs, des mémoires, et bien d'autres. Les prochains chapitres vont se concentrer exclusivement sur ces circuits simples, mais courants. Nous allons donner quelques exemples de circuits assez fréquents dans un ordinateur et voir comment construire ceux-ci avec des portes logiques.

Dans ce chapitre, nous allons nous concentrer sur quelques circuits, que j'ai décidé de regrouper sous le nom de circuits de sélection. Les circuits que nous allons présenter sont utilisés dans les mémoires, ainsi que dans certains circuits de calcul. Il est important de bien mémoriser ces circuits, ainsi que la procédure pour les concevoir : nous en aurons besoin dans la suite du cours. Ils sont au nombre de quatre : le décodeur, l'encodeur, le multiplexeur et le démultiplexeur.

Le décodeur modifier

Décodeur à 3 entrées et 8 sorties.

Le premier circuit que nous allons voir est le décodeur, un composant qui contient un grand nombre d'entrées et de sorties, avec des sorties qui sont numérotées. Un décodeur possède une entrée sur laquelle on envoie un nombre codé bits et sorties de 1 bit. Par exemple, un décodeur avec une entrée de 2 bits aura 4 sorties, un décodeur avec une entrée de 3 bits aura 8 sorties, un décodeur avec une entrée de 8 bits aura 256 sorties, etc. Généralement, on précise le nombre de bits d'entrée et de sortie comme suit : on parle d'un décodeur X vers Y pour X bits d'entrée et Y de sortie. Ce qui fait qu'on peut parler de décodeur 3 vers 8 pour un décodeur à 3 bits d'entrée et 8 de sortie, de décodeur 4 vers 16, etc.

Le fonctionnement d'un décodeur est très simple : il prend sur son entrée un nombre entier x codé en binaire, puis il positionne à 1 la sortie numérotée x et met à zéro toutes les autres sorties. Par exemple, si on envoie la valeur 6 sur ses entrées, il mettra la sortie numéro 6 à 1 et les autres à zéro.

Pour résumer, un décodeur est un circuit :

  • avec une entrée de bits ;
  • avec sorties de 1 bit ;
  • où les sorties sont numérotées en partant de zéro ;
  • où on ne peut sélectionner qu'une seule sortie à la fois : une seule sortie devra être placée à 1, et toutes les autres à zéro ;
  • et où deux nombres d'entrée différents devront sélectionner des sorties différentes : la sortie de notre contrôleur qui sera mise à 1 sera différente pour deux nombres différents placés sur son entrée.

La table de vérité d'un décodeur modifier

Au vu de ce qui vient d'être dit, on peut facilement écrire la table de vérité d'un décodeur. Pour l'exemple, prenons un décodeur 2 vers 4, pour simplifier la table de vérité. Voici sa table de vérité complète, c’est-à-dire qui contient toutes les sorties regroupées :

E0 E1 S0 S1 S2 S3
0 0 1 0 0 0
0 1 0 1 0 0
1 0 0 0 1 0
1 1 0 0 0 1

Vous remarquerez que la table de vérité est assez spéciale. Les seuls bits à 1 sont sur la diagonale. Et cela ne vaut pas que dans l'exemple choisit, mais cela se généralise pour tous les décodeurs. Sur chaque ligne, il n'y a qu'un seul bit à 1, ce qui traduit le fait qu'une entrée ne met qu'une seule sortie est à 1 et met les autres à 0. Si on traduit la table de vérité sous la forme d'équations logiques et de circuit, on obtient ceci :

Equations logiques et circuit d'un décodeur 2 vers 4.

Il y a des choses intéressantes à remarquer sur les équations logiques. Pour rappel, l'équation logique d'une sortie est composée, dans le cas général, soit d'un minterm unique, soit d'un OU entre plusieurs minterms. Chaque minterm est l'équation d'un circuit qui compare l'entrée à un nombre bien précis et dépendant du minterm. Si on regarde bien, l'équation de chaque sortie correspond à un minterm et à rien d'autre, il n'y a pas de OU entre plusieurs minterms. Les minterms sont de plus différents pour chaque sortie et on ne trouve pas deux sorties avec le même minterm. Enfin, chaque minterm possible est présent : X bits d'entrée nous donnent 2^X entrées différentes possibles, donc 2^X minterms possibles. Et il se trouve que tous ces minterms possibles sont représentés dans un décodeur, ils ont tous leur sortie associée. C'est une autre manière de définir un décodeur : toutes ses sorties codent un minterm, deux sorties différentes ont des minterms différents et tous les minterms possibles sur n bits sont représentés.

Ces informations vont nous être utiles pour la suite. En effet, grâce à elles, nous allons en déduire une méthode générale pour fabriquer un décodeur, peu importe son nombre de bits d'entrée et de sortie. Mais elles permettent aussi de montrer que l'on peut créer n'importe quel circuit combinatoire quelconque à partir d'un décodeur et de quelques portes logiques. Dans ce qui suit, on suppose que le circuit combinatoire en question a une entrée de n bits et une seule sortie de 1 bit. Pour rappel, ce genre de circuit se conçoit en utilisant une table de vérité qu'on traduit en équations logiques, puis en circuits. Le circuit obtenu est alors soit un simple minterm, soit un OU entre plusieurs minterms. Or, le décodeur contient tous les minterms possibles pour une entrée de n bits, avec un minterm par sortie. Il suffit donc de prendre une porte OU et de la connecter aux minterms/sorties adéquats.

Conception d'un circuit combinatoire quelconque à partir d'un décodeur.

Fabriquer un circuit combinatoire avec un décodeur gaspille pas mal de transistors. En effet, le décodeur fournit tous les minterms possibles, alors que seule une minorité est réellement utilisée pour fabriquer le circuit combinatoire. Les minterms en trop correspondent à des paquets de portes NON et ET reliées entre elles, qui ne servent à rien. De plus, les minterms ne sont pas simplifiés. On ne peut pas utiliser les techniques vues dans les chapitres précédents pour simplifier les minterms et réduire le nombre de portes logiques utilisées. Le décodeur reste tel qu'il est, avec l'ensemble des minterms non-simplifiés. Mais la simplicité de conception du circuit reste un avantage dans certaines situations. Notamment, les circuits avec plusieurs bits de sortie sont faciles à fabriquer, notamment si les sorties partagent des minterms (si un minterm est présent dans l'équation de plusieurs sorties différentes, l'usage d'un décodeur permet de facilement factoriser celui-ci).

Ceci étant dit, passons à la conception d'un décodeur avec des portes logiques.

L'intérieur d'un décodeur modifier

On vient de voir que chaque sortie d'un décodeur correspond à son propre minterm, et que tous les minterms possibles sont représentés. Rappelons que chaque minterm est associé à un circuit qui compare l'entrée à une constante X, X dépendant du minterm. En combinant ces deux informations, on devine qu'un décodeur est simplement composé de comparateurs avec une constante que de minterms/sorties. Par exemple, si je prends un décodeur 7 vers 128, cela veut dire qu'on peut envoyer en entrée un nombre codé entre 0 et 127 et que chaque nombre aura son propre minterm associé : il y aura un minterm qui vérifie si l'entrée vaut 0, un autre vérifie si elle vaut 1, un autre qui vérifie si elle vaut 2, ... , un minterm qui vérifie si l'entrée vaut 126, et enfin un minterm qui vérifie si l'entrée vaut 127.

Pour reformuler d'une manière bien plus simple, on peut voir les choses comme suit. Si l'entrée du décodeur vaut N, la sortie mise à 1 est la sortie N. Bref, déduire quand mettre à 1 la sortie N est facile : il suffit de comparer l'entrée avec N. Si l'adresse vaut N, on envoie un 1 sur la sortie, et on envoie un zéro sinon. Pour cela, j'ai donc besoin d'un comparateur pour chaque sortie, et le tour est joué. Précisons cependant que cette méthode gaspille beaucoup de circuits et qu'il y a une certaine redondance. En effet, les comparateurs ont souvent des portions de circuits qui sont identiques et ne diffèrent parfois que ce quelques portes logiques. En utilisant des comparateurs séparés, ces portions de circuits sont dupliquées, alors qu'il serait judicieux de partager.

Exemple d'un décodeur à 8 sorties.

Comme autre méthode, plus économe en circuits, on peut créer un décodeur en assemblant plusieurs décodeurs plus simples, nommés sous-décodeurs. Ces sous-décodeurs sont des décodeurs normaux, auxquels on a ajouté une entrée RAZ, qui permet de mettre à zéro toutes les sorties : si on met un 0 sur cette entrée, toutes les sorties passent à 0, alors que le décodeur fonctionne normalement sinon. Construire un décodeur demande suffisamment de sous-décodeurs pour combler toutes les sorties. Si on utilise des sous-décodeurs à n entrées, ceux-ci prendront en entrée les n bits de poids faible de l'entrée du décodeur que l'on souhaite construire (le décodeur final). Dans ces conditions, les n décodeurs auront une de leurs sorties à 1. Pour que le décodeur final se comporte comme il faut, il faut désactiver tous les sous-décodeurs, sauf un avec l'entrée RAZ. Pour commander les n bits RAZ des sous-décodeurs, il suffit d'utiliser un décodeur qui est commandé par les bits de poids fort du décodeur final.

Décodeur 3 vers 8 conçu à partir de décodeurs 2 vers 4.

Le démultiplexeur modifier

Les décodeurs ont des cousins : les multiplexeurs et les démultiplexeurs. Un démultiplexeur a plusieurs sorties et une seule entrée. Les sorties sont numérotées de 0 à la valeur maximale. Il permet de sélectionner une sortie et de recopier l'entrée dessus, les autres sorties sont mises à 0. Pour séléctionner la sortie, le démultiplexeur possède une entrée de commande, sur laquelle on envoie le numéro de la sortie de destination. Comme le nom l'indique, le démultiplexeur fait l'exact inverse du multiplexeur, que nous verrons plus bas.

Le démultiplexeur à deux sorties modifier

Le démultiplexeur le plus simple est le démultiplexeur à deux sorties. Il possède une entrée de donnée, une entrée de commande et deux sorties, toutes de 1 bit. Suivant la valeur du bit sur l'entrée de commande, il recopie le bit d'entrée, soit sur la première sortie, soit sur la seconde. Les deux sorties sont numérotées respectivement 0 et 1.

Démultiplexeur à 2 sorties.

On peut le concevoir facilement en partant de sa table de vérité.

Entrée de commande Select Entrée de donnée Input Sortie 1 Sortie 0
0 0 0 0
0 1 0 1
1 0 0 0
1 1 1 0

Le circuit obtenu est le suivant :

Démultiplexeur à deux sorties.

Les démultiplexeurs à plus de deux sorties modifier

Il est parfaitement possible de créer des démultiplexeurs en utilisant les méthodes du chapitre sur les circuits combinatoires, comme ma méthode des minterms ou les tableaux de Karnaugh. On obtient alors un démultiplexeur assez simple, composé de deux couches de portes logiques : une couche de portes NON et une couche de portes ET à plusieurs entrées.

Démultiplexeur fabriqué avec une table de vérité.

Mais cette méthode n'est pas pratique, car elle utilise beaucoup de portes logiques et que les portes logiques avec beaucoup d'entrées sont difficiles à fabriquer. Pour contourner ces problèmes, on peut ruser. Ce qui a été fait pour les multiplexeurs peut aussi s'adapter aux démultiplexeurs : il est possible de créer des démultiplexeurs en assemblant des démultiplexeurs 1 vers 2. Évidemment, le même principe s'applique à des démultiplexeurs plus complexes : il suffit de rajouter des couches.

Circuit d'un démultiplexeur à 4 sorties, conçu à partir de démultiplexeurs à 2 sorties.

Un démultiplexeur peut aussi se fabriquer en utilisant un décodeur et quelques portes ET. Pour comprendre pourquoi, regardons la table de vérité d'un démultiplexeur à quatre sorties. Si vous éliminez le cas où l'entrée de donnée Input vaut 0, et que vous tenez compte uniquement des entrées de commande, vous retombez sur la table de vérité d'un décodeur. Cela correspond aux cases en rouge.

Input E0 E1 S0 S1 S2 S3
0 0 0 0 0 0 0
0 0 1 0 0 0 0
0 1 0 0 0 0 0
0 1 1 0 0 0 0
1 0 0 1 0 0 0
1 0 1 0 1 0 0
1 1 0 0 0 1 0
1 1 1 0 0 0 1

En réalité, Le fonctionnement d'un démultiplexeur peut se résumer comme suit : soit l'entrée Input est à 1 et il fonctionne comme un décodeur dont l'entrée est l'entrée de commande, soit l'entrée Input vaut 0 et sa sortie est mise à 0. On devine donc qu'il faut combiner un décodeur avec le circuit de mise à zéro vu dans le chapitre précédent. On devine rapidement que l'entrée Input commande la mise à zéro de la sortie, ce qui donne le circuit suivant :

Démultiplexeur conçu à partir d'un décodeur.

La couche de portes ET en aval du décodeur peut être simplifiée. Il est possible de remplacer les portes ET par des interrupteurs. Ce faisant, seule une entrée sera connectée à la sortie, alors que les autres seront déconnectées. Toute la difficulté est d'implémenter les interrupteurs, et il y a plusieurs possibilités pour cela :

  • La manière la plus simple d’implémenter ces interrupteurs serait d'utiliser un transistor dont la grille est commandée par le décodeur. Mais ce n'est pas possible sur les circuits CMOS actuels, où tout transistor PMOS doit être couplé à un autre transistor NMOS.
  • Pour résoudre ce problème, l'interrupteur en question est implémenté avec une porte à transmission, fabriquée avec deux transistors. C'est la même méthode que celle utilisée pour le multiplexeur 2 vers 1 : chaque entrée est connectée à la sortie à travers une porte à transmission, la commande de chaque porte à transmission étant le fait du décodeur.

Le multiplexeur modifier

Les décodeurs ont des cousins : les multiplexeurs et les démultiplexeurs. Les multiplexeurs sont des composants qui possèdent un nombre variable d'entrées, mais une seule sortie. Un multiplexeur permet de sélectionner une entrée et de recopier son contenu sur sa sortie, les entrées non-sélectionnées étant ignorées. Sélectionner l'entrée à recopier sur la sortie se fait en configurant une entrée de commande du multiplexeur. Les entrées sont numérotées de 0 à la valeur maximale. Configurer l'entrée de commande demande juste d'envoyer le numéro de l'entrée sélectionnée dessus.

Multiplexeur à 4 entrées.

Le multiplexeur à deux entrées modifier

Le multiplexeur le plus simple est le multiplexeur à deux entrées et une sortie. Il est facile de le construire avec des portes logiques, dans les implémentations les plus simples. Sachez toutefois que les multiplexeurs utilisés dans les ordinateurs récents ne sont pas forcément fabriqués avec des portes logiques, mais qu'on peut aussi les fabriquer directement avec des transistors.

Multiplexeur à deux entrées - symbole.

Pour commencer, établissons sa table de vérité. On va supposer qu'un 0 sur l'entrée de commande sélectionne l'entrée a. La table de vérité devrait être la suivante :

Entrée de commande Entrée a Entrée b Sortie
0 0 0 0
0 0 1 0
0 1 0 1
0 1 1 1
1 0 0 0
1 0 1 1
1 1 0 0
1 1 1 1

Sélectionnons les lignes qui mettent la sortie à 1 :

Entrée de commande Entrée a Entrée b Sortie
0 1 0 1
0 1 1 1
1 0 1 1
1 1 1 1

On sait maintenant quels comparateurs avec une constante utiliser. On peut, écrire l'équation logique du circuit. La première ligne donne l'équation suivante : , la seconde donne l'équation , la troisième l'équation et la quatrième l'équation . L'équation finale obtenue est donc :

L'équation précédente est assez compliquée, mais il y a moyen de la simplifier assez radicalement. Pour cela, nous allons utiliser les règles de l’algèbre de Boole. Pour commencer, nous allons factoriser et  :

Ensuite, factorisons dans le premier terme et dans le second :

Les termes et valent 1 :

On sait que , ce qui fait que l'équation simplifiée est la suivante :

Le circuit qui correspond est :

Multiplexeur à deux entrées - circuit.

Il est aussi possible de fabriquer un multiplexeur 2 vers 1 en utilisant des portes à transmission. L'idée est de relier chaque entrée à la sortie par l'intermédiaire d'une porte à transmission. Quand l'une sera ouverte, l'autre sera fermée. Le résultat n'utilise que deux portes à transmission et une porte NON, ce qui prend beaucoup moins de transistors. Voici le circuit qui en découle :

Multiplexeur fabriqué avec des portes à transmission

Les multiplexeurs à plus de deux entrées modifier

Il est possible de concevoir un multiplexeur quelconque à partir de sa table de vérité. Le résultat est alors un circuit composé d'une porte OU à plusieurs entrées, de plusieurs portes ET, et de quelques portes NON. Un exemple est illustré ci-dessous. Vous remarquerez cependant que ce circuit a un défaut : la porte OU finale a beaucoup d'entrées, ce qui pose de nombreux problèmes techniques. Il est difficile de concevoir des portes logiques avec un très grand nombre d'entrées. Aussi, les applications à haute performance demandent d'utiliser d'autres solutions.

Multiplexeur conçu à partir de sa table de vérité.

Une solution alternative est de concevoir un multiplexeur à plus de deux entrées en combinant des multiplexeurs plus simples. Par exemple, en prenant deux multiplexeurs plus simples, et en ajoutant un multiplexeur 2 vers 1 sur leurs sorties respectives. Le multiplexeur final se contente de sélectionner une sortie parmi les deux sorties des multiplexeurs précédents, qui ont déjà effectué une sorte de présélection.

Multiplexeur conçu à partir de multiplexeurs plus simples.

Il existe toutefois une manière bien plus simple pour créer des multiplexeurs : il suffit d'utiliser un décodeur, quelques portes OU, et quelques portes ET. L'idée est de :

  • sélectionner l'entrée à recopier sur la sortie ;
  • mettre les autres entrées à zéro ;
  • faire un OU entre toutes les entrées : vu que toutes les entrées non-sélectionnées sont à zéro, la sortie de la porte OU aura la même valeur que l'entrée sélectionnée.

Pour sélectionner l'entrée adéquate du multiplexeur, on utilise un décodeur : si la sortie n du décodeur est à 1, alors l'entrée numéro n du multiplexeur sera recopiée sur sa sortie. Dans ces conditions, l'entrée de commande du multiplexeur correspond à l'entrée du décodeur. Pour mettre à zéro les entrées non-sélectionnées, on adapte le circuit de mise à zéro précédent, basé sur une couche de portes ET, sauf que chaque porte ET est connectée à sa propre entrée. En fait, les entrées forment un nombre, tous les bits sauf un doivent être mis à 0. Pour cela, on applique un masque calculé par le décodeur.

Multiplexeur 2 vers 4 conçu à partir d'un décodeur.

Simplifier le circuit peut se faire sur deux points : la couche de portes ET, et la porte OU finale.

Multiplexeur conçu avec un OU câblé.

Commençons par la porte OU. Sur les vieux circuits et avec les vielles technologies de fabrication, il était intéressant de remplacer la porte OU finale par une porte OU câblée. Pour rappel, un OU câblé est un circuit équivalent à une porte OU, que nous avions vu dans le chapitre sur les circuits intégrés. Il est fabriqué en connectant toutes les sorties à ORer sur un seul fil, d'où le nom OU câblé.

Un OU câblé peut se faire de plusieurs manières, mais la plus commune demande que les sorties des portes logiques ET soient de type collecteur ouvert, à savoir qu'elles fournissent seulement un 1, et déconnectent leur sortie quand elles doivent sortir un 0 (ou inversement). De plus, il faut relier le fil soit à la masse (à la tension d'alimentation) à travers une résistance. Le circuit illustré ci-dessous utilise une méthode similaire. Le OU câblé est en réalité un circuit équivalent à une porte NAND réalisée avec un ET câblé. Le ET câblé est plus simple à fabriquer, mais le circuit utilise une porte logique en plus.

Passons maintenant à la couche de portes ET en aval du décodeur. Il est possible de remplacer les portes ET par des interrupteurs, comme on l'a fait pour le démultiplexeur. Notons qu'on peut combiner des interrupteurs avec un OU câblé. Dans ce cas, le circuit se simplifie grandement et les résistances disparaissent. En effet, vu qu'on travaille avec des interrupteurs, la tension d'entrée est recopiée sur la sortie, pas besoin de connecter le fil à la tension d'alimentation ou la masse. Le résultat est le circuit suivant :

Multiplexeur basé sur des interrupteurs.

Une autre possibilité est d'utiliser directement des tampons trois-états. Pour rappel, les tampons trois-états sont une amélioration de la porte OUI. Suivant ce qu'on met sur leur entrée de commande, soit ils se comportent comme une porte OUI normale, soit ils déconnectent l'entrée de la sortie. Ils fonctionnent donc comme des interrupteurs, en un peu améliorés.

Multiplexeur conçu à partir de tampons trois-états.

L'encodeur modifier

Encodeur à 8 entrées (et 3 sorties).

Il existe un circuit qui fait exactement l'inverse du décodeur : c'est l'encodeur. Là où les décodeurs ont une entrée de bits et sorties de 1 bit, l'encodeur a à l'inverse entrées de 1 bit avec une sortie de bits. Par exemple, un encodeur avec une entrée de 4 bits aura 2 sorties, un décodeur avec une entrée de 8 bits aura 3 sorties, un décodeur avec une entrée de 256 bits aura 8 sorties, etc. Comme pour les décodeurs, on parle d'un encodeur X vers Y pour X bits d'entrée et Y de sortie. Ce qui fait qu'on peut parler de décodeur 8 vers 3 pour un décodeur à 8 bits d'entrée et 3 de sortie, de décodeur 16 vers 4, etc.

Entrées et sorties d'un encodeur.

De plus, contrairement au décodeur, ce sont les entrées qui sont numérotées de 0 à N et non les sorties. Dans ce qui suit, on va supposer qu'une seule des entrées est à 1. Il existe des encodeurs capables de traiter le cas où plusieurs bits d'entrée sont à 1, qui sont appelés des encodeurs à priorité, mais nous les laissons pour le chapitre suivant. Le chapitre suivant sera totalement dédié aux encodeurs à priorité, aussi nous préférons nous focaliser sur le cas d'un encodeur simple, capable de traiter uniquement le cas om une seule entrée est à 1. En sortie, l'encodeur donne le numéro de l'entrée qui est à 1. Par exemple, si l'entrée numéro 5 est à 1 et les autres à 0, alors l'encodeur envoie un 5 sur sa sortie.

L'encodeur 4 vers 2 modifier

Prenons l'exemple d'un encodeur à 4 entrées et 2 sorties. Écrivons sa table de vérité. D'après la description du circuit, on devrait trouver ceci :

Table de vérité d'un encodeur 4 vers 2
E3 E2 E1 E0 S1 S0
0 0 0 1 0 0
0 0 1 0 0 1
0 1 0 0 1 0
1 0 0 0 1 1

Vous voyez que la table de vérité est incomplète. En effet, l'encodeur fonctionne tant qu'une seule de ses entrées est à 1. L'encodeur dit alors quelle est la sortie à 1, mais cela suppose que les autres soient à 0. Si plusieurs entrées sont à 1, le comportement de l'encodeur est potentiellement erroné. En effet, il donnera un résultat incorrect sur certaines entrées. Mais passons cela sous silence et ne tenons compte que de la table de vérité partielle précédente. On peut traduire cette table de vérité en circuit logique. On obtient alors les équations suivantes :

Le tout donne le circuit suivant :

Exemple d'encodeur à 4 entrées et 2 sorties.

Les encodeurs à plus de deux sorties modifier

Il est possible de créer un encodeur complexe en combinant plusieurs encodeurs simples. C'est un peu la même chose qu'avec les décodeurs, pour lesquels on peut créer un décodeur 8 vers 256 à base de deux décodeurs 7 vers 128, ou de quatre décodeurs 6 vers 64. L'idée de découper le nombre d'entrée en morceaux séparés, chaque morceau étant traité par un encodeur à priorité distinct des autres. Les résultats des différents encodeurs sont ensuite combinés pour donner le résultat final.

Pour comprendre l'idée, prenons la table de vérité d'un encodeur 8 vers 3; donnée dans le tableau ci-dessous.

Table de vérité d'un encodeur 8 vers 3
E7 E6 E5 E4 E3 E2 E1 E0 S2 S1 S0
0 0 0 0 0 0 0 1 0 0 0
0 0 0 0 0 0 1 0 0 0 1
0 0 0 0 0 1 0 0 0 1 0
0 0 0 0 1 0 0 0 0 1 1
0 0 0 1 0 0 0 0 1 0 0
0 0 1 0 0 0 0 0 1 0 1
0 1 0 0 0 0 0 0 1 1 0
1 0 0 0 0 0 0 0 1 1 1

En regardant bien, vous verrez que vous pouvez trouver la table de vérité d'un encodeur 4 vers 2 en deux exemplaires, indiquées en rouge.

Table de vérité d'un encodeur 8 vers 3
E7 E6 E5 E4 E3 E2 E1 E0 S2 S1 S0
0 0 0 0 0 0 0 1 0 0 0
0 0 0 0 0 0 1 0 0 0 1
0 0 0 0 0 1 0 0 0 1 0
0 0 0 0 1 0 0 0 0 1 1
0 0 0 1 0 0 0 0 1 0 0
0 0 1 0 0 0 0 0 1 0 1
0 1 0 0 0 0 0 0 1 1 0
1 0 0 0 0 0 0 0 1 1 1

On voit que les deux bits de poids faibles correspondent à la sortie de l'encodeur activé par l'entrée. Si le premier encodeur est activé, c'est lui qui fournit les bits de poids faibles. Inversement, si c'est le second encodeur qui a un résultat non-nul, c'est lui qui fournit les bits de poids faible. Notons que seul un des deux encodeurs a une sortie non-nulle à la fois : soit le premier a une sortie non-nulle, soit c'est le second, mais c'est impossible que ce soit les deux en même temps. Cela permet de déduire quelle opération permet de mixer les deux résultats : un simple OU logique suffit. Car, pour rappel, 0 OU X donne X, quelque que soit le X en question. Les bits de poids faible du résultat se calculent en faisant un OU entre les deux résultats des encodeurs.

Ensuite, il faut déterminer comment fixer le bit de poids fort du résultat. Il vaut 0 si le premier encodeur a une entrée non-nulle, et 1 si c'est le premier encodeur qui a une entrée non-nulle. Pour cela, il suffit de vérifier si les bits de poids forts, associés au premier encodeur, contiennent un 1. Si c'est le cas, alors on met la troisième sortie à 1.

Encodeur fabriqué à partir d'encodeurs plus petits.

Notons que cette procédure, à savoir faire un OU entre les sorties de deux encodeurs simples, puis faire un OU pour calculer le troisième bit, marche pour tout encodeur de taille quelconque. À vrai dire, le circuit obtenu plus haut d'un encodeur 4 vers 2 est conçu ainsi, mais en combinant deux encodeurs 2 vers 1.

La procédure consiste à ajouter trois portes OU à deux encodeurs. Mais ceux-ci sont eux-même composés de portes OU associées à des encodeurs plus petits, et ainsi de suite. On peut poursuivre ainsi jusqu’à tomber sur des encodeurs 4 vers 2, qui sont eux-mêmes composés de deux portes OU. Au final, on se retrouve avec un circuit conçu uniquement à partir de portes OU. Notons qu'il est possible de simplifier le circuit obtenu avec la procédure en fusionnant des portes OU. Si on simplifie vraiment au maximum, le circuit consiste alors en une porte OU à plusieurs entrées par sortie, chacune étant connectée à certaines entrées bien précises. Pour un encodeur 8 vers 3, la simplification du circuit devrait donner ceci :

Encodeur 8 vers 3.

L'encodeur à priorité modifier

L'encodeur à priorité est un dérivé du circuit encodeur, vu dans la section précédente. La différence ne se situe pas dans le nombre d'entrée ou de sortie, ni même dans son interface extérieure. Comme pour l'encodeur normal, l'encodeur à priorité possède entrées numérotées de 0 à et N sorties. Une autre manière plus intuitive de le dire est qu'il possède N entrées et sorties. Pas de changement de ce point de vue. La différence entre encodeur simple et encodeur à priorité tient dans leur fonctionnement, dans le calcul qu'ils font. Avec un encodeur normal, on a supposé que seul un bit d'entrée pouvait être à 1, les autres étant systématiquement à 0. Si cette condition est naturellement remplie dans certains cas d’utilisation, ce n'est pas le cas dans d'autres. L'encodeur à priorité est un encodeur amélioré dans le sens où il donne un résultat valide même quand plusieurs bits d'entrée sont à 1. Il donne donc un résultat pour n'importe quel nombre passé en entrée.

Mais avant de passer aux explications, un peu de terminologie utile. Dans ce qui suit, nous aurons à utiliser des expressions du type "le 1 de poids faible", "le 1 de poids faible" et quelques autres du même genre. Quand nous parlerons du 1 de poids faible, nous voudrons parler du premier 1 que l'on croise dans un nombre en partant de sa droite. Par exemple, dans le nombre 0110 1000, le 1 de poids faible est le quatrième bit. Quant au "1 de poids fort", c'est le premier 1 que l'on croise quand on parcourt le nombre à partir de sa gauche. Dans le cas le plus fréquent, l'encodeur à priorité prend en entrée un nombre et donne la position du 1 de poids fort. Mais dans d'autres cas, l'encodeur à priorité donne la position du 1 de poids faible. Il existe des équivalents, mais qui trouvent cette fois-ci les zéros de poids fort/faible, mais nous n'en parlerons pas dans ce chapitre.

L'encodeur à priorité conçu à partir de sa table de vérité modifier

Il est possible de concevoir l'encodeur à priorité à partir de sa table de vérité, mais les méthodes des minterms ou des maxterms ne donnent pas de très bons résultats.

Notons que ces encodeurs ont souvent une nouvelle entrée notée V, qui indique si la sortie est valide, et qui indique qu'au moins une entrée est à 1. Elle vaut 1 si au moins une entrée est à 1, 0 si toutes les entrées sont à 0.

À titre d'exemple, la table de vérité d'un encodeur à priorité 4 vers 2 est illustré ci-dessous. Le signe X signifie que le bit peut prendre la valeur 0 ou 1 sans que cela change quoique ce soit à l'entrée.

E3 E2 E1 E0 S1 S0 V
0 0 0 0 0 0 0
0 0 0 1 0 0 1
0 0 1 X 0 1 1
0 1 X X 1 0 1
1 X X X 1 1 1

Les équations logiques obtenues sont donc les suivantes :

On voit quelle est la logique de chaque équation. Pour chaque ligne de la table de vérité, il faut vérifier si les bits de poids fort sont à 0, suivi par un 1, les bits de poids faible après le 1 étant oubliées. Pour le bit de validité, il suffit de faire un OU entre toutes les entrées. Les deux dernières équations se simplifient en :

,

Le circuit obtenu est le suivant :

Encodeur à priorité 4 vers 2.

La table de vérité d'un encodeur à priorité 8 vers 3 est illustré ci-dessous. Le signe X signifie que le bit peut prendre la valeur 0 ou 1 sans que cela change quoique ce soit à l'entrée.

Table de vérité d'un encodeur à priorité 8 vers 3.

Utiliser la table de vérité a des défauts. Premièrement, ce n'est pas la meilleure des solutions pour des circuits avec un grand nombre d'entrée. Faire cela donne des tables de vérité rapidement importantes, mêmes pour des encodeurs avec peu de sorties. Le circuit final utilise beaucoup de portes logiques comparé aux autres méthodes. Les solutions alternatives que nous allons voir dans ce qui suit permettent de résoudre ces deux problèmes en même temps.

Les encodeurs à priorité récursifs modifier

Une première solution consiste à créer un gros encodeur à base d'encodeurs plus petits.L'idée de découper le nombre d'entrée en morceaux séparés, chaque morceau étant traité par un encodeur à priorité distinct des autres. Les résultats des différents encodeurs sont ensuite combinés pour donner le résultat final. Naturellement, il est préférable d'utiliser plusieurs exemplaires d'un même encodeur, c'est à dire que pour une entrée de 256 bits, il vaut mieux utiliser soit deux décodeurs 7 vers 128, soit quatre décodeurs 6 vers 64, etc. La construction est similaire à celle vue dans le chapitre précédent, dans la section sur les encodeurs. La différence est que le OU entre les sorties des encodeurs est remplacé par un multiplexeur. Une version générale est illustrée ci-dessous. On voit que les encodeurs ont une sortie de résultat de X bits notée idx et une sortie de validité notée vld.

La sortie de validité finale se calcule en combinant les sorties de validité de chaque encodeur. La sortie est par définition à 1 tant qu'un seul encodeur a une sortie non-nulle, donc quand un seul encodeur a un bit de validité à 1. En clair, c'est un simple OU entre les bits de validité. Reste à déterminer la sortie de donnée, celle qui donne la position du 1 de poids fort. On peut dire que si l'on utilise des encodeurs avec N bits de sortie, alors les N bits de poids faible du résultat seront donnés par le premier encodeur avec une sortie non-nulle. Les résultats de chaque encodeur donnent doncles X bits de poids faible, un seul résultat devant être sélectionné. Le résultat à sélectionner est le premier à avoir un résultat non-nul, donc à avoir un bit de validité à 1. En clair, on peut déterminer quel est le bon encodeur, le bon résultat, en analysant les bits de validité. Mieux : d'après ce qui a été dit, on peut deviner que l'analyse réalisée correspond à trouver la position du premier encodeur à avoir un bit de validité à 1. En clair, c'est l'opération réalisée par un encodeur à priorité lui-même.

Tout cela permet de déterminer les N bits de poids faible, amis les autres bits, ceux de poids fort, sont encore à déterminer. Pour cela, on peut remarquer que ceux-ci sont eux-même fournit par l'encodeur à priorité qui commande le MUX.

Construction d'un encodeur à priorité à partir d'encodeur à priorité plus petits.

Notons qu'avec cette méthode, il est possible, mais pas très intuitif, de fabriquer un encodeur configurable, capable de se comporter soit comme un encodeur de type Find Highest Set, soit de type Find First Set. L'implémentation la plus simple demande de modifier le circuit qui combine les résultats pour qu'il soit configurable et puisse faire les deux opérations à la demande.

L'encodeur à priorité avec un circuit d'isolation du 1 de poids fort/faible modifier

Une autre solution part d'un encodeur normal, auquel on ajoute un circuit qui se charge de sélectionner un seul des bits passé sur son entrée. Le circuit de gestion des priorités a pour fonction de trouver sélectionner un bit et de mettre les autres 1 à 0. Suivant le circuit de priorité considéré, le bit sélectionné est soit le 1 de poids fort, soit le 1 de poids faible. Dans certains cas, le circuit de priorité est configurable et peut trouver l'un ou l'autre suivant ce qu'on lui demande. Dans ce qui va suivre, nous allons partir du principe que l'on souhaite avoir un encodeur qui trouve le 1 de poids fort, sauf indication contraire.

Encodeur à priorité.

Une méthode assez pratique découpe le circuit de gestion des priorité en petites briques de bases, reliées les unes à la suite des autres. L'idée est que les briques de base sont connectées de manière à propager un signal de mise à zéro. Si une brique détecte un 1, elle envoie un signal aux briques précédentes/suivantes, qui leur dit de mettre leur sortie à zéro. Ce faisant, une fois le premier 1 trouvé, on est certain que les autres bits précédents/suivants sont mis à zéro. Suivant les connexions des briques de base, on peut obtenir soit un encodeur qui effectue l'opération Find First Set, soit encodeur de type Find Highest Set et réciproquement. En fait, suivant que les briques soient reliées de droite à gauche ou de gauche à droite, on obtiendra l'un ou l'autre de ces deux encodeurs.

Circuit de gestion des priorités.

Chaque brique de base peut soit recopier le bit en entrée, soit le mettre à zéro. Pour décider quoi faire, elle regarde le signal d'entrée RAZ (Remise A Zéro). Si le bit RAZ vaut 1, la sortie est mise à zéro automatiquement. Dans le cas contraire, le bit passé en entrée est recopié. De plus, chaque brique de base doit fournir un signal de remise à zéro RAZ à destination de la brique suivante. Ce signal RAZ de sortie est mis à 1 dans deux cas : soit si le bit d'entrée vaut, soit quand le signal d'entrée RAZ est à 1. Si vous cherchez à la concevoir à partir d'un table de vérité, vous obtiendrez ceci :

Brique de base du circuit de gestion des priorités d'un encodeur à priorité.
Circuit de gestion des priorité - Circuit de la brique de base.

Le circuit complet d'un encodeur à priorité peut être déduit facilement à partir des raisonnements précédents. Après quelques simplifications, on peut obtenir le circuit suivant. On voit qu'on a ajouté une ligne de briques RAZ à l'encodeur 8 vers 3 vu plus haut.

Encodeur à priorités

Le défaut de cette méthode est que le circuit de gestion des priorité est assez lent. Dans le pire des cas, le signal de remise à zéro traverse toutes les briques de base, soit autant qu'il y a de bits d'entrée. Si chaque brique de base met un certain temps, le temps mis pour que le circuit de priorité fasse son travail est proportionnel au nombre de bits de l'entrée. Cela n'a l'air de rien, mais cela peut prendre un temps rédhibitoire pour les circuits de haute performance, destinés à fonctionner à haute fréquence. Pour ces circuits, on préfère que le temps de calcul soit proportionnel au logarithme du nombre de bits d'entrée, un temps proportionnel étant considéré comme trop lent, surtout pour des opérations simples comme celles étudiées ici.

Une version légèrement différente de ce circuit est utilisée dans le processeur ARM1, un des tout premiers processeur ARM. L'encodeur à priorité était bidirectionnel, à savoir capable de déterminer soit la place du 1 de poids faible, soit du 1 de poids fort. Pour ceux qui veulent en savoir plus, et qui ont déjà un bagage solide en architecture des ordinateurs, voici un lien à ce sujet :

More ARM1 processor reverse engineering: the priority encoder


Dans ce chapitre et les suivants, nous allons voir comment implémenter sous forme de circuits certaines opérations extrêmement courantes dans un ordinateur. Les quelques circuits que nous allons voir seront réutilisés massivement dans les chapitres qui suivront, aussi nous allons passer quelque temps sur ces bases. Pour simplifier, les opérations réalisées par un ordinateur se classent en trois types :

  • Les opérations arithmétiques, à savoir les additions, multiplications et autres, qui auront chacune leur propre chapitre.
  • Les décalages et rotations, où on décale tous les bits d'un nombre d'un ou plusieurs crans vers la gauche/droite, qui feront l'objet d'un futur chapitre.
  • Les opérations logiques qui font l'objet de ce chapitre.

Les opérations logiques manipulent entre une et deux opérandes. Il n'y en a qu'une seule qui agit sur une opérande. C'est l'opération NON, aussi appelée NOT, qui inverse tous les bits d'un nombre. Les autres opérations effectuent un ET/OU/XOR entre deux nombres. A savoir qu'elles font un ET/OU/XOR entre les deux bits de même poids des deux opérandes.

Les portes logiques universelles commandables modifier

Dans cette section, nous allons voir comment créer un circuit qui regroupe toutes les portes logiques en une seule. L'idée est de créer un circuit qui peut effectuer un ET/OU/XOR et toutes les autres opérations, et l'on peut choisir quelle opération réaliser grâce à une entrée de commande.

Par exemple, imaginons un circuit un peu plus limité, capable de faire à la fois un ET, un OU, un XOR et un NXOR. Le circuit contiendra une entrée de commande de 2 bits, et la valeur sur cette entrée permet de sélectionner quelle opération faire : 00 pour un ET, 01 pour un OU, 11 pour un XOR, 01 pour le NXOR.

Nous allons créer un tel circuit, sauf qu'il est capable de faire toutes les opérations entre deux bits et regroupe donc les 16 portes logiques existantes. Nous allons aussi voir la même chose, mais pour les portes logiques de 1 bit.

La porte logique universelle à entrée 1-bit modifier

Pour commencer, nous allons voir des circuits qui modifient un bit d'entrée. Il y a quatre opérations possibles : recopier le bit d'entrée, inverser le bit d'entrée, le mettre à 0, le mettre à 1.

Avec cette liste, on peut déjà avoir l'idée d'une première solution, qui se base sur un circuit multiplexeur. L'idée est de placer le bit d'entrée sur la première entrée du MUX, le bit inversé par une porte NON sur la seconde entrée, un 1 sur la troisième, et un 0 sur la quatrième. Il suffit alors d'attribuer les valeurs adéquates sur l'entrée de commande : 00 = OUI, 01 = NON, 10 = Set, 11 = Reset. Cela demande cependant d'utiliser un MUX 4 vers 2, ce qui est beaucoup pour un simple porte logique universelle.

Porte logique universelle de 1 bit

Mais il y a une solution plus simple, qui demande de combiner plusieurs circuits plus simples.

Les circuits commandables de reset/set/inversion modifier

Pour comprendre comment concevoir ces circuits, il faut rappeler les relations suivantes, qui donnent le résultat d'un ET/OU/XOR entre un bit quelconque noté a et un bit qui vaut 0 ou 1.

Opération Interprétation du résultat
Porte ET Mise à zéro du bit d'entrée
Recopie du bit d'entrée
Porte OU Mise à 1 du bit d'entrée
Recopie du bit d'entrée
Porte XOR Recopie du bit d'entrée
Inversion du bit d'entrée

Avec cela, on peut créer trois circuits assez simples : un circuit qui inverse le bit d'entrée à la demande, un autre qui le met à 1, un autre qui le met à 0. Ces trois circuits ont une entrée de commande qui détermine s'il faut faire l'opération. Le circuit recopie le bit d'entrée si cette entrée est à 0, mais il inverse/set/reset le bit d'entrée si elle est à 1.

Le circuit de mise à 1 commandable est une porte simple OU.

Le circuit d’inversion commandable est une simple porte XOR.

Le circuit de Reset, qui permet de mettre à zéro un bit si besoin, est légèrement plus compliqué. Le circuit prend entrée le bit d'entrée, puis un bit de commande qui indique s'il faut mettre à zéro le bit d'entrée ou non. Le bit de commande en question est appelé le bit Reset. Si le signal Reset est à 1, alors on met à zéro le bit d'entrée, mais on le laisse intact sinon. Le tableau ci-dessus nous dit que la porte ET est tout adaptée : elle recopie le bit d'entrée si l'autre opérande vaut 1, et elle le met à 0 si l'autre opérande vaut 0. La seule différence avec le circuit qu'on veut est que la seconde opérande est l'exact inverse du signal Reste voulu, ce qui signifie qu'on doit ajouter une porte NON. Le tout donne le circuit ci-dessous.

Circuit de mise à zéro d'un bit

Le circuit final de la porte logique universelle de 1 bit modifier

Il nous reste maintenant à combiner ces circuits pour obtenir le circuit voulu. Pour cela, il suffit de placer les trois portes logiques ET/OU/XOR en série, l'une après l'autre. Le problème est qu'il faut préciser la seconde opérande pour chacune des trois portes, alors que l'entrée de commande faire 2 bits. Il faut alors ajouter un circuit combinatoire sur l'entrée de commande pour commander la seconde opérande des trois portes. Tout dépend alors des choix fait pour les valeurs de l'entrée de commande.

Porte logique universelle de 1 bit, faite avec trois portes

Mais il y a moyen de se passer d'une porte logique ! On peut se passer de la porte ET pour mettre à 0 le bit d'entrée, ou de celle pour le mettre à 1. L'idée est que mettre à 0 et mettre à 1 sont deux opérations inverses l'une de l'autre, et l'on a déjà une porte spécialement dans l'inversion. Il suffit donc de mettre la XOR pour inverser le bit d'entrée à la fin du circuit. Juste avant, on place la porte pour mettre à 0 le bit d'entrée : mettre à 1 revient à mettre à 0 dans la porte ET, puis à inverser le résultat. On peut faire la même chose, mais en mettant une porte OU à la place de la porte ET : la porte OU met à 1, et la porte XOR peut inverser si besoin pour mettre à 0. En faisant comme cela, il ne reste que deux portes logiques, donc deux entrées. En choisissant bien les valeurs sur l'entrée de commande, on peut connecter les entrées de commande directement sur les opérandes des deux portes, sans passer par un circuit combinatoire.

Porte logique universelle de 1 bit, faite avec deux portes

La porte universelle commandable 2-bit modifier

Sachez qu'avec un simple multiplexeur, on peut créer un circuit qui effectue toutes les opérations bit à bit possible avec deux bits. Et cela a déjà été utilisé sur de vrais ordinateurs. Pour deux bits, divers théorèmes de l’algèbre de Boole nous disent que ces opérations sont au nombre de 16, ce qui inclus les traditionnels ET, OU, XOR, NAND, NOR et NXOR. Voici la liste complète de ces opérations, avec leur table de vérité ci-dessous (le nom des opérations n'est pas indiqué) :

  • Les opérateurs nommés 0 et 1, qui renvoient systématiquement 0 ou 1 quel que soit l'entrée ;
  • L'opérateur OUI qui recopie l'entrée a ou b, et l'opérateur NON qui l'inverse : , , ,  ;
  • L’opérateur ET, avec éventuellement une négation des opérandes : , , ,  ;
  • La même chose avec l’opérateur OU : , , ,  ;
  • Et enfin les opérateurs XOR et NXOR : , .
a b
0 0 - 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1
0 1 - 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
1 0 - 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1
1 1 - 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1

Le circuit à concevoir prend deux bits, que nous noterons a et b, et fournit sur sa sortie : soit a ET b, soit a OU b, soit a XOR b, etc. Pour sélectionner l'opération, une entrée du circuit indique quelle est l'opération à effectuer, chaque opération étant codée par un nombre. On pourrait penser que concevoir ce circuit serait assez complexe, mais il n'en est rien grâce à une astuce particulièrement intelligente. Regardez le tableau ci-dessus : vous voyez que chaque colonne forme une suite de bits, qui peut être interprétée comme un nombre. Il suffit d'attribuer ce nombre à l'opération de la colonne ! En faisant ainsi, le nombre attribué à chaque opération contient tous les résultats de celle-ci. Il suffit de sélectionner le bon bit parmi ce nombre pour obtenir le résultat. Et on peut faire cela avec un simple multiplexeur, comme indiqué dans le schéma ci-dessous !

Unité de calcul bit à bit de 2 bits, capable d'effectuer toute opération bit à bit.

Il faut noter que le raisonnement peut se généraliser avec 3, 4, 5 bits, voire plus ! Par exemple, il est possible d'implémenter toutes les opérations bit à bit possibles entre trois bits en utilisant un multiplexeur 8 vers 3.

Les unités de calcul logique modifier

Maintenant que nous sommes armés des portes logiques universelles, nous pouvons implémenter un circuit généraliste, qui peut effectuer la même opération logique sur tous les bits. Ce circuit est appelé une unité de calcul logique. Elle prend en entrée deux opérandes, ainsi qu'une entrée de commande sur laquelle on précise quelle opération il faut faire.

Elle est simplement composée d'autant de portes universelles 2 bits qu'il n'y a de bits dans les deux opérandes. Par exemple, si on veut un circuit qui manipule des opérandes 8 bits, il faut prendre 8 portes universelles deux bits. Toutes les entrées de commande des portes sont reliées à la même entrée de commande.

Unité de calcul bit à bit de 4 bits, capable d'effectuer toute opération bit à bit

Les opérations FFS, FFZ, CTO et CLO modifier

Dans cette section, nous allons aborder plusieurs opérations fortement liées entre elles, illustrées dans le schéma ci-dessous. Elles sont très courantes sur la plupart des ordinateurs, surtout dans les ordinateurs embarqués. Beaucoup d'ordinateurs, comme les anciens mac avec des processeurs type Power PC et les processeurs MIPS ou RISC ont des instructions pour effectuer ces opérations.

Mais avant de passer aux explications, un peu de terminologie utile. Dans ce qui suit, nous aurons à utiliser des expressions du type "le 1 de poids faible", "les 0 de poids faible" et quelques autres du même genre. Quand nous parlerons du 0 de poids faible, nous voudrons parler du premier 0 que l'on croise dans un nombre en partant de sa droite. Par exemple, dans le nombre 0011 1011, le 0 de poids faible est le troisième bit en partant de la droite. Quand nous parlerons du 1 de poids faible, c'est la même chose, mais pour le premier bit à 1. Par exemple, dans le nombre 0110 1000, le 1 de poids faible est le quatrième bit. Quant aux expressions "le 1 de poids fort" et "les 0 de poids fort" elles sont identiques aux précédentes, sauf qu'on parcourt le nombre à partir de sa gauche.

Par contre, les expressions "LES 1 de poids faible" ou "LES 0 de poids faible" ne parlent pas de la même chose. Quand nous voudrons parler des 1 de poids faible, au pluriel, nous voulons dire : tous les bits situés avant le 0 de poids faible. Par exemple, prenons le nombre 0011 0011 : les 1 de poids faible correspondent ici aux deux premiers bits en partant de la droite. Même chose quand on parle des zéros de poids faible au pluriel. Quant aux expressions "les 1 de poids fort" ou "les 0 de poids fort" elles sont identiques aux précédentes, sauf qu'on parcourt le nombre à partir de sa gauche.

Les opérations bit à bit complexes modifier

La première opération que nous allons aborder, Find First Set, donne la position du 1 de poids faible. Cette opération est liée à l'opération Count Trailing Zeros, qui donne le nombre de zéros situés à droite de ce 1 de poids faible. L'opération Find First Set est opposée au calcul du Find highest set, qui donne la position du 1 de poids fort. Le nombre de zéros situés à gauche de ce bit à 1 est appelé le Count Leading Zeros.

Ces quatre opérations ont leur équivalents en remplaçant les 0 par des 1 et réciproquement. Par exemple, l'opération Find First Zero donne la position du 0 de poids faible (le plus à droite) et l'opération Find Highest Zero donne la position du 0 de poids fort (le plus à gauche). L'opération Count Trailing Ones donnent le nombre de 1 situés à gauche du 0 de poids fort, tandis que l'opération Count Leading Ones donne le nombre de 1 situés à droite du 0 de poids faible.

Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.

La numérotation des bits et l'équivalence de certaines opérations modifier

Dans toutes ces opérations, les bits sont numérotés, leur numéro étant appelé leur position ou leur indice. La position d'un bit est donc donnée par ce numéro. Ces opérations varient selon la méthode utilisée pour numéroter les bits. On peut commencer à compter les bits à partir de 0, le 0 étant le numéro du bit de poids faible. Mais on peut aussi compter à partir de 1, le bit de poids faible étant celui de numéro 1. Ces deux conventions ne sont pas équivalentes. Si on choisit la première convention, certaines opérations sont équivalentes. Par exemple, les opérations Count Trailing Zeros et Find First Set donnent toutes les deux le même résultat. Avec l'autre convention, les deux différent de 1 systématiquement.

Avec la première convention, pour un nombre codé sur bits, on a :

On voit que certaines opérations sont équivalentes, ce qui nous arrange bien. De plus, on peut calculer le résultat de l'opération FHS à partir de l'opération CLZ et réciproquement. De même le résultat de FHZ peut se calculer à partir du résultat de CLO et inversement. Il suffit d'effectuer une simple soustraction, avec une constante qui plus est, ce qui est simple à fabriquer. Ce qui revient à ajouter un circuit pour faire le calcul .

De fait, nous n'aurons qu'à aborder quatre calculs :

  • le Find First Set, abréviée FFS ;
  • le Find highest set, abrévié FHS ;
  • le Find First Zero, abréviée FFZ ;
  • le Find highest Zero, abrévié FHZ.

On peut même aller plus loin et éliminer encore plus le nombre d'opérations différentes à effectuer. En effet, les opérations FHS et FHZ peuvent se déduire l'une de l'autre, en changeant le nombre passé en entrée. Pour cela, il suffit d'inverser les bits de l'entrée, avec un inverseur commandable. Avec l'inversion, le 1 de poids fort deviendra le 0 de poids fort et inversement. Idem pour les opérations FFS et FFZ. En inversant l'entrée, le 1 de poids faible deviendra le 0 de poids faible et inversement.

Circuit qui effectue les opérations FHS, FFS, CLZ et autres.

L'encodeur à priorité modifier

Reste à voir comment effectuer les deux opérations restantes, à savoir Find Highest Set et Find First Set. Et pour cela, nous devons utiliser un encodeur à priorité. Dans le cas le plus fréquent, l'encodeur à priorité prend en entrée un nombre et donne la position du 1 de poids fort. Ces encodeurs à priorité réalisent l'opération Find Highest Set, qui est reliée à l’opération count leading zeros. Ce qui leur vaut le nom anglais de leading zero detector (LZD) ou encore de leading zero counter (LZC). Mais dans d'autres cas, l'encodeur à priorité donne la position du 1 de poids faible, ce qui correspond à l'opération Count Trailing Zeros. Il existe aussi des encodeurs qui donnent la position du zéro de poids faible, voire du zéro de poids fort, ce qui correspond respectivement aux opérations Find First Zero et Find highest Zero. En clair, pour les quatre opérations précédentes, il existe un encodeur à priorité qui s'en charge.


Les circuits séquentiels modifier

La totalité de l'électronique grand public est basée sur des circuits combinatoires auxquels on ajoute des mémoires. Pour le moment, on sait créer des circuits combinatoires, mais on ne sait pas faire des mémoires. Pourtant, on a déjà tout ce qu'il faut. Il est en effet parfaitement possible de créer des mémoires avec des portes logiques. Toutes les mémoires sont conçues à partir de circuits capables de mémoriser un ou plusieurs bits. Ces circuits sont ce qu'on appelle des bascules, ou flip-flops. Pour une question de simplicité, ce chapitre parlera des circuits capables de mémoriser un bit seulement, pas plusieurs. Nous verrons comment combiner ces bits pour former une mémoire ou des compteurs dans le chapitre suivant.

L'interface d'une bascule modifier

Avant de voir comment sont fabriquées les bascules, nous allons voir quelles sont leurs entrées et leurs sorties. La raison à cela est que des bascules aux entrées-sorties similaires peuvent être construites sur des principes suffisamment différents pour qu'on les voie à part. Si on ne regarde que les entrées-sorties, on peut grosso-modo classer les bascules en quelques grands types principaux : les bascules RS, les bascules JK et les bascules D. Nous ne parlerons pas des bascules JK dans ce qui va suivre, car elles sont très peu utilisées et que nous n'en ferons pas usage dans le reste du cours.

Les bascules D modifier

Interface d'une bascule D.

En premier lieu, on trouve les bascules D, les bascules les plus simples, qui ont deux entrées et deux sorties. Les deux entrées sont appelées D et E : D pour Data, E pour Enable. Le bit à mémoriser est envoyé directement sur l'entrée D. L'entrée Enable permet d'autoriser ou d'interdire les écritures dans la bascule. Ainsi, tant que cette entrée Enable reste à 0, le bit mémorisé par la bascule reste le même, peu importe ce qu'on met sur l'entrée D : il faut que l'entrée Enable passe à 1 pour que l'entrée soit recopiée dans la bascule et mémorisée. On trouve ensuite deux sorties complémentaires qui sont simplement l'inverse l'une de l'autre. La première sortie permet de lire le bit mémorisé dans la bascule RS, la seconde est simplement l'inverse de ce bit.

Les bascules RS modifier

Interface d'une bascule RS.

En second lieu, on trouve les bascules RS, qui possèdent deux entrées et deux sorties. La première sortie permet de lire le bit mémorisé dans la bascule RS, la seconde est simplement l'inverse de ce bit. Les deux sorties sont simplement l'inverse l'une de l'autre. Les deux entrées permettent de placer un 1 ou un 0 dans la bascule. L'entrée R permet de mettre un 1, l'entrée S permet d'y injecter un 0. Pour vous en rappeler, sachez que les entrées de la bascule ne sont nommées ainsi par hasard : R signifie Reset (qui signifie mise à zéro en anglais) et S signifie Set (qui veut dire mise à un en anglais).

Le principe de ces bascules est assez simple :

  • si on met un 1 sur l'entrée R et un 0 sur l'entrée S, la bascule mémorise un zéro ;
  • si on met un 0 sur l'entrée R et un 1 sur l'entrée S, la bascule mémorise un un ;
  • si on met un zéro sur les deux entrées, la sortie Q sera égale à la valeur mémorisée juste avant.
  • Si on met un 1 sur les deux entrées, on ne sait pas ce qui arrivera sur ses sorties. Après tout, quelle idée de mettre la bascule à un en même temps qu'on la met à zéro !
Entrée Reset Entrée Set Sortie Q
0 0 Bit mémorisé par la bascule
0 1 1
1 0 0
1 1 Dépend de la bascule

Le comportement obtenu quand on met deux 1 en entrée dépend de la bascule. Sur certaines bascules, appelées bascules à entrées non-dominantes, la combinaison est interdite : elle fait dysfonctionner le circuit et le résultat est imprédictible. Il faut dire que cette combinaison demande de mettre le circuit à la fois à 0 (entrée R) et à 1 (entrée S). Mais sur d'autres bascules dites à entrée R ou S dominante, l'entrée R sera prioritaire sur l'entrée S ou inversement. Sur les bascules à entrée R dominante, l'entrée R surpasse l'entrée S : la bascule est mise à 0 quand les deux entrées sont à 1. A l'inverse, sur les bascules à entrée S dominante, l'entrée S surpasse l'entrée R : la bascule est mise à 1 quand les deux entrées sont à 1.

Les bascules RS inversées modifier

Bascule RS inversée.

Il existe aussi des bascules RS inversées, où les entrées doivent être mises à 0 pour faire ce qu'on leur demande. Ces bascules fonctionnent différemment de la bascule précédente :

  • si on met un 1 sur l'entrée R et un 0 sur l'entrée S, la bascule mémorise un 1 ;
  • si on met un 0 sur l'entrée R et un 1 sur l'entrée S, la bascule mémorise un 0 ;
  • si on met un 1 sur les deux entrées, la sortie Q sera égale à la valeur mémorisée juste avant ;
  • si on met un 0 sur les deux entrées, le résultat est indéterminé.
Entrée /R Entrée /S Sortie Q
0 0 Interdit
0 1 0
1 0 1
1 1 Bit mémorisé par la bascule

Là encore, quand les deux entrées sont à 0, on fait face à trois possibilités, comme sur les bascules RS normales : soit le résultat est indéterminé, soit l'entrée R prédomine, soit l'entrée S prédomine.

Les bascules JK modifier

Bascule JK.

Les bascules JK peuvent être vues comme des bascules RS améliorées. La seule différence est ce qui se passe quand on envoie un 1 sur les entrées R et S. Sur une bascule RS, le résultat dépend de la bascule, il est indéterminé. Sur les bascules JK, le contenu de la bascule est inversée.

Entrée J Entrée K Sortie Q
0 0 Bit mémorisé par la bascule
0 1 1
1 0 0
1 1 inversion du bit mémorisé

Les bascules JK, RS et RS inversées à entrée Enable modifier

Bascule RS à entrée Enable.

Il est possible de modifier les bascules JK, RS et RS inversées, pour faire permettre d' « activer » ou d' « éteindre » les entrées R et S à volonté. En faisant cela, les entrées R et S ne fonctionnent que si l'on autorise la bascule à prendre en compte ses entrées.

Pour cela, il suffit de rajouter une entrée E à notre circuit. Suivant la valeur de cette entrée, l'écriture dans la bascule sera autorisée ou interdite. Si l'entrée E vaut zéro, alors tout ce qui se passe sur les entrées RS ou JK ne fera rien : la bascule conservera le bit mémorisé, sans le changer. Par contre, si l'entrée E vaut 1, alors les entrées RS ou JK feront ce qu'il faut et la bascule fonctionnera comme une bascule RS/JK normale.

La porte C, une bascule spéciale modifier

Porte-C

Enfin, nous allons voir la porte C, une bascule particulière qui sera utilisée quand nous verrons les circuits et les bus asynchrones. Elle a deux entrées A et B, comme les bascules RS et les bascules D, mais seulement une sortie. Quand les deux entrées sont identiques, la sortie de la bascule correspond à la valeur des entrées (cette valeur est mémorisée). Quand les deux entrées différent, la sortie correspond au bit mémorisé.

Entrée A Entrée B Sortie
0 0 0
0 1 Bit mémorisé par la bascule
1 0 Bit mémorisé par la bascule
1 1 1

L'implémentation des bascules avec des portes logiques modifier

Le principe qui se cache derrière toutes ces bascules est le même. Elles sont organisées autour d'un circuit dont on boucle la sortie sur son entrée. Cela veut dire que sa sortie est connectée à une de ses entrées, les autres entrées étant utilisées pour commander la bascule. Nous allons distinguer l'entrée bouclée et la ou les entrées de commande.

Bascule - fonctionnement interne.

Le circuit doit avoir une particularité bien précise : si l'entrée de commande est à la bonne valeur (0 sur certaines bascules, 1 sur d'autres), l'entrée bouclée est recopiée sur la sortie à l'identique. On dit que le circuit a des entrées potentiellement idempotentes. Ainsi, tant que l'entrée de commande est à la bonne valeur, la bascule sera dans un état stable où la sortie et l'entrée de commande restons à la valeur mémorisée. Le circuit en question peut être une porte logique centrale, qui peut être une porte ET, OU, XOR, NAND, NOR, NXOR, ou un multiplexeur.

Bascule - boucle de rétroaction

Toujours est-il qu'un circuit séquentiel contient toujours au moins une entrée reliée sur une sortie, contrairement aux circuits combinatoires, qui ne contiennent jamais la moindre boucle !

La bascule D fabriquée avec un multiplexeur modifier

Le cas le plus simple de circuit bouclé est la bascule D conçue à partir d'un multiplexeur. L'idée est très simple. Quand l'entrée Enable est à 0, la sortie du circuit est bouclée sur l'entrée : le bit mémorisé, qui était présent sur la sortie, est alors renvoyé en entrée, formant une boucle. Cette boucle reproduit en permanence le bit mémorisé. Par contre, quand l'entrée Enable vaut 1, la sortie du multiplexeur est reliée à l'entrée D. Ainsi, ce bit est alors renvoyé sur l'autre entrée : les deux entrées du multiplexeur valent le bit envoyé en entrée, mémorisant le bit dans la bascule.

Bascule D créée avec un multiplexeur.
Bascule D créée avec un multiplexeur.

Un défaut de cette implémentation est qu'elle ne fournit par la sortie avec le bit inversée. Pour cela, il est possible d'ajouter une porte NON au circuit précédent.

Les deux implémentations possibles, suivant le multiplexeur utilisé modifier

Multiplexeur à deux entrées - circuit
Multiplexeur fabriqué avec des portes à transmission

Pour rappel, un multiplexeur peut s'implémenter de différentes manières, comme nous l'avons vu dans le chapitre sur les circuits de sélection. Il est possible de l'implémenter en utilisant seulement des portes logiques, ou alors en utilisant des pass transistors. Les deux possibilités sont illustrées ci-contre. Pour un multiplexeur fabriqué avec des portes logiques uniquement, boucler sa sortie sur son entrée ne pose aucun problème particulier. Mais les multiplexeurs des circuits haute performance sont généralement conçus en utilisant des portes à transmission. Et un problème survient quand on veut fabriquer une bascule D avec de tels multiplexeurs. En faisant cela, la boucle se résume à une porte à transmission et un fil, sans aucune porte logique entre les deux. Le courant qui circule dans ce fil se dissipe rapidement du fait de la résistance du fil, et disparait. Il n'y a pas de porte logique alimentée en courant/tension qui peut régénérer le signal électrique.

La solution est alors d'utiliser un multiplexeur à base de portes à transmission, mais de rajouter des portes logiques dans la boucle. La solution la plus simple et la plus évidente, est de rajouter une porte OUI (la porte logique qui recopie son entrée sur sa sortie et dont l'utilité n'était pas évidente). Et la manière la plus simple de fabriquer une porte OUI est d'utiliser deux portes NON qui se suivent. La boucle contient alors deux portes NON qui se suivent, ce qui donne le circuit ci-dessous. De plus, cela permet d'avoir les deux sorties Q : la sortie Q inversée est prise en sortie de la première porte NON. Cela garantit que la boucle est alimentée en courant/tension quand elle est fermée. Son contenu ne s'efface pas avec le temps, mais est automatiquement régénéré par les portes NON. L'ensemble sera stable tant que la boucle est fermée.

Implémentation conceptuelle d'une bascule D

Un tel circuit peut sembler bizarre, mais il est très utilisé dans les mémoires dites SRAM, qui composent les registres du processeur ou les mémoires caches. Son avantage certain est qu'il est plus économe en portes logiques. En utilisant un multiplexeur normal, on devrait utiliser une porte NON, une porte OU et deux portes ET. Avec ce circuit, on utilise au maximum quatre transistors et trois portes NON, comme illustré ci-dessous.

Implémentation alternative d'une bascule D.

Et en simplifiant le tout, on peut utiliser deux transistors seulement, au lieu de quatre : on retire un transistor par porte à transmission pour n'en laisser qu'un seul.

Bascule avec un multiplexeur basé sur des pass-through transistors

Les bascules D avec entrées de Set/Reset modifier

Circuit de mise à zéro d'un bit

Certaines bascules D ont une entrée R, qui met à zéro le bit mémorisé dans la bascule quand l'entrée R est à 1. Pour cela, elles ajoutent un circuit de mise à zéro, que nous avons déjà vu dans le chapitre sur les opérations bit à bit. Pour rappel, il s'agit d'un circuit qui prend un bit d'entrée, en plus du bit d'entrée R, et fournit la sortie adéquate. Le bit de sortie est soit égal au bit d'entrée si R = 0, soit égal à 0 si R = 1. e porte ET couplée à une porte NON, comme illustré ci-contre. Ce circuit de mise à zéro est placé après la seconde porte NON, et sa sortie est bouclée sur l'entrée du circuit. Le circuit obtenu est le suivant :

Bascule D avec entrée Reset

Le circuit peut se simplement fortement en fusionnant les trois portes situées entre les deux sorties Q, à savoir la porte ET et les deux portes NON qui la précédent. La loi de De Morgan nous dit que l'ensemble est équivalent à une porte NOR, ce qui donne le circuit suivant :

Bascule D avec entrée Reset, simplifiée

La bascule RS fabriquée avec une porte OU et une porte ET modifier

Un autre exemple est la forme la plus basique de bascule RS : la bascule RS de type ET-OU. Dans celle-ci, on trouve entre trois portes : une porte ET, une porte OU, et éventuellement une porte NON. Un exemple de porte RS de ce type est le suivant, d'autres manières de connecter le tout qui donnent le même résultat. On peut par exemple se passer d'inverseur, ou inverser l'ordre des portes logiques ET et OU. L'essentiel est la boucle indiquée en vert, qui fait que le bit de sortie est recopié sur les entrées bouclées des deux portes, l'ensemble formant une boucle qui relie la sortie à elle-même.

Bascule RS de type ET-OU.

Son fonctionnement est assez simple à expliquer. La porte ET a deux entrées, dont une est bouclée et l'autre est une entrée de commande. Les deux portes recopient leur entrée en sortie si on place ce qu'il faut sur l'entrée de commande. Par contre, toute autre valeur modifie le bit inséré dans la bascule.

  • Si on place un 0 sur l'entrée de commande de la porte OU, elle recopiera l'entrée bouclée sur sa sortie. Par contre, y mettre un 1 donnera un 1 en sortie, peu importe le contenu de l'entrée bouclée. En clair, l'entrée de commande de la porte OU sert d'entrée S à la bascule.
  • La porte ET recopie l'entrée bouclée, mais seulement si on place un 1 sur l'entrée de commande. Si on place un 0, elle aura une sortie égale à 0, peu importe l'entrée bouclée. En clair, l'entrée de commande de la porte ET est l'inverse de ce qu'on attend de l'entrée R à la bascule RS. Pour obtenir une véritable entrée R, il est possible d'ajouter une porte NON sur l'entrée /R, sur l'entrée de la porte ET. En faisant cela, on obtient une vraie bascule RS.

Si on essaye de concevoir le circuit, on se retrouve alors face à un choix : l'ordre dans lequel mettre les portes ET et OU. Les deux portes ET et OU peuvent être mises dans n'importe quel ordre : soit on met la porte ET avant la porte OU, soit on fait l'inverse. La seule différence sera ce qu'il se passe quand on active les deux entrées à la fois. Si la porte ET est située après la porte OU, l'entrée Reset sera prioritaire sur l'entrée Set quand elles sont toutes les deux à 1. Et inversement, si la porte OU est située après, ce sera le signal Set qui sera prioritaire. Voici ci-dessous les tables de vérité correspondantes pour chaque circuit.

Circuit avec la porte ET avant la porte OU
Entrée Reset Entrée Set Sortie Q Circuit
0 0 Bit mémorisé par la bascule Porte OU avant la porte ET.
1 0 0
X (0 ou 1) 1 1
Circuit avec la porte OU avant la porte ET
Entrée Reset Entrée Set Sortie Q Circuit
0 0 Bit mémorisé par la bascule Porte OU avant la porte ET.
0 1 0
1 X (0 ou 1) 1

Une autre manière de voir les choses est que ce circuit possède deux sorties Q équivalentes, situées aux deux points entre les portes OU et ET. Cette façon de voir les choses sera très utile dans ce qui va suivre.

Les bascules RS à NOR et à NAND modifier

Le circuit précédent a bien une sortie Q, mais pas de sortie /Q. Pour la rajouter, il suffit simplement d'ajouter une porte NON sur la sortie Q. Mais faire ainsi ne permet pas de profiter de certaines simplifications bien appréciables. Il est en effet possible de se débarrasser des deux portes NON, celle en amont de la porte ET, et de celle sur la sortie /Q. Pour cela, nous allons procéder d'une autre manière. Au lieu d'ajouter une seule porte NON, nous allons ajouter deux portes, en amont de la porte OU. En faisant, le circuit devient celui-ci :

Bascule RS à NOR - conception à partir d'une bascule ET-OU - 1

On peut alors regrouper des portes logiques consécutives. Premièrement, on peut regrouper la porte OU avec la porte NON immédiatement à sa suite. Mais on peut aussi regrouper la porte ET et les deux portes NON restantes. En effet, nous avons vu dans le chapitre sur les circuits combinatoires que cette combinaison de portes est équivalente à une porte NOR. Le circuit devient donc :

Bascule RS à NOR - conception à partir d'une bascule ET-OU - 2

Le résultat est ce qu'on appelle une bascule RS à NOR, qui tire son nom du fait qu'elle est fabriquée exclusivement avec des portes logiques NOR. En réorganisant le circuit, on trouve ceci :

Circuit d'une bascule RS à NOR.

Il est possible de faire la même manipulation, mais cette fois-ci sur une bascule RS inversée, de type ET-OU encore une fois. La bascule RS inversée est identique à la bascule ET-OU précédente, si ce n'est que la porte NON est placée sur l'entrée R et non sur l'entrée S. En clair, elle est sur une entrée de la porte OU et non sur la porte ET. Le résultat est une bascule RS à NAND, qui est une bascule RS inversée à deux sorties (Q et /Q), composée intégralement de portes NAND.

Circuit d'une bascule RS à NAND.

Les bascules peuvent se fabriquer à partir d'autres bascules modifier

Il y a quelques chapitres, nous avons vu qu'il est possible de créer une porte logique en combinant d'autres portes logiques. Et bien sachez qu'il est possible de faire la même chose pour des bascules. On peut par exemple fabriquer une bascule RS à partir d'une bascule D, et réciproquement. Ou encore, on peut fabriquer une bascule D à partir d'une bascule JK, et inversement. Les possibilités sont nombreuses. Et pour cela, il suffit juste d'ajouter un circuit combinatoire qui traduit les entrées de la bascule voulue vers les entrées de la bascule utilisée.

Le passage d'une bascule RS à une bascule RS inversée (et inversement) modifier

Il est possible de créér une bascule RS normale à partir d'une bascule RS inversée en inversant simplement les entrées R et S avec une porte NON. Et inversement, le passage d'une bascule RS normale à une bascule RS inversée se fait de la même manière. Il est possible de faire cela avec une bascule RS à portes NOR ou NAND, mais ainsi qu'avec une bascule RS à ET-OU.

Bascule RS conçue avec une bascule RS inversée.

Il s'agit d'une méthode simple, qui a la particularité de garder le caractère dominant/non-dominant des entrées. Rappelez-vous que pour des bascules RS à NOR ou RS à NAND, il y a une combinaison d'entrée qui est interdite. Mettez à 1 les deux entrées sur une RS à NOR, et le résultat sera indéterminé, pareil si vous mettez les deux entrées à 0 sur une bascule RS à NAND. Il faut dire qu'une telle combinaison demande de mettre à la bascule à la fois à zéro (signal R) et à 1 (entrée S). De telles bascules sont dites à entrées non-dominantes. Avec une bascule RS de type ET-OU, on n'a pas ce problème : suivant la bascule, on aura l'entrée R qui sera prioritaire, ou l'entrée S (suivant l'ordre des portes logiques : le ET avant le OU ou inversement). On dit alors que l'entrée R est dominante dans le premier cas, que l'entrée S est dominante dans le second. Et bien en mettant deux portes NON en entrée d'une bascule RS inversée, la bascule RS finale gardera le caractère dominant/non-dominant de ses entrées.

Cependant, il est possible de partir d'une bascule RS inversée/normale non-dominante, et d'en faire une bascule RS normale/inversée à entrée R ou S dominante. Pour cela, au lieu d'ajouter deux portes NON en entrée du circuit, on ajoute un petit circuit spécialement conçu. Ce circuit de conversion traduit les signaux d’entrée R et S en signaux /R et /S, (ou inversement). Prenons l'exemple d'une bascule RS normale à entrée R prioritaire, fabriquée à partir d'une bascule RS à NAND (inversée à entrée non-dominantes). La table de vérité du circuit de conversion des entrées est la suivante. Rappelez-vous que l'on veut que l'entrée R soit prioritaire. Ce qui veut dire que si R est à 1, alors on garantit que le signal /R est actif et que /S est inactif. On a donc :

R S
0 0 1 1
0 1 1 0
1 0 0 1
1 1 0 1

L'entrée n'est autre que l'inverse de l'entrée R, ce qui fait qu'une simple porte NON suffit.

L'entrée a pour équation logique :

Le tout donne le circuit suivant :

FF NAND-RS R-dominant

L'implémentation des bascules RS avec une entrée Enable modifier

Bascule RS à entrée Enable fabriquée à partir d'une bascule RS normale.

Dans la section sur l'interface d'une bascule, nous avons vu l'entrée Enable, qui active ou désactive l'écriture dans une bascule. Elle est toujours présente sur les bascules D, mais facultative sur les bascules RS. Les bascules précédentes ne disposent pas de l'entrée Enable, sauf dans le cas de la bascule D. Elles sont donc conceptuellement plus simples que celles qui disposent d'une entrée Enable. Et vous l'avez peut-être senti venir, mais il est possible de modifier les bascules sans entrée Enable, pour leur ajouter cette entrée. Notamment, il est possible de modifier une bascules RS normale pour lui ajouter une entrée Enable. Pour cela, il suffit d'ajouter un circuit avant les entrées R et S, qui inactivera celles-ci si l'entrée E vaut zéro. La table de vérité de ce circuit est identique à celle d'une simple porte ET.

Le circuit obtenu en utilisant une bascule RS à NOR est celui-ci :

Circuit d'une bascule RS à entrée Enable.

Et le circuit obtenu avec une bascule à portes NAND est le suivant. Le circuit est simple à comprendre : on part d'une porte RS inversée à NAND, on ajoute deux portes NON pour en faire une porte RS normale, puis on ajoute les deux portes ET pour l'entrée Enable. Ensuite, on simplifie le circuit en fusionnant les portes ET avec les portes NON, ce qui donne des portes NAND. Le tout donne le circuit simplifié suivant :

Circuit d'une bascule RS à entrée Enable, second.

Les bascules D conçues à partir de bascules RS à entrée Enable modifier

On peut construire une bascule D à partir d'une simple bascule RS ou RS inversée. Il suffit d'ajouter un circuit qui déduise quoi mettre sur les entrées R et S suivant la valeur sur D. Mais en réfléchissant un peu, on se rend compte qu'il est préférable d'utiliser une bascule RS à entrée Enable. En effet, l'entrée Enable de la bascule D et de la bascule RS sont la même, elles ont exactement le même comportement et la même utilité. Dans ce cas, il suffit de prendre une bascule RS à entrée Enable et d'ajouter un circuit qui convertit l'entrée D en Entrées R et S.

Bascule D fabriquée avec une bascule RS, à NOR.

Pour une bascule RS normale, on peut remarquer que l'entrée R est toujours égale à l'inverse de D, alors que S est toujours strictement égale à D. Il suffit d'ajouter une porte NON avant l'entrée R d'une bascule RS à entrée Enable, pour obtenir une bascule D. On peut faire cela à la fois avec des bascules RS à NOR ou à NAND.

Bascule D à NAND.
Bascule D fabriquée à partir d'une bascule RS à entrée Enable de type NAND.

Il est possible d'améliorer légèrement le circuit précédent, afin de retirer la porte NON, en changeant le câblage du circuit. En effet, la porte NON inverse l'entrée D tout le temps, quelque soit la valeur de l'entrée Enable. Mais on n'en a besoin que lorsque l'entrée Enable est à 1. On peut donc remplacer la porte NON par une porte qui sort un 0 quand l'entrée D et l'entrée Enable sont à 1, mais qui sort un 1 sinon. Il s'agit ni plus ni moins qu'une porte NAND, et le circuit précédent la contient déjà : c'est celle en haut à gauche. On peut donc prendre sa sortie pour l'envoyer au bon endroit, ce qui donne le circuit suivant :

Bascule D à NAND.

Il est aussi possible de fabriquer une bascule D avec une bascule RS à ET/OU. Le circuit obtenu est alors identique au circuit obtenu avec un multiplexeur basé sur des portes logiques.

Les bascules JK conçues à partir de bascules RS modifier

Il est possible de construire une bascule JK à partir d'une bascule RS. Ce qui n'est pas étonnant, vu que les bascules RS et JK sont très ressemblantes. Il suffit d'ajouter un circuit qui déduise quoi mettre sur les entrées R et S suivant la valeur sur les entrées J et K. Le circuit en question est composé de deux portes ET, une par entrée.

Bascule JK obtenue à partir d'une bascule RS.

Il est possible de faire la même chose avec une bascule RS à entrée Enable, qui donne une bascule JK à entrée Enable.

Bascule JK obtenue à partir d'une bascule RS à entrée Enable.


Les bascules sont rarement utilisées seules. Elles sont combinées avec des circuits combinatoires pour former des circuits qui possèdent une capacité de mémorisation, appelés circuits séquentiels. L'ensemble des informations mémorisées dans un circuit séquentiel, le contenu de ses bascules, forme ce qu'on appelle l'état du circuit, aussi appelé la mémoire du circuit séquentiel. Un circuit séquentiel peut ainsi être découpé en deux morceaux : des bascules qui stockent l'état du circuit, et des circuits combinatoires pour mettre à jour l'état du circuit et sa sortie. Suivant la méthode utilisée pour déterminer la sortie, on peut classer les circuits séquentiels en deux catégories :

  • les automates de Moore, où la sortie ne dépend que de l'état mémorisé ;
  • et les automates de Mealy, où la sortie dépend de l'état du circuit et de ses entrées.
Ces derniers ont tendance à utiliser moins de portes logiques que les automates de Moore.
Automate de Moore et de Mealy

Concevoir des circuits séquentiels demande d'utiliser un formalisme assez complexe et des outils comme des machines à état finis (finite state machine). Mais nous ne parlerons pas de cela dans ce cours, car nous n'aurons heureusement pas à les utiliser.

La majorité des circuits séquentiels possèdent plusieurs bascules, dont certaines doivent être synchronisées entre elles. Sauf qu'un léger détail vient mettre son grain de sel : tous les circuits combinatoires ne vont pas à la même vitesse ! Si on change l'entrée d'un circuit combinatoire, cela se répercutera sur ses sorties. Mais toutes les sorties ne sont pas mises en même temps et certaines sorties seront mises à jour avant les autres ! Cela ne pose pas de problèmes avec un circuit combinatoire, mais ce n'est pas le cas si une boucle est impliquée, comme dans les circuits séquentiels. Si les sorties sont renvoyées sur les entrées, alors le résultat sur l'entrée sera un mix entre certaines sorties en avance et certaines sorties non-mises à jour. Le circuit combinatoire donnera alors un résultat erroné en sortie. Certes, la présence de l'entrée Enable permet de limiter ce problème, mais rien ne garantit qu'elle soit mise à jour au bon moment. En conséquence, les bascules ne sont pas mises à jour en même temps, ce qui pose quelques problèmes relativement fâcheux si aucune mesure n'est prise.

Le temps de propagation modifier

Pour commencer, il nous faut expliquer pourquoi tous les circuits combinatoires ne vont pas à la même vitesse. Tout circuit, quel qu'il soit, va mettre un petit peu de temps avant de réagir. Ce temps mis par le circuit pour propager un changement sur les entrées vers la sortie s'appelle le temps de propagation. Pour faire simple, c'est le temps que met un circuit à faire ce qu'on lui demande : plus ce temps de propagation est élevé, plus le circuit est lent. Ce temps de propagation dépend de pas mal de paramètres, aussi je ne vais citer que les principaux.

Le temps de propagation des portes logiques modifier

Une porte logique n'est pas un système parfait et reste soumis aux lois de la physique. Notamment, il n'a pas une évolution instantanée et met toujours un petit peu de temps avant de changer d'état. Quand un bit à l'entrée d'une porte logique change, elle met du temps avant de changer sa sortie. Ce temps de réaction pour propager un changement fait sur les entrées vers la sortie s'appelle le temps de propagation de la porte logique. Pour être plus précis, il existe deux temps de propagation : un temps pour passer la sortie de 0 à 1, et un temps pour la passer de 1 à 0. Les électroniciens utilisent souvent la moyenne entre ces deux temps de propagation, et la nomment le retard de propagation, noté .

Temps de propagation d'une porte logique.

Le chemin critique modifier

Délai de propagation dans un circuit simple.

Si le temps de propagation de chaque porte logique a son importance, il faut aussi tenir compte de la manière dont elles sont reliées. La relation entre "temps de propagation d'un circuit" et "temps de propagation de ses portes" n'est pas simple. Deux paramètres vont venir jouer les trouble-fêtes : le chemin critique et la sortance des portes logiques. Commençons par voir le chemin critique, qui n'est autre que le nombre maximal de portes logiques entre une entrée et une sortie de notre circuit. Pour donner un exemple, nous allons prendre le schéma ci-contre. Pour ce circuit, le chemin critique est dessiné en rouge. En suivant ce chemin, on va traverser trois portes logiques, contre deux ou une dans les autres chemins.

Le temps de propagation total, lié au chemin critique, se calcule à partie de plusieurs paramètres. Premièrement, il faut déterminer quel est le temps de propagation pour chaque porte logique du circuit. En effet, chaque porte logique met un certain temps avant de fournir son résultat en sortie : quand les entrées sont modifiées, il faut un peu de temps pour que sa sortie change. Ensuite, pour chaque porte, il faut ajouter le temps de propagation des portes qui précédent. Si plusieurs portes sont reliées sur les entrées, on prend le temps le plus élevé. Enfin, il faut identifier le chemin critique, le plus long : le temps de propagation de ce chemin est le temps qui donne le tempo maximal du circuit.

Temps de propagation par porte logique.
Temps de propagation pour chaque chemin.
Identification du chemin critique.

La sortance des portes logiques modifier

Passons maintenant au second paramètre lié à l'interconnexion entre portes logiques : la sortance. Dans les circuits complexes, il n'est pas rare que la sortie d'une porte logique soit reliée à plusieurs entrées (d'autre portes logiques). Le nombre d'entrées connectées à une sortie est appelé la sortance de la sortie. Il se trouve que plus on connecte de portes logiques sur une sortie, (plus sa sortance est élevée), plus il faudra du temps pour que la tension à l'entrée de ces portes passe de 1 à 0 (ou inversement). La raison en est que la porte logique fournit un courant fixe sur sa sortie, qui charge les entrées en tension électrique. Un courant positif assez fort charge les entrées à 1, alors qu'un courant nul ne charge pas les entrées qui retombent à 0. Avec plusieurs entrées, la répartition est approximativement équitable et chaque entrée reçoit seulement une partie du courant de sortie. Elles mettent plus de temps à se remplir de charges, ce qui fait que la tension met plus de temps à monter jusqu'à 1.

Influence de la sortance d'un circuit sur sa fréquence-période

Le temps de latence des fils modifier

Enfin, il faut tenir compte du temps de propagation dans les fils, celui mis par notre tension pour se propager dans les fils qui relient les portes logiques entre elles. Ce temps perdu dans les fils devient de plus en plus important au cours du temps, les transistors et portes logiques devenant de plus en plus rapides à force de les miniaturiser. Par exemple, si vous comptez créer un circuit avec des entrées de 256 à 512 bits, il vaut mieux le modifier pour minimiser le temps perdu dans les interconnexions que de diminuer le chemin critique.

Les circuits synchrones et asynchrones modifier

Sur les circuits purement combinatoires, le temps de propagation n'est que rarement un souci, à moins de rencontrer des soucis de métastabilité assez compliqués. Par contre, le temps de propagation doit être pris en compte quand on crée un circuit séquentiel : sans ça on ne sait pas quand mettre à jour les bascules du circuit. Si on le fait trop tôt, le circuit combinatoire peut sauter des états : il se peut parfaitement qu'on change le bit placé sur l'entrée avant qu'il ne soit mémorisé. De plus, les différents circuits d'un composant électronique n'ont pas tous le même temps de propagation, et ceux-ci vont fonctionner à des vitesses différentes. Si l'on ne fait rien, on peut se retrouver avec des dysfonctionnements : par exemple, un circuit lent peut rater deux ou trois nombres envoyés par un composant un peu trop rapide.

Pour éviter les ennuis dus à l'existence de ce temps de propagation, il existe deux grandes solutions, qui permettent de faire la différence entre circuits asynchrones et synchrones. Les circuits asynchrones préviennent les bascules quand ils veulent la mettre à jour. Quand le circuit combinatoire et les bascules sont tous les deux prêts, on autorise l'écriture dans les bascules. Mais ce n'est pas cette solution qui est utilisée dans les circuits de nos ordinateurs, qui sont des circuits synchrones. Dans les circuits synchrones, les bascules sont mises à jour en même temps.

Les circuits synchrones modifier

Les circuits synchrones mettent à jour leurs bascules à intervalles réguliers. La durée entre deux mises à jour est constante et doit être plus grande que le temps de propagation le plus long du circuit : on se cale donc sur le circuit combinatoire le plus lent. Les concepteurs d'un circuit doivent estimer le pire temps de propagation possible pour le circuit et ajouter une marge de sûreté. Pour mettre à jour les circuits à intervalles réguliers, le signal d'autorisation d'écriture est une tension qui varie de façon cyclique : on parle alors de signal d'horloge. Le temps que met la tension pour effectuer un cycle est ce qu'on appelle la période. Le nombre de périodes par seconde est appelé la fréquence. Elle se mesure en hertz. On voit sur ce schéma que la tension ne peut pas varier instantanément : elle met un certain temps pour passer de 0 à 1 et de 1 à 0. On appelle cela un front. Le passage de 0 à 1 est appelé un front montant et le passage de 1 à 0 un front descendant.

Fréquence et période.

Un point important est que le signal d'horloge passe régulièrement de 0 à 1. Dans le cas idéal, 50% de la période est à l'état 1 et les 50% restants à l'état 0. On a alors un signal carré. Mais il arrive que le temps passé à l'état 1 ne soit pas forcément le même que le temps passé à 0. Par exemple, le signal peut passer 10% de la période à l'état 1 et 90% du temps à l'état 0. C'est assez rare, mais possible et même parfois utile. Le signal n'est alors pas appelé un signal carré, mais un signal rectangulaire. Le signal d'horloge est donc à 1 durant un certain pourcentage de la période. Ce pourcentage est appelé le rapport cyclique (duty cycle).

Signaux d'horloge asymétriques

En faisant cela, le circuit mettra ses sorties à jour lors d'un front montant (ou descendant) sur son entrée d'horloge. Entre deux fronts montants (ou descendants), le circuit ne réagit pas aux variations des entrées. Rappelons que seuls les circuits séquentiels doivent être synchronisés ainsi, les circuits combinatoires étant épargnés par les problématiques de synchronisation. Pour que les circuits séquentiels soient cadencés par une horloge, les bascules du circuit sont modifiées de manière à réagir aux fronts montants et/ou aux fronts descendants, ce qui fait que la mise à jour de l'état interne du circuit est synchronisée sur l'horloge. Évidemment, l’horloge est envoyée au circuit via une entrée spéciale : l'entrée d'horloge. L'horloge est ensuite distribuée à l'intérieur du composant, jusqu'aux bascules, par un ensemble de connexions qui relient l'entrée d'horloge aux bascules.

Circuit séquentiel synchrone.

En théorie, plus un composant utilise une fréquence élevée, plus il est rapide. C'est assez intuitif : plus un composant peut changer d'état un grand nombre de fois par seconde, plus, il peut faire de calculs, et plus il est performant. Cela n'est toutefois pas un élément déterminant : un processeur de 4 gigahertz peut être bien plus rapide qu'un processeur de 200 gigahertz, pour des raisons techniques qu'on verra plus tard dans ce cours. Mais dans les grandes lignes, une hausse de la fréquence signifie une performance plus élevée. Les processeurs et mémoires ont vu leur fréquence augmenter au fil du temps, ce qui explique en partie pourquoi leur performance a augmenté au cours du temps. Pour donner un ordre de grandeur, le premier microprocesseur avait une fréquence de 740 kilohertz (740 000 hertz), alors que les processeurs actuels montent jusqu'à plusieurs gigahertz : plusieurs milliards de fronts par secondes ! Mais cela a eu pour défaut d'augmenter la consommation d'énergie des processeurs, et la chaleur qu'ils émettent. Car, comme on le verra dans plusieurs chapitres, un composant chauffe d'autant plus qu'il a une fréquence élevée. Les premiers processeurs étaient refroidis par un simple radiateur, alors que les processeurs modernes demandent un radiateur, un ventilateur et une pâte thermique de qualité pour dissiper leur chaleur. Pour limiter la catastrophe, les fabricants de processeurs ont inventé diverses techniques permettant de diminuer la consommation énergétique et la dissipation thermique d'un processeur, mais cela est un sujet pour un autre chapitre. Toujours est-il que l'augmentation en fréquence des processeurs modernes est de plus en plus contrainte par la dissipation de chaleur et la consommation d'énergie.

Dans un ordinateur moderne, chaque composant a sa propre horloge, qui peut être plus ou moins rapide que les autres. Par exemple, le processeur fonctionne avec une horloge différente de l'horloge de la mémoire RAM ou des périphériques. La présence de plusieurs horloges vient du fait que certains composants sont plus lents que d'autres. Plutôt que de caler tous les composants d'un ordinateur sur le plus lent en utilisant une seule horloge, il vaut mieux utiliser une horloge différente pour chaque composant : les mises à jour des circuits sont synchronisées à l'intérieur d'un composant (dans un processeur, ou une mémoire), alors que les composants eux-mêmes synchronisent leurs communications avec d'autres mécanismes. Ces multiples signaux d'horloge dérivent d'une horloge de base qui est « transformée » en plusieurs horloges, grâce à des montages électroniques spécialisés (des PLL ou des montages à portes logiques un peu particuliers).

Les circuits asynchrones modifier

Les circuits asynchrones n'utilisent pas d'horloge pour synchroniser leurs composants/sous-circuits. L’asynchrone permet à deux circuits/composants de se synchroniser, l'un des deux étant un émetteur, l'autre étant un récepteur. Pour se synchroniser, l’émetteur indique au récepteur qu'il lui a envoyé une donnée. Le récepteur réceptionne alors la donnée et indique qu'il a pris en compte les données envoyées. Cette synchronisation se fait grâce à des fils spécialisés du bus de commande, qui transmettent des bits particuliers.

Communication asynchrone

La transmission des données/requêtes peut se faire de deux manières différentes : la première utilise un signal de requête qui indique que de nouvelles données sont disponibles, la seconde n'envoie que des données dupliquées. Ces deux méthodes portent les noms de Bundled Encoding et de Multi-Rail Encoding. La première est la plus intuitive, car elle correspond à l'encodage des bits que nous utilisons depuis le début de ce cours, alors que la seconde est inédite à ce point du cours.

Le Bundled Encoding modifier

Avec la méthode du Bundled Encoding, aussi appelée codage simple-track, la synchronisation utilise deux fils : REQ et ACK (des mots anglais request =demande et acknowledg(e)ment =accusé de réception). Le fil REQ indique au récepteur que l'émetteur lui a envoyé une donnée, tandis que le fil ACK indique que le récepteur a fini son travail et a accepté la donnée entrante.

Plus rarement, un seul fil est utilisé à la fois pour la requête et l'acquittement, ce qui limite le nombre de fils. Un 1 sur ce fil signifie qu'une requête est en attente (le second composant est occupé), tandis qu'un 0 indique que le second composant est libre. Ce fil est manipulé aussi bien par l'émetteur que par le récepteur. L'émetteur met ce fil à 1 pour envoyer une donnée, le récepteur le remet à 0 une fois qu'il est libre.

Signaux de commande d'un bus asynchrone

Si l'on utilise deux fils séparés, le codage des requêtes et acquittements peut se faire de plusieurs manières. Deux d'entre elles sont très utilisées et sont souvent introduites dans les cours sur les circuits asynchrones. Elles portent les noms de protocole à 4 phases et protocole à 2 phases. Elles ne sont cependant pas les seules et beaucoup de protocoles asynchrones utilisent des méthodes alternatives, mais ces deux méthodes sont très pédagogiques, d'où le fait qu'on les introduise ici.

Protocoles de transmission asynchrone à 2 et 4 phases. Les chiffres correspondent au nombre de fronts de la transmission.

Avec le protocole à 4 phases, les requêtes d'acquittement sont codées par un bit et/ou un front montant. Les signaux REQ/ACK sont mis à 1 en cas de requête/acquittement et repassent 0 s'il n'y en a pas. Le protocole assure que les deux signaux sont remis à zéro à la fin d'une transmission, ce qui est très important pour le fonctionnement du protocole. Lorsque l'émetteur envoie une donnée au récepteur, il fait passer le fil REQ de 0 à 1. Cela dit au récepteur : « attention, j'ai besoin que tu me fasses quelque chose ». Le récepteur réagit au front montant et/ou au bit REQ et fait ce qu'on lui a demandé. Une fois qu'il a terminé, il positionne le fil ACK à 1 histoire de dire : j'ai terminé ! les deux signaux reviennent ensuite à 0, avant de pouvoir démarrer une nouvelle transaction.

Avec le protocole à deux phases, tout changement des signaux REQ et ACK indique une nouvelle transmission, peu importe que le signal passe de 0 à 1 ou de 1 à 0. En clair, les signaux sont codés par des fronts montants et descendants, et non par le niveau des bits ou par un front unique. Il n'y a donc pas de retour à 0 des signaux REQ et ACK à la fin d'une transmission. Une transmission a lieu entre deux fronts de même nature, deux fronts montants ou deux fronts descendants.

Le tout est illustré ci-contre. On voit que le protocole à 4 phases demande 4 fronts pour une transmission : un front montant sur REQ pour le mettre à 1, un autre sur ACk pour indiquer l'acquittement, et deux fronts descendants pour remettre les deux signaux à 0. Avec le protocole à 2 phases, on n'a que deux fronts : deux fronts montants pour la première transmission, deux fronts descendants pour la suivante. D'où le nom des deux protocoles : 4 et 2 phases.

Le Multi-Rail Encoding modifier

Les circuits asynchrones précédents, qui le Bundled Encoding, utilisent un fil par bit de données et un fil pour le signal REQ. Mais cette manière de faire a quelques défauts, le principal étant la sensibilité aux délais. Pour faire simple, la conception du circuit doit prendre en compte le temps de propagation dans les fils : il faut garantir que le signal REQ arrive au second circuit après les données, ce qui est loin d'être trivial. Pour éviter cela, d'autres circuits utilisent plusieurs fils pour coder un seul bit, ce qui donne un codage multiple-rails.

Le cas le plus simple utilise deux fils par bit, ce qui lui vaut le nom de codage dual-rail.

Dual-rail protocol

Il en existe plusieurs sous-types, qui différent selon ce qu'on envoie sur les deux fils qui codent un bit.

  • Certains circuits asynchrones utilisent un signal REQ par bit, d'où la présence de deux fils par bit : un pour le bit de données, et l'autre pour le signal REQ.
  • D'autres codent un bit de données sur deux bits, certaines valeurs indiquant un bit invalide.
Protocole 3 états

Les bascules synchrones modifier

Utiliser une horloge demande cependant d'adapter les circuits vus précédemment, les bascules devant être modifiées. En effet, les bascules précédentes sont mises à jour quand un signal d'autorisation est mis à 1. Mais avec un signal d'horloge, les bascules doivent être mises à jour lors d'un front, montant ou descendant, peu importe. Pour cela, les bascules ont une entrée d'autorisation d'écriture modifiée, qui réagit au signal d'horloge. Les bascules commandées par une horloge sont appelées des bascules synchrones.

Les bascules synchrones peuvent mettre à jour leur contenu soit lors d'un front montant, soit d'un front descendant, soit les deux, soit lorsque la tension d'horloge est à 1. Suivant le cas, le symbole utilisé pour représenter l'entrée d'horloge est différent, comme illustré ci-dessous.

Symboles des bascules synchrones.
Notons qu'en anglais, le terme bascule se dit flip-flop ou latch. De nos jours, le terme latch étant utilisé pour les bascules non-synchrones, alors que le terme flip-flop est utilisé pour désigner les bascules synchrones.

Les types de bascules synchrones modifier

Il existe plusieurs types de bascules synchrones, qu'on peut classer en fonction de leurs entrées-sorties.

Symbole d'une bascule D synchrone.

La plus simple est la bascule D synchrone est une bascule D modifiée de manière à mettre à jour son contenu sur un front (montant). Celle-ci possède deux entrées : une entrée D sur laquelle on envoie la donnée à mémoriser (entrée d'écriture), et une autre pour l'horloge. Elle contient entre une et deux sorties : une pour la donnée mémorisée (sortie de lecture) et éventuellement une autre pour son opposé. Son fonctionnement est simple : son contenu est mis à jour avec ce qu'il y a sur l'entrée D, mais seulement lors d'un front (montant ou descendant suivant la bascule). Plus rares, certaines bascules D contiennent des entrées R et S pour les mettre à zéro ou à 1. La plupart des bascules D ont une entrée R pour les remettre à zéro, tandis que l'entrée S est absente, celle-ci étant peu utile.

Entrée CLK Entrée D Sortie Q
Front montant (ou descendant, suivant la bascule) 0 0
1 1
Pas de front montant 0 ou 1 Pas de changement
Bascule SR synchrone.

Il existe aussi des versions synchrones des bascules RS à entrée Enable. Sur celles-ci, l'entrée Enable est juste remplacée par une entrée pour l’horloge. On les appelle des bascules RS synchrones.

Entrée CLK Entrée R Entrée S Sortie Q
Front montant (ou descendant, suivant la bascule) 0 0 Pas de changement
0 1 Mise à 1
1 0 Mise à 0
1 1 Indéterminé.
Pas de front montant 0 ou 1 0 ou 1 Pas de changement
Bascule synchrone JK.

Les bascules JK ont aussi leur version synchrone, les bascules JK synchrones.

Entrée CLK Entrée R Entrée S Sortie Q
Front montant (ou descendant, suivant la bascule) 0 0 Pas de changement
0 1 Mise à 1
1 0 Mise à 0
1 1 Inversion du bit mémorisé
Pas de front montant 0 ou 1 0 ou 1 Pas de changement
Bascule T.

La bascule T est une bascule qui n'existe que comme bascule synchrone. Elle possède deux entrées : une entrée d'horloge et une entrée nommée T. Cette bascule inverse son contenu quand l'entrée T est à 1, mais à condition qu'il y ait un front sur le signal d'horloge. En clair, l'inversion a lieu quand il y a à la fois un front et un 1 sur l'entrée T. Si l'entrée T est maintenu à 1 pendant longtemps, cette bascule inverse son contenu à chaque cycle d'horloge. À ce propos, l'entrée T tire son nom du mot anglais Toggle, qui veut dite inverser.

Entrée CLK Entrée T Sortie Q
Front montant (ou descendant, suivant la bascule) 0 Pas de changement
1 Inversion du contenu de la bascule
Pas de front montant 0 ou 1 Pas de changement
Chronogramme qui montre le fonctionnement d'une bascule T. Le chronogramme montre comment évolue la sortie Q en fonction du temps, en fonction de l'entrée d'horloge C et de l'entrée T.
Cette bascule est utilisée pour fabriquer des compteurs, des circuits dans lesquels des bits doivent régulièrement être inversées.
Bascule T simplifiée.

La bascule T simplifiée est une bascule T dont l'entrée T a été retiré. Cette bascule change d'état à chaque cycle d'horloge, sans besoin d'autorisation de la part d'une entrée T.

Entrée CLK Sortie Q
Front montant (ou descendant, suivant la bascule) Inversion du bit mémorisé
Pas de front montant Pas de changement

L'intérieur d'une bascule synchrone modifier

Pour fabriquer une bascule synchrone, les solutions sont nombreuses et dépendent de si l'on parle d'une bascule D ou d'une bascule RS. Néanmoins, une méthode assez courante, et assez simple, est de partir d'une bascule non-synchrone, puis de la modifier pour la rendre synchrone. Les bascules les plus indiquées pour cela sont les bascules avec une entrée Enable : il suffit de transformer l’entrée Enable en entrée d'horloge. Évidemment, cela demande de faire quelques modifications. Il ne suffit pas d'envoyer le signal d'horloge sur l'entrée Enable pour que cela marche.

La méthode la plus simple consiste à placer deux bascules D l'une à la suite de l'autre. De plus, l'entrée Enable de la seconde bascule est précédée d'une porte NON. Avec cette méthode, la première bascule est mise à jour quand l’horloge est à 0, la seconde étant mise à jour avec le contenu de la première quand l'horloge est à 1. Dans ces conditions, la sortie finale de la bascule est mise à jour après un front montant.

Negative-edge triggered master slave D flip-flop

On peut faire exactement la même chose avec deux bascules asynchrones RS l'une à la suite de l'autre.

Bascule R synchrone basée sur des bascules RS non-synchrones.
Bascule D cadencée par une horloge.

Une autre méthode associe trois bascules RS normales, les deux premières formant une couche d'entrée qui commande la troisième bascule. Ces deux bascules d'entrée vont en quelque sorte traiter le signal à envoyer à la troisième bascule. Quand le signal d'horloge est à 0, les deux bascules d'entrée fournissent un 1 sur leur sortie : la troisième bascule reste donc dans son état précédent, sans aucune modification. Quand l'horloge passe à 1 (front montant), seule une des deux bascules va fournir un 1 en sortie, l'autre voyant sa sortie passer à 0. La bascule en question dépend de la valeur de D : un 0 sur l'entrée D force l'entrée R de la troisième bascule, un 1 forçant l'entrée S. Dit autrement, le contenu de la troisième bascule est mis à jour. Quand l'entrée d'horloge passe à 1, les bascules se figent toutes dans leur état précédent. Ainsi, la troisième bascule reste commandée par les deux bascules précédentes, qui maintiennent son contenu (les entrées R et S restent à leur valeur obtenue lors du front montant).

Il est aussi possible de créer une bascule D synchrone avec des transistors. Nous n'étudierons pas ce cas, qui est franchement compliqué et relève plus de l'état de l'art que d’autre chose. Tout au plus, nous pouvons nous contenter de vous donner le circuit obtenu, pour le plaisir de vos yeux.

True single-phase edge-triggered flip-flop with reset

Fabriquer des bascules synchrones à partir d'autres bascules synchrones modifier

Une bascule JK synchrone se fabrique facilement à partir d'une bascule RS synchrones, ce qui n'est pas étonnant quand on sait que leur comportement est presque identique, la seule différence étant ce qui se passe quand les entrées RS sont toutes les deux à 1. Il suffit, comme pour une bascule JK asynchrone, d'ajouter quelques circuits pour convertir les entrées JK en entrées RS.

Bascule JK synchrone, conçue à partir d'une bascule RS synchrone.

La bascule D synchrone peut se fabriquer partir d'une bascule JK ou RS synchrone. Il suffit alors d'ajouter un circuit combinatoire pour traduire les entrées D et E en entrées RS ou JK.

Bascule D fabriquée avec une bascule JK synchrone. Bascule D fabriquée avec une bascule RS synchrone.

La bascule T simplifiée est la version la plus simple de bascule T, celle qui n'a pas d'entrée T et se contente d'inverser son contenu à chaque cycle d'horloge. La fabriquer est assez simple : il suffit de prendre une bascule D synchrone et de relier sa sortie /Q à son entrée D. On peut aussi faire la même chose avec une bascule JK synchrone ou une bascule RS synchrone.

Bascule T simplifiée fabriquée avec une bascule D synchrone. Bascule T simplifiée fabriquée avec une bascule RS synchrone. Bascule T simplifiée fabriquée avec une bascule JK synchrone.

Une bascule T normale peut s’implémenter une bascule T simplifiée, une bascule RS synchrone ou une bascule JK synchrone. Pour le circuit basé sur une bascule T simplifiée, l'idée est de faire un ET entre l'entrée T et le signal d'horloge, ce ET garantissant que le signal d’horloge est mis à 0 si l'entrée T est à zéro.

Bascule T simplifiée, fabriquée avec une bascule T simplifiée. Bascule T simplifiée, fabriquée avec une bascule RS synchrone. Bascule T fabriquée avec une bascule JK.

L'utilisation de l'horloge pour les circuits combinatoires : la logique dynamique MOS modifier

La logique dynamique permet de créer des portes logiques ou des bascules d'une manière assez intéressante. Logiquement, nous aurions dû la voir dans le chapitre sur les transistors et les portes logiques, mais le problème est que cette logique fait appel à un signal d'horloge. Et aussi étonnant que cela puisse paraître, le signal d’horloge est alors utilisé pour fabriquer des circuits combinatoires !

Un transistor MOS peut servir de condensateur modifier

Les technologies CMOS conventionnelles mettent la sortie d'une porte logique à 0/1 en la connectant à la tension d'alimentation ou à la masse. La logique pass transistor transfère la tension et le courant de l'entrée vers la sortie. Dans les deux cas, la sortie est connectée directement ou indirectement à la tension d'alimentation quand on veut lui faire sortie un 1. Avec la logique dynamique, ce n'est pas le cas. La sortie est maintenue à 0 ou à 1 en utilisant un réservoir d'électron qui remplace la tension d'alimentation.

En électronique, il existe un composant qui sert de réservoir à électricité : il s'agit du condensateur. On peut le charger en électricité, ou le vider pour fournir un courant durant une petite durée de temps. Par convention, un condensateur stocke un 1 s'il est rempli, un 0 s'il est vide. L'intérieur d'un condensateur est formé de deux couches de métal conducteur, séparées par un isolant électrique. Les deux plaques de conducteur sont appelées les armatures du condensateur. C'est sur celles-ci que les charges électriques s'accumulent lors de la charge/décharge d'un condensateur. L'isolant empêche la fuite des charges d'une armature à l'autre, ce qui permet au condensateur de fonctionner comme un réservoir, et non comme un simple fil.

Il est possible de fabriquer un pseudo-condensateur avec un transistor MOS. En effet, tout transistor MOS a un pseudo-condensateur caché entre la grille et la liaison source-drain. Pour comprendre ce qui se passe dans ce transistor de mémorisation, il faut savoir ce qu'il y a dans un transistor CMOS. À l'intérieur, on trouve une plaque en métal appelée l'armature, un bout de semi-conducteur entre la source et le drain, et un morceau d'isolant entre les deux. L'ensemble forme donc un condensateur, certes imparfait, qui porte le nom de capacité parasite du transistor. Suivant la tension qu'on envoie sur la grille, l'armature va se remplir d’électrons ou se vider, ce qui permet de stocker un bit : une grille pleine compte pour un 1, une grille vide compte pour un 0.

Anatomie d'un transistor CMOS

L'utilisation de transistors MOS comme condensateur n'est pas spécifique à la logique dynamique. Certains mémoires RAM le font, comme nous le verrons dans le chapitre sur les cellules mémoires. Aussi, il est intéressant d'en parler maintenant, histoire de préparer le terrain. D'ailleurs, les mémoires RAM sont remplies de logique dynamique.

L'utilisation des pseudo-condensateurs en logique dynamique modifier

Un circuit conçu en logique dynamique contient un transistor est utilisé comme condensateur. Il s’insère entre la tension d'alimentation et la sortie du circuit. Son rôle est simple : lorsqu'on utilise la sortie, le condensateur se vide, ce qui place la sortie à 1. le reste du temps, le condensateur est relié à la tension d'alimentation et se charge. Un circuit en logique dynamique effectue son travail en deux phases : une phase d'inactivité où il remplit ses condensateurs, et une phase où sa sortie fonctionne. Les deux phases sont appelées la phase de précharge et la phase d'évaluation. La succession de ces deux phases est réalisée par le signal d'horloge : la première pahse a lieu quand le signal d'horloge est à 1, l'autre quand il est à 0.

Une porte NAND en logique dynamique CMOS modifier

Voici un exemple de porte NAND en logique dynamique MOS. La porte est alors réalisée avec des transistors NMOS et PMOS, le circuit ressemble à ce qu'on a en logique NMOS. En bas, on trouve les transistors NMOS pour relier la sortie au 0 volt. Mais au-dessus, on trouve un transistor CMOS qui remplace la résistance. Le fonctionnement du circuit est simple. Quand l'entrée clock est à 1, le condensateur se charge, les deux transistors NMOS sont déconnectés de la masse et le circuit est inactif. Puis, quand clock passe à 0, Le transistor PMOS se comporte en circuit ouvert, ce qui déconnecte la tension d'alimentation. Et son pseudo-condensateur se vide, ce qui fournit une tension d'alimentation de remplacement temporaire. Le transistor NMOS du bas se ferme, ce qui fait que les deux transistors A et B décident de si la sortie est connectée au 0 volt ou non. Si c'est le cas, le pseudo-condensateur se vide dans le 0 volt et la sortie est à 0. Sinon, le pseudo-condensateur se vide dans la sortie, ce qui la met à 1.

Porte NAND en logique CMOS.

Une bascule D en logique dynamique CMOS modifier

Il est possible de créer une bascule D en utilisant la logique dynamique. L'idée est de prendre une bascule D normale, mais d'ajouter un fonctionnement en deux étapes en ajoutant des transistors/interrupteurs. Pour rappel, une bascule D normale est composée de deux inverseurs reliés l'un à l'autre en formant une boucle, avec un multiplexeur pour permettre les écritures dans la boucle.

Implémentation conceptuelle d'une bascule D
Animation du fonctionnement de la bascule précédente.

Le circuit final ajoute deux transistors entre les inverseurs tête-bêche. Les transistors en question sont reliés à l'horloge, l'un étant ouvert quand l'autre est fermé. Grâce à eux, le bit mémorisé circule d'un inverseur à l'autre : il est dans le premier inverseur quand le signal d'horloge est à 1, dans l'autre inverseur quand il est à 0 (en fait son inverse, comme vous l'aurez compris). Le tout est illustré ci-contre. Cette implémentation a été utilisée autrefois, notamment dans le processeur Intel 8086.

Bascule D en logique Dynamique, avec entrée Enable

Il existe une variante très utilisée, qui permet de remplacer le multiplexeur par un circuit légèrement plus simple. Avec elle, on a deux entrées pour commander la bascule, et non une seule entrée Enable. L'entrée Enable autorise les écriture, l'entrée Hold ferme la boucle qui relie la sortie du second inverseur au premier. Chaque entrée est associé à un transistor/interrupteur. Le transistor sur lequel on envoie l'entrée Enable se ferme uniquement lors des écritures et reste fermé sinon. A l'inverse, le transistor relié au signal Hold est fermé en permanence, sauf lors des écritures. En clair, les deux signaux sont l'inverse l'un de l'autre. Il permet de fermer le circuit, de bien relier les deux inverseurs en tête-bêche, sauf lors des écritures. On envoie donc l'inverse de l'entrée Enable sur ce transistor.

Bascule D en logique dynamique

Une manière de comprendre le circuit précédent est de le comparer à celui avec le multiplexeur. Le multiplexeur est composé d'une porte NON et de deux transistors. Il se trouve que les deux transistors en question sont placés au même endroit que les transistors connectés aux signaux Hold et Enable. En prenant retirant la porte NON du multiplexeur, on se retrouve avec le circuit. Au lieu de prendre un Signal Enable qui commande les deux transistors, ce qui demande d'ajouter une porte NON vu que les deux transistors doivent faire l'inverse l'un de l'autre, on se contente d'envoyer deux signaux séparés pour commander chaque transistor indépendamment.

Avantages et inconvénients modifier

Les circuits en logique dynamique sont opposés aux circuits en logique statique, ces derniers étant les circuits CMOS, PMOS, NMOS ou TTL vu jusqu'à présent. Les circuits dynamiques et statiques ont des différences notables, ainsi que des avantages et inconvénients divers. Si on devait résumer :

  • la logique dynamique utilise généralement un peu plus de transistors qu'un circuit CMOS normal ;
  • la logique dynamique est souvent très rapide par rapport à la concurrence, car elle n'utilise que des transistors NMOS, plus rapides ;
  • la consommation d'énergie est généralement supérieure comparé au CMOS.

Un désavantage de la logique dynamique est qu'elle utilise plus de transistors. On économise certes des transistors MOS, mais il faut rajouter les transistors pour déconnecter les transistors NMOS de la masse (0 volt). Le second surcompense le premier.

Un autre désavantage est que le signal d'horloge ne doit pas tomber en-dessous d'une fréquence minimale. Avec une logique statique, on a une fréquence maximale, mais pas de fréquence minimale. Avec un circuit statique peut réduire la fréquence d'un circuit pour économiser de l'énergie, pour améliorer sa stabilité, et de nombreux processeurs modernes ne s'en privent pas. On peut même stopper le signal d'horloge et figer le circuit, ce qui permet de le mettre en veille, d'en stopper le fonctionnement, etc. Impossible avec la logique dynamique, qui demande de ne pas tomber sous la fréquence minimale. Cela a un impact sur la consommation d'énergie, sans compter que cela se marie assez mal avec certaines applications. Un processeur moderne ne peut pas être totalement fabriqué en logique dynamique, car il a besoin d'être mis en veille et qu'il a besoin de varier sa fréquence en fonction des besoins.

La distribution de l'horloge dans un circuit complexe modifier

L’horloge est distribuée aux bascules et autres circuits à travers un réseau de connexions électriques qu'on appelle l'arbre d'horloge. L'arbre d'horloge le plus simple, illustré dans la première image ci-dessous, relie directement l'horloge à tous les composants à cadencer.

Arbre d’horloge simple.
Arbre d'horloge avec des buffers (les triangles sur le schéma).

Il va de soit que l'arbre d'horloge est beaucoup plus compliqué avec la logique dynamique qu'avec la logique statique. Avec la logique statique, seules les bascules doivent recevoir le signal d'horloge, avec éventuellement quelques rares circuits annexes. Mais avec la logique dynamique, toutes les portes logiques doivent recevoir le signal d'horloge, ce qui rend la distribution de l'hrologe beaucoup plus compliquée. C'est un point qui fait que la logique dynamique est assez peu utilisée, et souvent limitée à quelques portions bien précise d'un processeur.

Les défauts inhérents aux arbres d’horloge modifier

Un problème avec cette approche est la sortance de l'horloge. Cette dernière est connectée à trop de composants, ce qui la ralentit. Pour éviter tout problème, on peut ajouter des buffers, de petits répéteurs de signal. S'ils sont bien placés, ils réduisent la sortance nécessaire et empêchent que le signal de l'horloge s'atténue en parcourant les fils.

Un autre problème très fréquent sur les circuits à haute performance est qu'une bonne partie de la consommation d'énergie a lieu dans l'arbre d'horloge. Ce n'était pas le cas avec les anciens transistors dits bipolaires, ou les anciennes technologies de fabrication de circuits imprimés, mais c'est devenu un problème depuis l'arrivée des transistors CMOS. Nous en reparlerons dans le chapitre sur la consommation énergétique des ordinateurs, mais il est intéressant de mettre quelques chiffres sur ce phénomène. Entre 20 à 30% de la consommation énergétique des processeurs modernes a lieu dans l'arbre d'horloge. En comparaison, les circuits asynchrones se passent de cette consommation d'énergie, sans compter que leurs mécanismes de synchronisations sont moins gourmands en courant. Ils sont donc beaucoup plus économes en énergie et chauffent moins. Malheureusement, leur difficulté de conception les rend peu courants.

Le décalage d’horloge (clock skew) modifier

Clock skew lié aux temps de transmission dans les fils.

Un problème courant sur les circuits à haute fréquence est que les fils qui transmettent l’horloge ont chacun des délais de transmission différents. La raison principale à cela est qu'ils n'ont pas la même longueur, ce qui fait que l'électricité met plus de temps à traverser les quelques micromètres de différence entre fils. En conséquence, les composants sont temporellement décalés les uns d'avec les autres, même si ce n'est que légèrement. Ce phénomène est appelé le décalage d'horloge, traduction du terme clock skew utilisé en langue anglaise. Il ne pose pas de problème à faible fréquence et/ou pour des fils assez courts, mais c'est autre chose pour les circuits à haute fréquence. Pour éviter les effets néfastes du clock skew sur les circuits haute-fréquence, on doit concevoir l'arbre d'horloge avec des techniques assez complexes.

Par exemple, on peut jouer sur la forme de l'arbre d'horloge. Dans les schémas du dessus, l'arbre d'horloge part d'un côté du processeur, de là où se trouve la broche pour l'horloge. En faisant cela, un côté du processeur recevra l'horloge avant l'autre, entraînant l'apparition d'un délai entre la gauche du processeur et sa droite. Pour éviter cela, on peut faire partir l'horloge du centre du processeur. Le fil de l'horloge part de la broche d'horloge, va jusqu’au centre du processeur, puis se ramifie de plus en plus en direction des composants. En faisant cela, on garantit que les délais sont équilibrés entre les deux côtés du processeur. Cependant, il existera quand même un délai entre les composants proches du centre et ceux sur les bords du processeur. Mais le délai maximal est minimisé. Entre un délai proportionnel à la largeur du processeur, et un délai proportionnel à la distance maximale centre-bord (environ la moitié de la diagonale), le second est plus faible.

Il arrive que le clock skew soit utilisé volontairement pour compenser d'autres délais de transmission. Pour comprendre pourquoi, imaginons que deux composants soient reliés l'une avec l'autre, le premier envoyant ses données au second. Il y a évidemment un petit délai de transmission entre les deux. Mais sans clock skew, les deux composants recevront l'horloge en même temps : le receveur captera un front montant de l'horloge avant les données de l'émetteur. En théorie, on devrait cadencer l'horloge de manière à ce que ce délai inter-composants ne pose pas de problème. Mais cela n'est pas forcément la meilleure solution si on veut fabriquer un circuit à haute fréquence. Pour éviter cela, on peut ajouter un clock skew, qui retardera l’horloge du receveur. Si le clock skew est supérieur ou égal au temps de transmission inter-composants, alors le receveur réceptionnera bien le signal de l'horloge après les données envoyées par l'émetteur. On peut ainsi conserver un fonctionnement à haute fréquence, sans que les délais de transmission de données ne posent problème. Cette technique porte le nom barbare de source-synchronous clocking.

Interaction entre le clock skew et le délai de transmission entre deux circuits.

Les domaines d'horloge modifier

Il existe des composants électroniques qui sont divisés en plusieurs morceaux cadencés à des fréquences différentes. Par exemple, les processeurs modernes contiennent plusieurs cœurs (pour simplifier, on a regroupé plusieurs processeurs dans un même boitier, et chaque processeur a été renommé cœur), chacun capable d’exécuter un programme/logiciel. Chaque cœur a sa propre fréquence, et il est possible que deux cœurs différents aillent à des fréquences différentes. Par exemple, il est possible de faire varier dynamiquement la fréquence de chaque cœur suivant leur charge de travail : les cœurs très utilisés iront à haute fréquence, alors que ceux ayant peu de calculs à faire iront à basse fréquence. Comme autre possibilité, il est possible de créer un processeur avec des cœurs puissants à haute fréquence et des cœurs basse-consommation à basse fréquence. C'est le cas sur certaines puces ARM et sur les processeurs Intel Core de 12e génération, de micro-architecture "Alder Lake". Comme autre exemple, les processeurs modernes intègrent tous une carte graphique, les deux étant sur la même puce de silicium, dans le même boitier. Mais le processeur va à une fréquence plus élevée que la carte graphique : plusieurs gigahertz pour le processeur, plusieurs centaines de mégahertz pour la carte graphique. De tels composants sont très fréquents et nous en verrons quelques autres dans la suite du cours. Et pour parler de ces composants, il est très utile d'introduire la notion de domaine d'horloge.

Un domaine d'horloge est l'ensemble des registres qui sont reliés au même signal d'horloge, associé aux circuits combinatoires associés. Il n'incorpore pas l'arbre d'horloge ni même le circuit de génération du signal d'horloge, mais c'est là un détail. La plupart des composants électroniques ont un seul domaine d'horloge, ce qui veut dire que tout le circuit est cadencé à une fréquence unique, la même pour toute la puce. D'autres ont plusieurs domaines d'horloge qui vont à des fréquences distinctes. Les raisons à cela sont multiples, mais la principale est que autant certains circuits ont besoin d'être performants et donc d'avoir une haute fréquence, d'autres peuvent très bine faire leur travail à une fréquence plus faible. L'usage de plusieurs domaines d'horloge permet à une portion critique de la puce d'être très rapide, tandis que le reste de la puce va à une fréquence inférieure. Une autre raison est l’interfaçage entre deux composants allant à des vitesses différentes, par exemple pour faire communiquer un processeur avec un périphérique.

Il arrive que deux domaines d'horloge doivent communiquer ensemble et s'échanger des données, et l'on parle alors de clock domain crossing. Et cela pose de nombreux problèmes, du fait de la différence de fréquence. Les deux domaines d'horloge ne sont pas synchronisés, n'ont pas la même fréquence, la même phase, rien ne colle. Dans les explications qui suivent, on va prendre l'exemple de l'échange d'un bit entre deux domaines d'horloge, qui va d'un domaine d'horloge source vers un domaine d'horloge de destination. Dans ce cas, on peut passer par l'intermédiaire d'une bascule inséré entre les deux domaines d'horloge, bascule cadencée à la fréquence du domaine d'horloge source.

Mais pour l'échange d'un nombre, les choses sont plus compliquées et insérer un registre entre les deux domaines d'horloge ne marche pas. En effet, lors d'un changement de valeur du nombre à transmettre, tous les bits du nombre n'arrivent pas au même moment dans le registre. Il est possible que le domaine d'horloge de destination voit un état transitoire, où seule une partie des bits a été mise à jour. Le résultat est que le domaine d'horloge de destination utilisera une valeur transitoire faussée, causa tout un tas de problèmes. Pour éviter cela, les nombres transmis entre deux domaines d'horloge sont encodés en code Gray, dans lequel les états transitoires n'existent pas. Pour rappel, entre deux nombres consécutifs en code Gray, seul un bit change.


Dans les chapitres précédents, nous avons vu comment mémoriser un bit, dans une bascule. Mais les bascules en elles-mêmes sont rarement utiles seules, car les données à mémoriser font généralement plusieurs bits, pas un seul. Stocker plusieurs bits est la raison d'être des registres, des composants qui mémorisent des plusieurs bits, que l'on peut modifier et/ou récupérer plus tard. Il existe plusieurs types de registres, et nous allons faire la distinction entre les registres simples et les registres à décalage. Les registres simples sont capables de mémoriser un nombre, de taille fixe, rien de plus. Les registres à décalage sont des registres simples améliorés, capables de faire quelques petites opérations sur leur contenu.

Les registres simples modifier

Les registres simples sont capables de mémoriser un nombre, codé sur une quantité fixe de bits. On peut à tout moment récupérer le nombre mémorisé dans le registre : on dit alors qu'on effectue une lecture. On peut aussi mettre à jour le nombre mémorisé dans le registre, le remplacer par un autre : on dit qu'on effectue une écriture. Les seules opérations possibles sur ces registres sont la lecture (récupérer le nombre mémorisé dans le registre) et l'écriture (mettre à jour le nombre mémorisé dans le registre, le remplacer par un autre).

L'interface d'un registre simple modifier

Registre de 4 Bits. On voit que celui-ci contient 4 entrées (à gauche), et 4 sorties (à droite). On peut aussi remarquer une entrée CLK, qui joue le rôle d'entrée d'autorisation.

Niveau entrées et sorties, les registres possèdent des entrées-sorties pour les données mémorisées, mais aussi des entrées-sorties de commande. Les entrées-sorties pour les données permettent de lire le contenu du registre ou d'y écrire. Les entrées de commande permettent de configurer le registre pour lui ordonner de faire une écriture, pour le remettre à zéro, ou toute autre opération.

Les entrées de données sont utilisées pour l'écriture, alors que les sorties de données servent pour la lecture. Le nombre mémorisé dans le registre est disponible sur les sorties du registre. Pour utiliser les entrées d'écriture, on envoie le nombre à mémoriser (celui qui remplacera le contenu du registre) sur les entrées d'écriture et on configure les entrées de commande adéquates.

Les entrées de commande varient suivant le registre, mais on trouve au moins une entrée Enable, qui a le même rôle que pour une bascule, à savoir autoriser une écriture. Si l'entrée Enable est à 1, le registre mémorise ce qu'il y a sur l'entrée de donnée. Mais si l'entrée Enable est à 0, le registre n'est pas mis à jour : on peut mettre n'importe quelle valeur sur les entrées, le registre n'en tiendra pas compte et ne remplacera pas son contenu par ce qu'il y a sur l'entrée. Pour résumer, l'entrée Enable sert donc à indiquer au registre si son contenu doit être mis à jour, quand une écriture a lieu. D'autres entrées de commandes sont parfois présentes, la plus commune étant une entrée permettant de remettre à zéro le registre. La présence d'un 1 sur cette entrée remet à zéro le contenu du registre, à savoir que celui-ci contient la valeur zéro.

L'intérieur d'un registre simple modifier

Un registre est composé de plusieurs bascules D qui sont toutes mises à jour en même temps. Pour cela, toutes les entrées E des bascules sont reliées à l'entrée de commande Enable.

Registre.

$

Les registres à décalage modifier

Les registres à décalage sont des registres dont le contenu est décalé d'un cran vers la gauche ou la droite sur commande. Nous aurons à les réutiliser plus tard dans ce cours, notamment dans la section sur les circuits de génération de nombres aléatoires, ou dans certains circuits liés au cache. Les registres à décalage sont presque tous synchrones et ce chapitre ne parlera que ce ces derniers. L'animation suivante illustre le fonctionnement d'un registre à décalage qui décale son contenu d'un cran vers la droite à chaque cycle d'horloge.

Registre à décalage.

La classification des registres modifier

On peut classer les registres selon le caractère de l'entrée et de la sortie, qui peut être parallèle (entrée de plusieurs bits) ou série (entrée d'un seul bit).

  • Sur les registres simples, les entrées et sorties pour les données sont toujours parallèles. Pour un registre de N bits, il y a une entrée d'écriture de N bits et une sortie de N bits. C'est la raison pour laquelle ils sont appelés des registres à entrées et sorties parallèles.
  • Sur les registres à entrée et sortie série, on peut mettre à jour un bit à la fois, de même qu'on ne peut en récupérer qu'un à la fois. Ces registres servent essentiellement à mettre en attente des bits tout en gardant leur ordre : un bit envoyé en entrée ressortira sur la sortie après plusieurs commandes de mise à jour sur l'entrée Enable.
  • Les registres à décalage à entrée série et sortie parallèle sont similaires aux précédents : on peut ajouter un nouveau bit en commandant l'entrée Enable et les anciens bits sont alors décalés d'un cran. Par contre, on peut récupérer (lire) tous les bits en une seule fois. Ils permettent notamment de reconstituer un nombre qui est envoyé bit par bit sur un fil (un bus série).
  • Enfin, il reste les registres à entrée parallèle et sortie série. Ces registres sont utiles quand on veut transmettre un nombre sur un fil : on peut ainsi envoyer les bits un par un.
Classification des registres à décalage.

Pour résumer, on distingue quatre types de registres (à décalage ou non), qui portent les noms de PIPO, PISO, SIPO et SISO. Les noms peuvent sembler barbares, mais il y a une logique derrière ces termes.La lettre P est pour parallèle, la lettre S est pour série. La lettre I signifie Input, ce qui veut dire entrée en anglais, la lettre O est pour Output, la sortie en anglais.

Classification des registres
Entrée parallèle Entrée série
Sortie parallèle PIPO (registre simple) SIPO
Sortie série PISO SISO

L'intérieur d'un registre à décalage modifier

Tous les registres sont conçus en plaçant plusieurs bascules les unes à la suite des autres, que ce soit pour les registres simples ou les registres à décalage. La seule différence tient dans la manière dont les bascules sont reliées. Toutes les bascules sont reliées à l'entrée d'horloge, l'entrée Enable, l'entrée Reset, ou aux autres entrées de commandes. Mais c'est une autre paire de manche pour les entrées/sorties de données.

Dans un registre simple, les bascules sont indépendantes et ne sont pas reliées entre elles.

Registre simple.

À l'inverse, dans les registres à décalage, il existe des connexions entre bascules. Plus précisément, les bascules sont reliées les unes à la suite des autres, elles forment une chaîne de bascules reliées deux à deux. Et les connexions entre bascules sont les mêmes que l'on parle d'un registre à décalage de type SIPO, PISO ou SISO.

Exemple de registre à décalage

Outre le fait que les bascules sont reliées de la même manière, les autres connexions sont les mêmes dans tous les registres. L'entrée d'horloge (non-représentée dans les schémas qui vont suivre) est envoyée à toutes les bascules. Même chose pour l'entrée Enable, qui est reliée aux entrées E de toutes les bascules. La différence entre ces registres tient dans les endroits où se trouvent les entrées et les sorties du registre.

Implémentation des registres avec des bascules.
Registre à entrée et sortie série.
Registre à entrée et sortie parallèle.
Registre à entrée série et sortie parallèle.
Registre à entrée parallèle et sortie série.

Une utilisation des registres : les mémoires SRAM modifier

Maintenant que nous avons les registres, il est temps d'en montrer une utilisation assez intéressante. Nous allons combiner les registres avec des multiplexeurs/démultiplexeurs pour former une mémoire adressable. Plus précisément, nous allons voir les mémoires de type SRAM, qui peuvent être vu comme un rassemblement de plusieurs registres. Mais ces registres ne sont pas assemblés pour obtenir un registre plus gros : par exemple, on peut fabriquer un registre de 32 bits à partir de 2 registres de 16 bits, ou de 4 registres de 8 bits. Ce n'est pas ce qui est fait sur les mémoires adressables, où les registres sont regroupés de manière à ce qu'il soit possible de sélectionner le registre qu'on veut consulter ou modifier.

Pour préciser le registre à sélectionner, chacun d'entre eux se voit attribuer un nombre : l'adresse. On peut comparer une adresse à un numéro de téléphone (ou à une adresse d'appartement) : chacun de vos correspondants a un numéro de téléphone et vous savez que pour appeler telle personne, vous devez composer tel numéro. Les adresses mémoires en sont l'équivalent pour les registres d'une mémoire adressable. Il existe des mémoires qui ne fonctionnent pas sur ce principe, mais passons : ce sera pour la suite.

Exemple : on demande à la mémoire de sélectionner le byte d'adresse 1002 et on récupère son contenu (ici, 17).

L'interface d'une mémoire SRAM modifier

Interface d'une SRAM.

Niveau entrées et sorties, une mémoire SRAM contient souvent des entrées-sorties dédiées aux transferts de données et plusieurs entrées de commande.

Les entrées de commande permettent de configurer la mémoire pour effectuer une lecture ou écriture, la mettre en veille, ou autre. Parmi les entrées de commande, on trouve une entrée de plusieurs bits, sur laquelle on peut envoyer l'adresse, appelée l'entrée d'adressage. On trouve aussi une entrée R/W d'un bit, qui permet de préciser si on veut faire une lecture ou une écriture. On trouve aussi parfois une entrée Enable Ou Chip Select, qui indique si la RAM est activée ou mise en veille, qui ressemble à l'entrée Enable des bascules.

Pour les données, tout dépend de la mémoire SRAM considérée. Sur certaines mémoires, on trouve une sortie sur laquelle on peut récupérer le registre sélectionné (on dit qu'on lit le registre) et une entrée sur laquelle on peut envoyer une donnée destinée à être écrite dans le registre sélectionné (on dit qu'on écrit le registre). On a donc une sortie pour la lecture et une entrée pour l'écriture. Mais sur d'autres mémoires SRAM, l'entrée et la sortie sont fusionnées en une seule entrée-sortie.

L'intérieur d'une mémoire RAM modifier

Une telle mémoire peut se fabriquer assez simplement : il suffit d'un ou de plusieurs multiplexeurs et de registres. Quand on présente l'adresse sur l'entrée de sélection du multiplexeur, celui-ci va connecter le registre demandé à la sortie (ou à l'entrée).

Intérieur d'une RAM fabriquée avec des registres et des multiplexeurs.

Les mémoire mortes et mémoires vives modifier

Les mémoires vues plus haut sont fabriquées avec des registres, eux-mêmes fabriqués avec des bascules, elles-mêmes fabriquées avec des portes logiques et/ou des transistors. Elles sont appelées des mémoires SRAM. Elles sont très utilisées, surtout dans les processeurs. Mais les mémoires sont très diverses et les mémoires SRAM ne sont qu'un type de mémoires parmi tant d'autres. Les mémoires SRAM font elles-mêmes partie de la catégorie des mémoires vives, aussi appelées mémoires RAM (bien que ce soit un abus de langage, comme on le verra dans plusieurs chapitres). De telles mémoires sont des mémoires électroniques, qui sont adressables, dans lesquelles on peut lire et écrire.

Il existe deux types de mémoires RAM : les mémoires SRAM vues auparavant, et les mémoires DRAM. La différence est leur caractère statique contre dynamique. Les mémoires SRAM sont des mémoires RAM statiques, alors que les mémoires DRAM sont des mémoires RAM dynamiques. C'est de là que vient le S de SRAM (S pour Statique) et le D de DRAM (D pour Dynamique). Les données d'une mémoire statique ne s'effacent pas tant qu'elles sont alimentées en courant. Par contre, les données des mémoires dynamiques s'effacent en quelques millièmes ou centièmes de secondes si l'on n'y touche pas. Il faut donc réécrire chaque bit de la mémoire régulièrement, ou après chaque lecture, pour éviter qu'il ne s'efface. On dit qu'on doit effectuer régulièrement un rafraîchissement mémoire. Le rafraîchissement prend du temps et a tendance à légèrement diminuer la rapidité des mémoires dynamiques. Les mémoires SRAM sont surtout utilisées dans les registres du processeur ou pour des mémoires de petite taille. Par contre, les mémoires DRAM sont utilisées pour des mémoires de plus grande taille, comme la mémoire principale de l'ordinateur. La raison est que les mémoires SRAM sont très gourmandes en transistors et en portes logiques, comparé aux mémoires DRAM. En contrepartie, les mémoires SRAM sont très rapides.

Outre les mémoires RAM, il existe des mémoires qui sont elles aussi électroniques, adressables, mais dans lesquelles on ne peut pas écrire : ce sont les mémoires ROM. Si on ne peut pas écrire dans une ROM, certaines permettent cependant de réécrire intégralement leur contenu : on dit qu'on reprogramme la ROM. Insistons sur la différence entre reprogrammation et écriture : l'écriture permet de modifier un byte sélectionné/adressé, alors que la reprogrammation efface toute la mémoire et la réécrit en totalité. De plus, la reprogrammation est généralement beaucoup plus lente qu'une écriture, sans compter qu'il est plus fréquent d'écrire dans une mémoire que la reprogrammer. Ce terme de programmation vient du fait que les mémoires ROM sont souvent utilisées pour stocker des programmes sur certains ordinateurs assez simples.

On peut classer les mémoires ROM en plusieurs types :

  • les mask ROM sont fournies déjà programmées et ne peuvent pas être reprogrammées ;
  • les mémoires PROM sont fournies intégralement vierges, et on peut les programmer une seule fois ;
  • les mémoires RPROM sont reprogrammables, ce qui signifie qu'on peut les effacer pour les programmer plusieurs fois ;
    • les mémoires EPROM s'effacent avec des rayons UV et peuvent être reprogrammées plusieurs fois de suite ;
    • certaines RPROM peuvent être effacées par des moyens électriques : ce sont les mémoires EEPROM.

Les mémoires de type mask ROM sont utilisées dans quelques applications particulières. Par exemple, elles étaient utilisées sur les vieilles consoles de jeux, pour stocker le jeu vidéo dans les cartouches. Elles servent aussi pour les firmware divers et variés, comme le firmware d'une imprimante ou d'une clé USB. De telles mémoires seront utiles dans les chapitres qui vont suivre. La raison en est que tout circuit combinatoire peut être remplacé par une mémoire adressable ! Imaginons que l'on souhaite créer un circuit combinatoire qui pour toute entrée A fournisse la sortie B. Celui-ci est équivalent à une ROM dont la lecture de l'adresse A renvoie B sur la sortie. Cette logique est notamment utilisée dans certains circuits programmables, les FPGA, comme on le verra plus tard.


Illustration du fonctionnement d'un compteur modulaire binaire de 4 bits, avec un pas de compteur de 1 (le contenu est augmenté de 1 à chaque mise à jour).

Les compteurs/décompteurs sont des circuits électroniques qui mémorisent un nombre qu'ils mettent à jour régulièrement. Cette mise à jour augmente ou diminue le compteur d'une quantité fixe, appelée le pas du compteur. Suivant la valeur du pas, on fait la différence entre les compteurs d'un côté et les décompteurs de l'autre. Comme leur nom l'indique, les compteurs comptent alors que les décompteurs décomptent. Les compteurs augmentent le contenu du compteur à chaque mise à jour, alors que les décompteurs le diminuent. Dit autrement, le pas d'un compteur est positif, alors que le pas d'un décompteur est négatif. Les compteurs-décompteurs peuvent faire les deux, suivant ce qu'on leur demande.

Les compteurs/décompteurs : généralités modifier

Les compteurs et décompteurs sont très variés et ils se distinguent sur un grand nombre de points.

La plupart des compteurs utilisent un pas constant, qui est fixé à la création du compteur, ce qui simplifie la conception du circuit. Par exemple, les compteurs incrémenteurs/décrémenteurs ont un pas fixe de 1, à savoir que le contenu de leur registre est incrémenté de 1 à chaque cycle d'horloge ou demande d'incrémentation. D'autres ont un pas variable, à savoir qu'il change à chaque incrémentation/décrémentation. Cela peut aussi servir à compter quelque chose qui varie de manière non-régulière. Par exemple, imaginez un circuit qui compte combien de voitures sont rentrées sur une autoroute par un péage bien précis. Plusieurs voitures peuvent rentrer sur le péage durant la même minute, presque en même temps. Pour cela, il suffit de prendre un compteur à pas variable, qui est incrémenté du nombre de voiture rentrées sur l'autoroute lors de la dernière période de temps. Évidemment, de tels compteurs à pas variables ont une entrée supplémentaire sur laquelle on peut envoyer le pas du compteur.

Suivant le compteur, la représentation du nombre mémorisé change : certains utilisent le binaire traditionnel, d'autres le BCD, d'autre le code Gray, etc. Mais tous les compteurs que nous allons voir seront des compteurs/décompteurs binaires, à savoir que les nombres qu'ils utilisent sont codés sur bits. Au passage, le nombre de bits du compteur est appelé la taille du compteur, par analogie avec les registres.

Vu que la taille d'un compteur est limitée, il cesse de compter au-delà d'une valeur maximale. La plupart des compteurs comptent de 0 à , avec la taille du compteur. D'autres compteurs ne comptent pas jusque-là : leur limite est plus basse que . Par exemple, certains compteurs ne comptent que jusqu'à 10, 150, etc. Ils sont appelés des compteurs modulo. Prenons un compteur modulo 6, par exemple : il compte de 0 à 5, et est remis immédiatement à zéro quand il atteint 6. Il compte donc comme suit : 0, 1, 2, 3, 4, 5, 0, 1, 2, ...

Outre la valeur de la limite du compteur, il est aussi intéressant de se pencher sur ce qui se passe quand le compteur atteint cette limite. Certains restent bloqués sur cette valeur maximale tant qu'on ne les remet pas à zéro "manuellement" : ce sont des compteurs à saturation. D'autres recommencent à compter naturellement à partir de zéro : ce sont des compteurs modulaires.

L'interface d'un compteur/décompteur modifier

Compteurs et décompteurs sont presque tous des circuits synchrones, à savoir cadencés par une horloge. Du moins, c'est le cas des compteurs/décompteurs que nous allons voir dans ce chapitre, par souci de simplicité. Un compteur/décompteur est donc relié à une entrée d'horloge.

De plus, certains compteurs ont une entrée Enable qui active/désactive le comptage. Le compteur s'incrémente/décrémente seulement si l'entrée Enable est à 1, mais ne fait rien si elle est à 0. Les compteurs les plus simples sont incrémentés/décrémentés à chaque cycle d'horloge et n'ont pas d'entrée Enable, mais les compteurs plus complexes en ont une. Ce faisant, le compteur/décompteur est incrémenté seulement si deux conditions sont réunies : un front montant sur le signal d'horloge, et une entrée Enable à 1. Dans les schémas qui vont suivre, nous ferons mention soit au signal d'horloge, soit à l'entrée Enable. Si on fait référence à l'entrée Enable, il est sous-entendu que les bascules du registre sont cadencées par une horloge, qui leur dit quand s'actualiser.

En outre, les compteurs ont souvent une entrée Reset qui permet de les remettre à zéro.

Sur les compteurs/décompteurs, il y a une entrée qui décide s'il faut compter ou décompter. Typiquement, elle est à 1 s'il faut compter et 0 s'il faut décompter.

Compteur 4 Bits.
Compteur 4 Bits avec entrée Reset.
Compteur 4 Bits avec entrée pour décider s'il faut compter ou décompter.

Un compteur/décompteur peut parfois être initialisé avec la valeur de notre choix. Pour cela, ils possèdent une entrée d'initialisation sur laquelle on peut placer le nombre initial, couplée à une entrée Reset qui indique si le compteur doit être réinitialisé ou non. Certains compteurs/décompteurs spécifiques n'ont pas d'entrée d'initialisation, mais seulement une entrée de reset, mais il s'agit là d'utilisations assez particulières où le compteur ne peut qu'être réinitialisé à une valeur par défaut. Pour les compteurs/décompteurs, il faut aussi rajouter une entrée qui précise s'il faut compter ou décompter.

Le circuit d'un compteur : généralités modifier

Un compteur/décompteur peut être vu comme une sorte de registre (ils peuvent stocker un nombre), mais qu'on aurait amélioré de manière à le rendre capable de compter/décompter. Tous les compteurs/décompteurs utilisent un registre pour mémoriser le nombre, ainsi que des circuits combinatoires pour calculer la prochaine valeur du compteur. Ce circuit combinatoire est le plus souvent, mais pas toujours, un circuit capable de réaliser des additions (compteur), des soustractions (décompteurs), voire les deux (compteur-décompteur). Plus rarement, il s'agit de circuits conçus sur mesure, dans le cas où le pas du compteur est fié une bonne fois pour toutes.

Fonctionnement d'un compteur (décompteur), schématique

Comme dit plus haut, certains compteurs ont une valeur maximale qui est plus faible que la valeur maximale du registre. Par exemple, on peut imaginer un compteur qui compte de 0 à 9 : celui-ci est construit à partir d'un registre de 4 bits qui peut donc compter de 0 à 15 ! Ces compteurs sont construits à partir d'un compteur modulo, auquel on rajoute un circuit combinatoire. Ce dernier détecte le dépassement de la valeur maximale et remet à zéro le registre quand c'est nécessaire, via l'entrée de Remise à zéro (entrée Reset).

Compteur modulo N.

L'incrémenteur/décrémenteur modifier

Certains compteurs, aussi appelés incrémenteurs comptent de un en un. Les décompteurs analogues sont appelés des décrementeurs. Nous allons voir comment créer ceux-ci dans ce qui va suivre. Il faut savoir qu'il existe deux méthodes pour créer des incrémenteurs/décrémenteurs. La première donne ce qu'on appelle des incrémenteurs asynchrones, et l'autre des incrémenteurs synchrones. Nous allons commencer par voir comment fabriquer un incrémenteur asynchrone, avant de passer aux incrémenteurs synchrones.

L'incrémenteur/décrémenteur asynchrone modifier

Pour fabriquer un incrémenteur asynchrone, la première méthode, il suffit de regarder la séquence des premiers entiers, puis de prendre des paires de colonnes adjacentes :

  • 000 ;
  • 001 ;
  • 010 ;
  • 011 ;
  • 100 ;
  • 101 ;
  • 110 ;
  • 111.

Pour la colonne la plus à droite (celle des bits de poids faible), on remarque que celle-ci inverse son contenu à chaque cycle d'horloge. Pour les colonnes suivantes, le bit sur une colonne change quand le bit de la colonne précédente passe de 1 à 0, en clair, lorsqu'on a un front descendant sur la colonne précédente. Maintenant que l'on sait cela, on peut facilement créer un compteur avec quelques bascules. On peut les créer avec des bascules T, D, JK, et bien d'autres. Nous allons d'abord voir ceux fabriqués avec des bascules T, plus simples, puis ceux fabriqués avec des bascules D.

Les incrémenteurs/décrémenteurs asynchrones à base de bascules T modifier

Pour rappel, les bascules T inversent leur contenu à chaque cycle d'horloge. Par simplicité, nous allons utiliser des bascules avec une sortie qui fournit l'inverse du bit stocké.

La première colonne inverse son contenu à chaque cycle, elle correspond donc à une bascule T simplifiée reliée directement à l'horloge. Les autres colonnes s'inversent quand survient un front descendant sur la colonne précédente. Le circuit qui correspond est illustré ci-dessous, avec des bascules T activées sur front descendant. Attention, cependant : la bascule la plus à gauche stocke le bit de poids FAIBLE, pas celui de poids fort. En fait, le nombre binaire est ici stocké de gauche à droite et non de droite à gauche ! Cela sera pareil dans tous les schémas qui suivront.

Compteur asynchrone de 3 bits, basé sur des bascules T simplifiées activées sur front descendant.

Il est aussi possible d'utiliser des bascules T actives sur front montant. En effet, notons qu'un front descendant sur la sortie Q correspond à un front montant sur la sortie /Q. En clair, il suffit de relier la sortie /Q d'une colonne sur l'entrée d'horloge de la suivante. Le circuit est donc le suivant :

Compteur asynchrone de 3 bits, basé sur des bascules T simplifiées activées sur front montant.

Un décrémenteur est strictement identique à un incrémenteur auquel on a inversé tous les bits. On peut donc réutiliser le compteur du dessus, à part que les sorties du compteur sont reliées aux sorties Q des bascules.

Décompteur asynchrone de 3 bits, basé sur des bascules T simplifiées activées sur front descendant.

Il est possible de fusionner un incrémenteur et un décrémenteur asynchrone, de manière à créer un circuit qui puisse soit incrémenter, soit décrémenter le compteur. Le choix de l'opération est réalisé par un bit d'entrée, qui vaut 1 pour une incrémentation et 0 pour une décrémentation. L'idée est que les entrées des bascules sont combinées avec ce bit, pour donner les entrées compatibles avec l'opération demandée.

AsyncCounter UpDown

L'incrémenteur asynchrone à base de bascules D modifier

Il est aussi possible d'utiliser des bascules D pour créer un compteur comme les deux précédents. En effet, une bascule T simplifiée est identique à une bascule D dont on boucle la sortie /Q sur l'entrée de données.

Compteur asynchrone, sans initialisation

Cette implémentation peut être modifiée pour facilement réinitialiser le compteur à une valeur non-nulle. Pour cela, il faut ajouter une entrée au compteur, sur laquelle on présente la valeur d’initialisation. Chaque bit de cette entrée est reliée à un multiplexeur, qui choisir quel bit mémoriser dans la bascule : celui fournit par la mise à jour du compteur, ou celui présenté sur l'entrée d'initialisation. On obtient le circuit décrit dans le schéma qui suit. Quand l'entrée Reset est activée, les multiplexeurs connectent les bascules aux bits sur l'entrée d'initialisation. Dans le cas contraire, le compteur fonctionne normalement, les multiplexeurs connectant l'entrée de chaque bascule à sa sortie.

Compteur asynchrone, avec initialisation.

L'incrémenteur/décrémenteur synchrone modifier

Passons maintenant à l'incrémenteur synchrone. Pour le fabriquer, on repart de la séquence des premiers entiers. Dans ce qui va suivre, nous allons créer un circuit qui compte de 1 en 1, sans utiliser d'additionneur. Pour comprendre comment créer un tel compteur, nous allons reprendre la séquence d'un compteur, déjà vue dans le premier extrait :

  • 000
  • 001
  • 010
  • 011
  • 100
  • 101
  • 110
  • 111

On peut remarquer quelque chose dans ce tableau : peu importe la colonne, un bit s'inversera au prochain cycle d’horloge quand tous les bits des colonnes précédentes valent 1. Et c'est vrai quelle que soit la taille du compteur ou sa valeur ! Ainsi, prenons le cas où le compteur vaut 110111 :

  • les deux premiers 1 sont respectivement précédés par la séquence 10111 et 0111 : vu qu'il y a un zéro dans ces séquences, ils ne s'inverseront pas au cycle suivant ;
  • le bit qui vaut zéro est précédé de la séquence de bit 111 : il s'inversera au cycle suivant ;
  • le troisième 1 en partant de la gauche est précédé de la séquence de bits 11 : il s'inversera aussi ;
  • même raisonnement pour le quatrième 1 en partant de la gauche ;
  • 1 le plus à droite correspond au bit de poids faible, qui s'inverse tous les cycles.

Pour résumer, un bit s'inverse (à la prochaine mise à jour) quand tous les bits des colonnes précédentes valent 1. Pour implanter cela en circuit, on a besoin d'ajouter un circuit qui détermine si les bits des colonnes précédentes sont à 1, qui n'est autre qu'un simple ET entre les bits en question. On pourrait croire que chaque bascule est précédée par une porte ET à plusieurs entrées qui fait un ET avec toutes les colonnes précédentes. Mais en réalité, il y a moyen d'utiliser des portes plus simples, avec une banale porte ET à deux entrées pour chaque bascule. Le résultat est indiqué ci-dessous.

Compteur synchrone à incrémenteur avec des bascules T.

L’implémentation de circuit avec des bascules D est légèrement plus complexe. Il faut ajouter un circuit qui prend en entrée le contenu de la bascule et un bit qui indique s'il faut inverser ou pas. En écrivant sa table de vérité, on s’aperçoit qu'il s'agit d'un simple XOR.

Compteur synchrone à incrémenteur avec des bascules D.

On peut appliquer la même logique pour un décrémenteur. Avec ce circuit, un bit s'inverse lorsque tous les bits précédents sont à zéro. En utilisant le même raisonnement que celui utilisé pour concevoir un incrémenteur, on obtient un circuit presque identique, si ce n'est que les sorties des bascules doivent être inversées avant d'être envoyée à la porte XOR qui suit.

La gestion des débordements d'entiers des incrémenteurs/décrémenteurs modifier

Pour rappel, tout compteur a une valeur maximale au-delà de laquelle il ne peut pas compter. Pour un compteur modulo N, le compteur compte de 0 à N-1. Pour les compteurs non-modulo, ils peuvent compter de 0 à , pour un compteur de n bits. Tout nombre en dehors de cet intervalle ne peut pas être représenté. Si le résultat d'un calcul sort de cet intervalle, il ne peut pas être représenté par l'ordinateur et il se produit ce qu'on appelle un débordement d'entier. Les débordements d'entiers surviennent quand on incrémente une valeur au-delà de la valeur maximale du compteur, ce qui est loin d'être rare.

La gestion des débordements peut se faire de deux manières différentes, mais celle qui est la plus utilisée sur les compteurs est tout simplement de réinitialiser le compteur quand on dépasse la valeur maximale. Pour les compteurs non-modulo, il n'y a pas besoin de faire quoique ce soit, car ils sont réinitialisés automatiquement. Prenez un compteur contenant la valeur maximale 111....1111, incrémentez-le, et vous obtiendrez 000...0000 automatiquement, sans rien faire. Par contre, pour les compteurs modulo, c'est une autre paire de manche. La réinitialisation ne se fait pas automatiquement, et on doit ajouter des circuits pour réinitialiser le compteur, et pour détecter quand le réinitialiser.

Les incrémenteurs/décrémenteurs précédents ne peuvent pas être réinitialisés. Pour que cela soit possible, il faut d'abord rajouter une entrée qui commande la réinitialisation du compteur : mise à 1 pour réinitialiser le compteur, à 0 sinon. Ensuite, il faut que les bascules du compteur aient une entrée de réinitialisation Reset, qui les force à se remettre à zéro. Il suffit alors de faire comme avec n'importe quel registre réinitialisable : connecter ensemble les entrées Reset des bascules et relier le tout à l'entrée de réinitialisation du compteur.

Compteur réinitialisable.

Maintenant que l'on a de quoi les réinitialiser, on ajoute un comparateur qui détecte quand la valeur maximale est atteinte, afin de commander l'entrée de réinitialisation. Prenons un compteur modulo 6, par exemple, ce qui veut dire qu'il compte de 0 à 5, et est remis immédiatement à zéro quand il atteint 6. Il compte donc comme suit : 0, 1, 2, 3, 4, 5, 0, 1, 2, ... Le circuit comparateur vérifie si la valeur maximale 6 est atteinte et qui met à 1 l'entrée Reset si c'est le cas. Un tel circuit est juste un comparateur avec une constante, que vous savez déjà fabriquer à cet endroit du cours. Un exemple est illustré ci-dessous.

Compteur modulo 10.

Il peut être utile de prévenir quand un compteur a débordé. Cela a des applications assez intéressantes, qu'on verra dans les chapitres suivants, notamment pour ce qui est des diviseurs de fréquence et des timers. Pour cela, on ajoute une sortie au compteur, qui est mise à 1 quand le compteur déborde. Pour les compteurs modulo, la sortie n'est autre que la sortie du comparateur. Et on peut généraliser pour n'importe quel N. Mais les choses sont plus compliquées pour les compteurs non-modulo, qui comptent de 0 au , pour lesquels on doit ajouter un circuit détecte quand un débordement d'entier a lieu. Pour cela, il y a plusieurs solutions. La plus simple est d'ajouter un comparateur qui vérifie si le compteur est à 0, signe qu'il vient d'être réinitialisé et a donc débordé. L'autre solution est d'ajouter une bascule à la toute fin de l'incrémenteur : cette bascule est mise à 1 en cas de débordement. Il suffit alors d'ajouter un circuit qui compare la valeur de cette bascule avec sa valeur du cycle précédent, et le tour est joué.

Les compteurs basés sur des registres à décalage modifier

Certains compteurs sont basés sur un registre à décalage. Pour simplifier les explications, nous allons les classer suivant le type de registre à décalage utilisé. Pour rappel, un registre à décalage dispose d'une entrée et d'une sortie. L'entrée peut être de deux types : soit une entrée série qui prend 1 bit, soit une entrée parallèle qui prend un nombre. Il en est de même pour la sortie, qui peut être série ou parallèle, à savoir qu'elle peut fournir en sortie soit un bit unique, soit un nombre complet. En combinant les deux, cela donne quatre possibilités qui portent les noms de registres à décalage PIPO, PISO, SIPO et SISO (P pour parallèle, S pour série, I pourInput, O pour Output).

Classification des registres à décalage
Entrée parallèle Entrée série
Sortie parallèle (registre simple) SIPO
Sortie série PISO SISO

Les registres PIPO sont en réalité des registres simples, pas des registres à décalage, donc on les omets dans de qui suit. Les compteurs déterministes peuvent se fabriquer avec les trois types restants de registres à décalage, ce qui donne trois types de compteurs déterministes. Malheureusement, ces types ne portent pas de noms proprement dit. À la rigueur, les compteurs déterministes basés sur un registre SISO sont appelés des compteurs en anneau, et encore cette dénomination est légèrement impropre.

À l'exception de certains compteurs one-hot qui seront vu juste après, ces compteurs sont des compteurs à rétroaction. Un terme bien barbare pour dire que l'on boucle leur sortie sur leur entrée, parfois en insérant un circuit combinatoire entre les deux. Les compteurs à rétroaction sont particuliers dans le sens où on n'a pas à changer leur valeur en cours de fonctionnement : on peut les réinitialiser, mais pas insérer une valeur dans le compteur. La raison est qu'ils sont utilisés pour une fonction bien précise : dérouler une suite de nombres bien précise, prédéterminée lors de la création du compteur. Par exemple, on peut créer un compteur qui sort la suite de nombres 4,7,6,1,0,3,2,5 en boucle, quel qu’en soit l'utilité. Cela peut servir pour fabriquer des suites de nombres pseudo-aléatoires, ce qui est de loin leur utilisation principale, comme on le verra dans un chapitre spécialement dédié aux circuits générateurs d'aléatoire. Mais certaines de leurs utilisations sont parfois surprenantes.

Un bon exemple de cela est leur utilisation dans l'Intel 8008, le tout premier microprocesseur 8 bits commercialisé, où ce genre de compteurs étaient utilisés pour le pointeur de pile, pour économiser quelques portes logiques. Ces compteurs implémentaient la séquence suivante : 000, 001, 010, 101, 011, 111, 110, 100. Pour les connaisseurs qui veulent en savoir plus, voici un article de blog sur le sujet : Analyzing the vintage 8008 processor from die photos: its unusual counters.

Les compteurs en anneau et de Johnson (SISO) modifier

Les compteurs basés sur des registres à décalage SISO sont aussi appelés des compteurs en anneau. Ils sont appelés ainsi car, généralement, on boucle la sortie du registre sur son entrée, ce qui veut dire que le bit sortant du registre est renvoyé sur son entrée. Pour les compteurs fabriqués avec un registre SISO (entrée et sortie de 1 bit), le bit entrant dans le registre à décalage est calculé à partir du bit sortant. Et il n'y a pas 36 façons de faire ce calcul : soit le bit sortant est laissé tel quel, soit il est inversé, pas d'autre possibilité. La première possibilité, où le bit entrant est égal au bit sortant, donne un compteur en anneau, vu plus haut. La seconde possibilité, où le bit sortant est inversé avant d'être envoyé sur l'entrée du registre à décalage, donne un compteur de Johnson. Les deux sont très différents, et ne fonctionnent pas du tout pareil.

Les compteurs en anneau, ou compteurs one-hot modifier

Les compteurs one-hot sont appelés ainsi, car ils permettent de compter dans une représentation des nombres qui n'est pas le binaire, qui porte justement le nom de représentation one-hot. Dans une telle représentation, un seul bit est à 1 pendant que les autres sont à 0. Cela laisse peu de valeurs possibles : pour N bits, on peut encoder seulement N valeurs. Les entiers sont codés de la manière suivante : le nombre N est encodé en mettant le énième bit à 1, avec la condition que l'on commence à compteur à partir de zéro. Dit autrement, le nombre encodé est égal au poids du bit à 1. Par exemple, si le bit de poids faible (celui de poids 0) est à 1, alors on code la valeur 0. Si le bit de poids numéro 1 est à 1, alors on code la valeur 1. Et ainsi de suite.

Décimal Binaire One-hot
0 000 00000001
1 001 00000010
2 010 00000100
3 011 00001000
4 100 00010000
5 101 00100000
6 110 01000000
7 111 10000000
Il est important de remarquer que dans cette représentation, le zéro est n'est PAS codé en mettant tous les bits à 0. Le zéro est codé en mettant le bit de poids faible à 1.

Un compteur en représentation one-hot contient un nombre codé de cette manière, qui est incrémenté ou décrémenté si besoin. Pour donner un exemple, la séquence d'un compteur en anneau de 4 bits est :

  • 0001 ;
  • 0010 ;
  • 0100;
  • 1000.

Incrémenter ou décrémenter le compteur demande alors de faire un simple décalage, ce qui fait que le compteur est un simple registre à décalage et non pas un compteur combiné à un incrémenteur/décrémenteur compliqué. L'économie en circuits d'incrémentation n'est pas négligeable. De plus, faire des comparaisons avec ce type de compteur est très simple : le compteur contient la valeur N si le énième bit est à 1. Pas besoin d'utiliser de circuit comparateur, juste de lire un bit. Mais les économies en termes de circuits (incrémenteur et comparateur) sont cependant contrebalancée par le fait que le compteur demande plus de bascules. Imaginons que l'on veut un compteur qui compte jusqu'à une valeur N arbitraire : un compteur en binaire normal utilisera environ bascules, alors qu'un compteur one-hot demande N bascules. Mais si N est assez petit, l'économie de bascules est assez faible, alors que l'économie de circuits logiques l'est beaucoup plus. De plus, il n'y a pas qu'une économie de circuit : le compteur est plus rapide.

Si vous ne mettez que des 0 dans un compteur en anneau, il restera bloqué pour toujours. En effet, décaler une suite de 0 donnera la même suite de 0.

En théorie, un simple registre à décalage peut faire office de compteur one-hot, mais son comportement est alors invalide en cas de débordement. Un débordement met la valeur du registre à décalage à zéro, ce qui n'est pas l'effet recherché. On peut en théorie rajouter des circuits combinatoires annexes pour gérer le débordement, mais il y a encore plus simple : boucler la sortie du registre sur son entrée. Le résultat donne donc un type particulier de compteurs one-hot, appelé les compteurs en anneau sont des registres à décalage dont le bit sortant est renvoyé sur l'entrée. En faisant cela, on garantit que le registre revient à zéro, zéro étant codé avec un 1 dans le bit de poids faible. En cas de débordement, le registre est mis à zéro par le débordement, mais le bit sortant vaut 1 et ce 1 sortant est envoyé sur l'entrée pour y être inséré dans le bit de poids faible.

Compteur en anneau de 4 bits

Il y a peu d'applications qui utilisent des compteurs en anneau. Ils étaient autrefois utilisés dans les tous premiers ordinateurs, notamment ceux qui géraient une représentation des nombres spécifique appelée la Bi-quinary coded decimal. Ils étaient aussi utilisés comme diviseurs de fréquence, comme on le verra dans le chapitre suivant. De nos jours, de tels compteurs sont utilisés dans les séquenceurs de processeurs, mais aussi dans les séquenceurs de certains périphériques, ou dans les circuits séquentiels simples qui se résument à des machines à états. Ils sont alors utilisés car très rapides, car ils n'ont pas besoin de circuit comparateur pour connaitre la valeur stockée dedans, et qu'ils sont parfaitement adaptés au stockage de petites valeurs. Nous n'allons pas rentrer dans le détail de leurs utilisations et n'en reparlerons pas dans la suite du cours, ce qui fait que nous parlons de ces compteurs ici et pas dans le chapitre sur les compteurs.

Les compteurs de Johnson modifier

Sur les compteurs de Johnson, le bit sortant est inversé avant d'être bouclé sur l'entrée.

Compteur de Johnson de 4 bits

La séquence d'un compteur de Johnson de 4 bits est :

  • 1000 ;
  • 1100 ;
  • 1110 ;
  • 1111 ;
  • 0111 ;
  • 0011 ;
  • 0001 ;
  • 0000.

Vous remarquerez peut-être que lorsque l'on passe d'une valeur à la suivante, seul un bit change d'état. Sachant cela, vous ferez peut-être le parallèle avec le code Gray vu dans les tout premiers chapitres, mais cela n'a rien à voir. Les valeurs d'un compteur Johnson ne suivent pas un code Gray classique, ni même une variante de celui-ci. Les compteurs qui comptent en code Gray sont foncièrement différents des compteurs Johnson.

Une application des compteurs de Johnson, assez surprenante, est la fabrication d'un signal sinusoïdal. En combinant un compteur de Johnson, quelques résistances, et des circuits annexes, on peut facilement fabriquer un circuit qui émet un signal presque sinusoïdal (avec un effet d'escalier pas négligeable, mais bref). Les oscillateurs sinusoïdaux numériques les plus simples qui soient sont conçus ainsi. Quant aux compteurs en anneau, ils sont utilisés en lieu et place des compteurs normaux dans des circuits qui portent le nom de séquenceurs ou de machines à états, afin d'économiser quelques circuits. Mais nous en reparlerons dans le chapitre sur l'unité du contrôle du processeur.

Les registres à décalage à rétroaction de type SIPO/PISO modifier

D'autres compteurs sont fabriqués en prenant un registre à décalage SIPO ou PISO dont on boucle l'entrée sur la sortie. Pour être plus précis, il y a très souvent un circuit combinatoire qui s'intercale entre la sortie et l'entrée. Son rôle est de calculer ce qu'il faut mettre sur l'entrée, en fonction de la sortie.

Les registres à décalage à rétroaction linéaire modifier

Étudions en premier lieu le cas des registres à décalage à rétroaction linéaire. Le terme anglais pour de tels registres est Linear Feedback Shift Register, ce qui s’abrège en LFSR. Nous utiliserons cette abréviation dans ce qui suit pour simplifier grandement l'écriture. Les LFSR sont appelés ainsi pour plusieurs raisons. Déjà, registre à décalage implique qu'ils sont fabriqués avec un registre à décalage, et plus précisément des registres à décalage SIPO. A rétroaction indique que l'on boucle la sortie sur l'entrée. Linéaire indique que l'entrée du registre à décalage s'obtient par une combinaison linéaire de la sortie. Le terme combinaison linéaire signifie que l'on multiplie les bits de l'entrée par 0 ou 1, avant d'additionner le résultat. Vu que nous sommes en binaire, les constantes en question valent 0 ou 1. Voici un exemple de formule qui colle avec ce cahier des charges :

Une première simplification est possible : supprimer les multiplications par 0. Ce faisant, les bits associés ne sont tout simplement pas pris en compte dans le calcul d'addition. Tout se passe comme suit l'on ne tenait compte que de certains bits du LFSR, pas des autres. On a alors des opérations du genre :

Dans ce calcul, on ne garde qu'un seul bit du résultat, vu que l'entrée du registre à décalage ne fait qu'un bit. Par simplicité, on ne garde que le bit de poids faible. Or, il s'avère que cela simplifie grandement les calculs, car cela nous dispense de gérer les retenues. Et nous verrons dans quelques chapitres qu'additionner deux bits en binaire, sans tenir compte des retenues, revient à faire une simple opération XOR. On peut donc remplacer les additions par des XOR.

Le résultat est ce que l'on appelle un LFSR de Fibonacci, ou encore un LFSR classique, qui celui qui colle le mieux avec la définition.

Registre à décalage à rétroaction de Fibonnaci.

Les registres à décalage à rétroaction de Gallois sont un peu l'inverse des LFSR vus juste avant. Au lieu d'utiliser un registre à décalage SIPO, on utilise un registre à décalage PISO. Pour faire la différence, nous appellerons ces derniers les LFSR PISO, et les premiers LFSR SIPO. Avec les LFSR PISO, on prend le bit sortant et on en déduit plusieurs bits à partir d'un circuit combinatoire, qui sont chacun insérés dans le registre à décalage à un endroit bien précis. Bien sûr, la fonction qui calcule des différents bits à partir du bit d'entrée conserve les mêmes propriétés que celle utilisée pour les LFSR : elle se calcule avec uniquement des portes XOR. Leur avantage est qu'ils sont plus rapides, sans avoir les inconvénients des autres LFSR. Ils sont plus rapides car il n'y a qu'une seule porte logique entre la sortie et une entrée du registre à décalage, contre potentiellement plusieurs avec les LFSR SIPO.

Registre à décalage à rétroaction de Galois.

Les variantes des registres à décalage à rétroaction linéaire modifier

Il existe une variante des LFSR, qui modifie légèrement son fonctionnement. Il s'agit des registres à décalages à rétroaction affine.

Pour les LFSR SIPO, la fonction qui calcule le bit de résultat n'est pas linéaire, mais se calcule par une formule comme la suivante. Notez le +1 à la fin de la formule : c'est la seule différence.

Le résultat obtenu est l'inverse de celui obtenu avec le LFSR précédent. Un tel circuit est donc composé de portes NXOR, comparé à son comparse linéaire, composé à partir de portes XOR. Petite remarque : si je prends un registre à rétroaction linéaire et un registre à rétroaction affine avec les mêmes coefficients sur les mêmes bits, le résultat du premier sera égal à l'inverse de l'autre. Notons que tout comme les LFSR qui ne peuvent pas mémoriser un 0, de tels registres à décalage à rétroaction ne peuvent pas avoir la valeur maximale stockable dans le registre. Cette valeur gèle le registre à cette valeur, dans le sens où le résultat au cycle suivant sera identique. Mais cela ne pose pas de problèmes pour l'initialisation du compteur.

Pour les LFSR PISO, il existe aussi une variante affine, où les portes XOR sont remplacées par des portes NXOR.

Il existe enfin des compteurs de ce type qui ne sont pas des LFSR, même en incluant les compteurs de Gallois et autres. Ce sont des compteurs basés sur des registres à décalage où le circuit combinatoire inséré entre l'entrée et la sortie n'est pas basé sur des portes XOR ou NXOR. Ils sont cependant plus compliqués à concevoir, mais ils ont beaucoup d'avantages.

La période d'un compteur à rétroaction modifier

Un compteur à rétroaction est déterministe : pour le même résultat en entrée, il donnera toujours le même résultat en sortie. De plus, ce registre ne peut contenir qu'un nombre fini de valeurs, ce qui fait qu'il finira donc par repasser par une valeur qu'il aura déjà parcourue. Une fois qu'il repassera par cette valeur, son fonctionnement se reproduira à l'identique comparé à son passage antérieur. Lors de son fonctionnement, le compteur finira par repasser par une valeur parcourue auparavant et il bouclera. Il parcourt un nombre N de valeurs à chaque cycle, ce nombre étant appelé la période du compteur.

Le cas le plus simple est celui des compteurs en anneau, suivi par les compteurs Johnson. Les deux compteurs ont des périodes très différentes. Un compteur en anneau de N bits peut prendre N valeurs différentes, qui ont toutes un seul bit à 1. À l'opposé, un compteur Johnson peut prendre deux fois plus de valeurs. Pour nous en rendre compte, comparons la séquence de nombre déroulé par chaque compteur. Pour 5 bits, les séquences sont illustrées ci-dessous, dans les deux animations.

Compteur en anneau de 5 bits.
Compteur de Johnson de 5 bits.

La période des registres à décalage à rétroaction linéaire dépend fortement de la fonction utilisée pour calculer le bit de sortie, des bits choisis, etc. Dans le meilleur des cas, le registre à décalage à rétroaction passera par presque toutes les valeurs que le registre peut prendre. Si je dis presque toutes, c'est simplement qu'une valeur n'est pas possible : suivant le registre, le zéro ou sa valeur maximale sont interdits. Si un registre à rétroaction linéaire passe par zéro, il y reste bloqué définitivement. La raison à cela est simple : un XOR sur des zéros donnera toujours 0. Le même raisonnement peut être tenu pour les registres à rétroaction affine, sauf que cette fois-ci, c'est la valeur maximale stockable dans le registre qui est fautive. Tout le chalenge consiste donc à trouver quels sont les registres à rétroaction dont la période est maximale : ceux dont la période vaut . Qu'on se rassure, quelle que soit la longueur du registre, il en existe au moins un : cela se prouve mathématiquement, même si nous ne vous donnerons pas la démonstration.


L'initialisation d'un compteur à rétroaction modifier

Sur la quasi-totalité des compteurs et registres vu dans ce chapitre et les précédents, le compteur peut être initialisé à une valeur arbitraire. De plus, les débordements d'entiers sont possibles. Mais sur une bonne partie des compteurs à rétroaction, rien de tout cela n'est possible. Rappelons que les compteurs à rétroaction déroulent une suite de nombres bien précise, déterminée lors de la création du compteur. Donc, ça ne servirait à rien de charger une valeur arbitraire dans ces compteurs, du fait de leur fonctionnement. De plus, leur fonctionnement est périodique, ce qui fait que de tels compteurs ne peuvent pas déborder. En conséquence, un compteur à rétroaction ne peut pas être initialisé à une valeur arbitraire, mais seulement réinitialisé à une valeur de base qui est toujours la même.

Et ce qui est de l'initialisation, tous les compteurs basés sur un registre à décalage ne sont pas égaux. Pour résumer, deux cas sont possibles : soit le compteur peut être initialisé avec zéro sans que cela pose problème, soit ce n'est pas le cas. Les deux cas donnent des résultats différents. Autant le premier cas fait que l'on fait comme avec tous les autres registres, autant le second cas est inédit et demande des solutions particulières, qui sont les mêmes que le compteur oit en anneau, un LFSR, ou autre.

Le premier cas est celui où le compteur peut être initialisé avec zéro sans que cela ne pose problème. C'est le cas sur les compteurs de Johnson, mais aussi sur les registres à décalage à rétroaction non-linéaire. Sur de tels compteurs, la réinitialisation se fait comme pour n'importe quel registre/compteur. A savoir que les entrées de reset des bascules sont toutes connectées ensemble, au même signal de reset.

Compteur de Johnson de 4 bits

Dans le second cas, on ne peut pas l'initialiser avec uniquement des 0, comme les autres registres, et la méthode précédente ne fonctionne pas. C'est le cas sur les compteurs en anneau, et sur les LFSR. Sur les compteurs en anneau, lors de la réinitialisation, il faut faire en sorte que toutes les bascules soient réinitialisées, sauf une qui est mise à 1. Pour cela, il faut utiliser des bascules, avec une entrée pour les reset et une autre pour les mettre à 1. Le signal de reset est envoyée normalement sur toutes les bascules, sauf pour la première. La première bascule est configurée de manière à ce que le signal de reset la mette à 1, en envoyant le signal de reset directement sur l'entrée S (Set) qui met la bascule à 1 quand elle est activée. Cela garantit que le registre est réinitialisé avec un zéro codé en one-hot.

Compteur en anneau de 4 bits

Une autre solution est de mettre un multiplexeur juste avant l'entrée du registre à décalage. Cette solution marché bien dans le sens où elle permet d'initialiser le registre avec une valeur arbitraire, qui est insérée dans le registre en plusieurs cycles. Elle fonctionne sur les registres en anneau, mais aussi sur les LFSR. Pour les LFSR, le multiplexeur est connecté soit au bit calculé par les portes XOR, soit par une entrée servant uniquement de l'initialisation.

Initialisation d'un LFSR


Les compteurs servent à créer divers circuits fortement liés la gestion de la fréquence, ainsi qu'à la mesure du temps. L'idée derrière ces circuits est tout simplement de compter les cycles d'horloge. Vu qu'un compteur/décompteur est cadencé par le signal d'horloge, on peut l'incrémenter ou le décrémenter à chaque cycle d'horloge, ce qui lui fait compter les cycles d'horloge. Compter les cycles d'horloge a plusieurs utilités. On peut s'en servir pour mesurer des durées, ou pour diviser une fréquence. Dans ce qui va suivre, nous allosn voire deux types de circuits : les diviseurs de fréquence, et les timers.

Les diviseurs de fréquence modifier

Les diviseurs de fréquence sont des circuits qui prennent en entrée un signal d'horloge et fournissent en sortie un autre signal d'horloge de fréquence plus faible. Plus précisément, la fréquence de sortie est 2, 3, 4, ou 18 fois plus faible que la fréquence d'entrée. La fréquence est donc divisée par un nombre N, qui dépend du diviseur de fréquence. Il existe des diviseurs de fréquence qui divisent la fréquence par 2, d'autres par 4, d'autres par 13, etc.

Leur implémentation est simple : il suffit d'un compteur auquel on rajoute une sortie. Pour être plus précis, il faut utiliser un compteur modulo. Pour rappel, le compteur modulo est un compteur qui est remis à zéro quand il atteint une valeur limite. Pour un diviseur de fréquence par N, il faut plus précisément un compteur modulo par N. Tous les N cycles, le compteur déborde, à savoir qu'il dépasse sa valeur maximale et est remis à zéro. Une sortie du compteur indique si le compteur déborde : elle est mise à 1 lors d'un débordement et reste à 0 sinon. L'idée est de compter le nombre de cycles d'horloges, et de mettre à 1 la sortie quand le compteur déborde.

Par exemple, pour diviser une fréquence par 8, on prend un compteur 3 bits. A chaque fois que le compteur déborde et est réinitialisé, on envoie un 1 en sortie. Le résultat est un signal qui est à 1 tous les 8 cycles d'horloge, à savoir un signal de fréquence 8 fois inférieure. La même idée marche avec un diviseur de fréquence par 6, sauf que l'on doit alors utiliser un compteur modulo par 6, ce qui veut dire qu'il compte de 0 à 5 comme suit : 0, 1, 2, 3, 4, 5, 0, 1, 2, ... Le compteur déborde tous les 6 cycles d’horloge, ce qui fait que sa sortie de débordement est à 1 tous les 6 cycles, ce qui est demandé.

Si n'importe quel compteur fait l'affaire, il est cependant utile d'utiliser les compteurs les plus adaptés à la tâche. Pour faire un diviseur de fréquence, on utilise rarement un compteur complet, mais souvent des compteurs plus simples, comme un circuit incrémenteur ou des compteurs en anneau.

Les diviseurs de fréquence basés sur des compteurs en anneau modifier

Il est rare que l'on doive diviser une fréquence par 50 ou par 100, par exemple. Un diviseur de fréquence divise une fréquence par N, avec N très petit. Or, les compteurs one-hot, aussi appelés compteurs en anneau, sont particulièrement adaptés pour compter jusqu'à des valeurs assez faibles : 6, 10, 12, 25, etc. Il est donc naturel d'utiliser un compteur en anneau dans un diviseur de fréquence. Pour diviser une fréquence par N, il suffit de prendre un compteur en anneau qui compte de 0 à N-1 (inclut). Le signal de sortie est mis à 1 et/ou inversé quand le compteur est remis à zéro, c'est à dire quand son bit de poids faible est à 1. Une méthode alternative consiste à regarder au contraire le bit de poids fort : le compteur atteint N quand ce bit est à 1, ce qui fait qu'il a compté jusqu'à N. En clair, la sortie est obtenue en regardant la valeur du bit de poids faible/fort, sans même utiliser de comparateur. Pas besoin de comparateur, donc. Et au-delà de ça, le circuit obtenu est beaucoup plus simple qu’avec un compteur normal. Et c'est la raison pour laquelle les diviseurs de fréquence sont souvent conçus en utilisant des compteurs one-hot. Plus on divise une fréquence par un N très petit, plus les compteurs auront d'avantages : très simples, demandent peu de transistors/circuits, sont très rapides, prennent peu de place, permettent de se passer de circuit comparateur.

Les diviseurs de fréquence basés sur des incrémenteurs à bascule T modifier

Il est aussi possible de concevoir des diviseurs de fréquence en utilisant un banal incrémenteur. Si l'incrémenteur est un incrémenteur non-modulo, on se retrouve avec un diviseur de fréquence qui divise la fréquence d'entrée par une puissance de deux. Pour comprendre comment les fabriquer, nous allons étudier le cas le plus simple : celui qui divise par 2 la fréquence d'entrée. Et bien sachez qu'il s'agit d'une simple bascule T. En effet, regardons ce qui se passe quand on envoie un signal constamment à 1 sur son entrée T. Dans ce cas, la bascule s'inversera une fois par chaque cycle d'horloge. Un cycle d'horloge sur la sortie correspond au temps passé entre deux inversions.

Diviseur de fréquence par 2.

Pour créer un diviseur de fréquence par 4, il suffit d'enchainer deux fois le circuit précédent. La sortie de la première bascule T doit être envoyée sur l'entrée T de la seconde bascule. Et pour créer un diviseur de fréquence par 8, il suffit d'enchainer trois fois le circuit précédent. Et ainsi de suite. Au final, un diviseur de fréquence qui divise la fréquence d'entrée par 2^N est un enchainement de N bascules T, qui n'est autre qu'un circuit incrémenteur. La sortie d'un tel diviseur de fréquence se situe en sortie de la dernière bascule.

Diviseur de fréquence par 8.

On peut en profiter pour créer un circuit à plusieurs sorties, en mettant une sortie par bascule. Le circuit, illustré ci-dessous, fournit donc plusieurs fréquences de sortie : une à la moitié de la fréquence initiale, une autre au quart de la fréquence d'entrée, une autre au huitième, etc.

Diviseur de fréquence multiple.

Les timers modifier

Les timers, aussi appelés Programmable interval timer, sont des circuits capables de compter des durées. Leur fonctionnement est assez simple : on leur envoie un certain nombre de cycles d'horloge en entrée, et ils émettent un signal quand ce nombre de cycles est écoulé. Le signal en question est disponible sur une sortie de 1 bit, et correspond tout simplement au fait que cette sortie est mise à 1, pendant un cycle d'horloge. Ils permettent de compter des durées, exprimées en cycles d'horloge. On peut aussi générer un signal qui surviendra après 50 cycles d'horloge, ou après 100 cycles d'horloge, etc.

Les timers sont composés d'un compteur/décompteur cadencé par un signal d'horloge. Le compteur initialisé à 0, puis est incrémenté à chaque signal d'horloge, jusqu’à atteinte d'une valeur limite où il génère un signal. Pour un décompteur, c'est la même chose, sauf que le décompteur est initialisé à sa valeur limite et est décrémenté à chaque cycle, et envoie un signal quand il atteint 0. Les timers basés sur des décompteurs sont nettement plus simples que les autres, ce qui fait qu'ils sont plus utilisés. Pour que les timers soient configurables, on doit pouvoir préciser combien de cycles il faut (dé-)compter avant d'émettre un signal. On peut ainsi préciser s'il faut émettre le signal après 32 cycles d'horloge, après les 50 cycles, tous les 129 cycles, etc. Pour cela, il suffit de préciser le nombre de cycles à compter/décompter en entrée et d'initialiser le compteur/décompteur avec.

Les timers matériels peuvent compter de deux manières différentes, appelées mode une fois et mode périodique. Concrètement, le mode périodique divise la fréquence d'entrée, alors que le mode une fois compte durant une durée fixe avant de s'arrêter.

  • En mode une fois, le timer s'arrête une fois qu'il a atteint la limite configurée. On doit le réinitialiser manuellement, par l'intermédiaire du logiciel, pour l'utiliser une nouvelle fois. Cela permet de compter une certaine durée, exprimée en nombre de cycles d'horloge.
  • En mode périodique, le timer se réinitialise automatiquement avec la valeur de départ, ce qui fait qu'il reboucle à l'infini. En clair, le timer se comporte comme un diviseur de fréquence. Si le compteur est réglé de manière à émettre un signal tous les 9 cycles d'horloge, la fréquence de sortie sera de 9 fois moins celle de la fréquence d'entrée du compteur.

Un ordinateur est rempli de timers divers. Dans ce qui va suivre, nous allons voir les principaux timers, qui sont actuellement intégrés dans les PC modernes. Ils se trouvent sur la carte mère ou dans le processeur, tout dépend du timer.

Le watchdog timer modifier

Le watchdog timer est un timer spécifique dont le but est d'éteindre ou de redémarrer automatiquement l'ordinateur si jamais celui-ci ne répond plus ou plante. Tous les ordinateurs n'ont pas ce genre de timer, et beaucoup de PC s'en passent. Mais ce timer est très fréquent dans les architectures embarquées. Le watchdog timer est un compteur:décompteur) qui doit être réinitialisé régulièrement. S'il n'est pas réinitialisé, le watchdog timer déborde (revient à 0 ou atteint 0) et envoie un signal qui redémarre le système. le système est conçu pour réinitialiser le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer. Si jamais le système dysfonctionne gravement, le système ne pourra pas réinitialiser le watchdog timer et le système est redémarré automatiquement ou mis en arrêt.

Le Watchdog Timer et l'ordinateur.

Le Time Stamp Counter des processeurs x86 modifier

Tous les processeurs des PC actuels sont des processeurs dits x86. Nous ne pouvons pas expliquer ce que cela signifie pour le moment, retenez juste ce terme. Sachez que tous les processeurs x86 contiennent un compteur de 64 bits, appelé le Time Stamp Counter, qui mémorise le nombre de cycles d'horloge qu'a effectué le processeur depuis son démarrage. Les programmes peuvent accéder à ce registre assez facilement, ce qui est utile pour faire des mesures ou comparer les performances de deux applications. Il permet de compter combien de cycles d'horloge met un morceau de code à s’exécuter, combien de cycles prend une instruction à s’exécuter, etc. Les processeurs non-x86 ont un registre équivalent, que ce soit les processeurs ARM ou d'autres.

Malheureusement, ce compteur est tombé en désuétude pour tout un tas de raisons. La principale est que les processeurs actuels ont une fréquence variable. Nous expliquerons cela plus en détail dans quelques chapitres, mais les processeurs actuels font varier leur fréquence suivant les besoins. Ils augmentent leur fréquence quand on leur demande de faire beaucoup de calculs, et se mettent en mode basse(fréquence pour économiser de l'énergie si on ne leur demande pas grand chose. Avec une fréquence variable, le Time Stamp Counter perd complétement en fiabilité. Intel a tenté de corriger ce défaut en incrémentant ce registre à une fréquence constante, différente de celle du processeur, ce qui est encore le cas sur les processeurs Intel actuels. Le comportement est un peu différent sur les processeurs AMD, mais il compte par cycle d'horloge, avec des mécanismes de synchronisation assez complexes pour corriger l'effet de la fréquence variable.

L'horloge temps réel modifier

L'horloge temps réel est un timer qui génère une fréquence de 1024 Hz, soit près d'un Kilohertz. Dans ce qui suit, nous la noterons RTC, ce qui est l'acronyme du terme anglais Real Time Clock. La RTC prend en entrée un signal d'horloge de 32KHz, généré par un oscillateur à Quartz, et fournit en sortie un signal de fréquence 32 fois plus faible, c'est à dire de 1 KHz. Pour cela, elle est réglée en mode répétitif et son décompteur interne est initialisé à 32. La RTC génère donc un signal toutes les millisecondes, qui est envoyé au processeur. On peut, en théorie, changer la fréquence de la RTC, mais c'est rarement une bonne idée.

En théorie, la RTC permet de compter des durées assez courtes, comme le ping (le temps de latence d'un réseau, pour simplifier), le temps de rafraichissement de l'écran, ou bien d'autres choses. Mais dans les faits, les systèmes d'exploitation modernes ne l'utilisent pas pour ça. L'horloge temps réel est trop imprécise et sa fréquence n'aide pas. En effet, 1024 Hz est proche de 1000, mais pas assez pour faire des mesures à la missliseconde près, chose qui est nécessaire pour mesurer le ping ou d'autres choses utiles.

A la place, l'ordinateur l'utiliser pour compter les secondes, afin que l'ordinateur soit toujours à l'heure. Vous savez déjà que l'ordinateur sait quelle heure il est (vous pouvez regarder le bureau de Windows dans le coin inférieur droite de votre écran pour vous en convaincre) et il peut le faire avec une précision de l'ordre de la seconde. Mais pour savoir quel jour, heure, minute et seconde il est, l'ordinateur doit faire deux choses : mémoriser la date exacte à la seconde près, et avoir la capacité de compter le temps qui s'écoule, seconde par seconde. Pour cela, un ordinateur contient une CMOS RAM qui mémorise la date, et la RTC.

Le Programmable Interval Timer : l'Intel 8253 modifier

Intel 8253 and 8254

L'Intel 8253 est un timer programmable qui était autrefois intégré dans les cartes mères des ordinateurs personnels de type PC. Les premiers processeurs x86 étaient souvent secondés avec un Intel 8253 soudé à la carte mère. Il fût suivi par l'Intel 8254, qui en était une légère amélioration. S'il n'est plus présent dans un boitier de la carte mère, on trouve toujours un circuit semblable au 8253 à l'intérieur du chipset de la carte mère, voire à l'intérieur du processeur, pour des raisons de compatibilité. Sur les PC, il est cadencé par une horloge maitre, générée par un oscillateur à Quartz, dont la fréquence est de 32 768 Hertz, soit 2^15 cycles d'horloge par seconde. La fréquence générée par un compteur va donc de 18,2 Hz à environ 500 KHz. Il était utilisé pour dériver un grand nombre de fréquences utilisées dans l'ordinateur. Par exemple, le second compteur était utilisé par défaut pour le rafraichissement de la mémoire (D)RAM, mais il était souvent reprogrammé pour servir à générer des fréquences spécifiques par le BIOS ou la carte graphique.

L'intérieur de l'Intel 8253 est illustré ci-dessous. Nous allons expliquer l'ensemble de ce schéma, rassurez-vous, mais les explications seront plus simples à comprendre si vous survolez ce schéma en premier lieu.

Intel 8253, intérieur.

L'Intel 8253 contient trois compteurs de 16 bits, numérotés de 0 à 2. Chaque compteur possède deux entrées et une sortie : l'entrée CLOCK est celle de l'horloge de 32 MHz, l'entrée GATE active ou désactive le compteur, la sortie fournit le signal voulu et/ou la fréquence de sortie.

L'Intel 8253 lui-même possède plusieurs entrées et sorties. En premier lieu, on voit un port de 8 bits connecté aux trois compteurs, qui permet à l'Intel 8253 de communiquer avec le reste de l'ordinateur. La communication se fait dans les deux sens : soit de l'ordinateur vers les compteurs, soit des compteurs vers l'ordinateur. Dans le sens ordinateur -> compteurs, cela permet à l'ordinateur de programmer les compteurs, de les initialiser. Dans l'autre sens, cela permet de récupérer le contenu des compteurs, même si ce n'est pas très utilisé.

Ensuite, on trouve un registre de 8 bits, le Control Word register qui mémorise la configuration de l'Intel 8253. Le contenu de ce registre détermine le mode de fonctionnement du compteur, de combien doit compter le compteur et bien d'autres choses. Pour programmer les trois compteurs, il faut écrire un mot de 8 bits dans le Control Word register. La configuration de l'Intel 8253 fournie en sur le port de 8 bits pendant un cycle d'horloge, puis est mémorisée dans ce registre et reste pour les cycles suivants.

Mais l'écriture a lieu à condition que les 5 entrées de configuration soit bien réglées. Les 5 entrées de configuration sont les suivantes :

  • Deux bits A0 et A1 pour sélectionner le compteur voulu avec son numéro, ou le control word register.
  • Un bit RD à mettre à 0 pour que l'ordinateur récupère le compteur sélectionné ou le control word register sur le port de 8 bits.
  • Un bit WR à mettre à 0 pour que l'ordinateur modifie le compteur sélectionné ou le control word register, en envoyant le nombre pour l'initialisation sur le port de 8 bits.
  • Un bit CS qui active ou désactive l'Intel 8253 et permet de l'allumer ou de l’éteindre.

Pour écrire dans le Control Word register, il faut mettre le bit CS à 0 (on active l'Intel 8253), mettre à 1 le bit RD et à 0 le bit WR (on indique qu'on fait une écriture), et sélectionner le Control Word register en mettant les deux bits A0 et A1 à 1. Pour écrire dans un compteur, il faut faire la même chose, sauf que les bits A0 et A1 doivent être configurés de manière à donner le numéro du compteur voulu. LA lecture s'effectue elle aussi de la même manière, mais il faut inverser les bits RD et WR.

Le High Precision Event Timer (HPET) modifier

De nos jours, l'horloge temps réel et l'Intel 8253/8254 tendent à être remplacé par un autre timer, le High Precision Event Timer (HPET). Il s'agit d'un compteur de 64 bits, dont la fréquence est d'au moins 10 MHz. Il s'agit bien d'un compteur et non d'un décompteur. Il est couplé à plusieurs comparateurs, qui vérifient chacun une valeur limite, une valeur à laquelle générer un signal. La valeur limite peut être programmée, ce qui fait que chaque comparateur est associé à un registre pour mémoriser la valeur limite. Il doit y avoir au moins trois comparateurs, mais le nombre peut monter jusqu’à 256. Chaque comparateur doit pouvoir fonctionner en mode une fois, et au moins un comparateur doit pouvoir fonctionner en mode périodique.

High Precision Event Timer

Il faut noter que les systèmes d'exploitation conçus avant le HPET ne peuvent pas l'utiliser, pour des raisons techniques de compatibilité matérielle. C'est le cas de Windows XP avant le Service Pack 3. C'est la raison pour laquelle les cartes mères possèdent encore un PIT et une RTC, ou au moins qu'elles émulent RTC et PIT dans leurs circuits. D'ailleurs, pour économiser des circuits, les cartes mères modernes émulent le PIT et la RTC avec le HPET. Le HPET est configuré de manière à ce que le premier comparateur fournisse une fréquence de 1024 Hz, comme la RTC, et les 3 comparateurs suivants remplacent l'Intel 8253.

Le HPET gère de nombreux modes de fonctionnement : ses comparateurs peuvent être configuré en mode une fois ou périodique, on peut lui demander d'émuler la RTC et le PIT, etc. Aussi, il contient aussi de nombreux registres de configuration. En tout, on trouve 3 registres de configuration. à Cela, il faut ajouter trois registres pour configurer chaque comparateur indépendamment les uns des autres. Notons qu'il est aussi possible de lire ou écrire dans le compteur de 64 bits, mais ce n'est pas recommandé.


Les circuits de calcul et de comparaison modifier

Dans ce chapitre, nous allons voir les décalages et les rotations. Nous allons voir ce que sont ces opérations, avant de voir les circuits associés. Précisons que dans les ordinateurs modernes, décalages et rotations sont prises en charge par un circuit, le barrel shifter, qui est capable d'effectuer aussi bien des rotations que des décalages. Il en existe de nombreux types, mais nous allons voir les barrel shifters basés sur des multiplexeurs. Mais expliquons d'abord les différentes opérations de décalage et de rotation.

Les opérations de décalage modifier

Les décalages décalent un nombre de un ou plusieurs rangs vers la gauche, ou la droite. Le nombre à décaler est envoyé sur une entrée du circuit, de même que le nombre de rangs l'est sur une autre. Le circuit fournit le nombre décalé sur sa sortie. Il existe plusieurs opérations de décalage différentes et on peut les classer en plusieurs types. Dans les grandes lignes, on distingue les rotations, les décalages logiques et les décalages arithmétiques. Elles se distinguent sur plusieurs points, les principaux étant les suivants :

  • ce qu'on fait des bits qui sortent du nombre lors du décalage ;
  • comment on remplit les vides qui apparaissent lors du décalage ;
  • la manière dont est géré le signe du nombre décalé.
Décalages, gestion des bits entrants et sortants

Pour comprendre les deux premiers points, prenons l'exemple d'un nombre de 8 bits, comme ci-contre. L'exemple montre le décalage de 01011101 de deux rangs. On obtient 010111 : les deux bits de poids forts sont vides et les deux bits de fin (01) sortent du nombre. Et cela vaut pour tout décalage : d'un côté le décalage fait sortir des bits du nombre, de l'autre certains bits sont inconnus ce qui laisse des vides dans le nombre. Le nombre de bits sortants et de vides est strictement égal au nombre de rangs de décalage : si on décale de n rangs, alors cela laissera n vides et fera sortir n bits. Pour un décalage de n rangs, les vides sont dans les n bits de poids fort pour un décalage à droite et dans les n bits de poids faibles pour un décalage à gauche. Et les n bits sortant sont à l'opposé : bits de poids faible pour un décalage à droite et bits de poids fort pour un décalage à gauche. Ces deux points, la gestion des vides et des bits sortants, sont assez liés.

Le différents types de décalages modifier

En premier lieu, parlons de ce qu'on fait des bits qui sortent du nombre lors du décalage. Que fait-on de ces bits ?

La première solution est de les faire rentrer de l'autre côté, de les remettre au début du nombre décalé. L'opération en question est alors appelée une rotation. Il existe des rotations à droite et à gauche.

MSB : bit de poids fort

(Most Significant Bit)


LSB : bit de poids faible

(Least Significant Bit)

Rotation à gauche.
Rotation à droite.

L'autre solution est d'oublier les bits sortants. L’opération est alors appelée un décalage, qui peut être soit un décalage logique, soit un décalage arithmétique. Le fait que l'on oublie les bits sortants fait que les vides ne sont pas remplis et qu'il faut trouver de quoi les combler. Et c'est là qu'on peut faire la distinction entre décalages logiques et arithmétiques.

Avec un décalage logique, les vides sont remplis par des zéros, aussi bien pour un décalage à gauche et un décalage à droite.

Décalage logique à gauche.
Décalage logique à droite.
Décalage arithmétique à droite.

Avec un décalage arithmétique, la situation est différente pour un décalage à gauche et à droite. Le principe des décalages arithmétique est qu'ils conservent le bit de signe du nombre décalé (qui est supposé être signé), contrairement aux autres décalages. La situation est cependant quelque peu compliquée et tout dépend de l'implémentation exacte du décalage, tous les ordinateurs ne faisant pas la même chose.

Il n'y a pas d’ambigüité pour les décalages à droite, qui sont tous réalisés de la même manière sur toutes les architectures. Pour un décalage à droite, les vides dans les vides de poids forts sont remplis par le bit de signe. Ce remplissage est une sorte d'extension de signe, ce qui fait que la conservation du signe est automatique.

Décalage arithmétique à gauche qui ne conserve pas le bit de signe.

Pour un décalage à gauche, les choses sont plus compliquées. Les bits de poids faible vides sont remplis par des zéros, comme pour un décalage logique. Mais pour ce qui est de la conservation du bit de signe, c'est plus compliqué. On a deux écoles : la première ne conserve pas le bit de signe, la seconde le fait. Dans le premier cas, le décalage est identique à un décalage logique à gauche. Dans le second cas, le bit de signe n'est pas concerné par le décalage.

L'interprétation mathématique des décalages modifier

L'utilité principale des opérations de décalage est qu'elles permettent de faire simplement des multiplications ou divisions par une puissance de 2. Un décalage logique/arithmétique correspond à une multiplication ou division entière par 2^n : multiplication pour les décalages à gauche, division pour les décalages à droite. Les décalages logiques fonctionnent pour les entiers non signés, alors que les décalages arithmétiques fonctionnent sur les entiers signés. Le fait est qu'un décalage logique ne donne pas le bon résultat avec un entier signé, la raison étant qu'il ne préserve pas le bit de signe. À l'inverse, le décalage arithmétique conserve le bit de signe, du moins pour les décalages à droite, ce qui le rend adapté pour les entiers signés. Les décalages arithmétiques à droite permettent donc de faire des divisions par 2^n sur des nombres signés.

Modulo et quotient d'une division par une puissance de deux en binaire

Les arrondis lors des décalages modifier

Les décalages à droite entraînent l'apparition d'arrondis. Lorsqu'on effectue un décalage à droite, certains bits vont sortir du résultat et être perdus. L’équivalent en décimal est que les chiffres après la virgule sont perdus, ce qui arrondit le résultat. Mais cet arrondi dépend de la représentation des nombres utilisé. Pour comprendre pourquoi, il faut faire un rapide rappel sur les types d'arrondis en décimal.

En décimal, on peut arrondir de deux manières : soit on arrondit à l'entier au-dessus, soit on arrondi à l'entier au-dessous. Par exemple, prenons la division 29/4, qui a pour résultat 7.25. Cela donne 7 dans le premier cas et 8 dans le second. Pour un résultat négatif, c'est la même chose, mais le fait que le signe soit inversé change la donne. Par exemple, prenons le résultat de -29 / 4, soit -7.25. On peut l'arrondir soit à -7, soit à -8. En combinant les deux cas négatifs avec les deux cas positifs, on se trouve face à quatre possibilités :

  • l'arrondi vers la plus basse valeur absolue (vers zéro), qui donne respectivement 7 et -7 dans l'exemple précédent.
  • l'arrondi vers la plus basse valeur (vers moins l'infini), qui donne -8 et 7 dans l'exemple précédent ;
  • l'arrondi vers la plus haute valeur (vers plus l'infini), qui donne -7 et 8 dans l'exemple précédent ;
  • l'arrondi vers la plus haute valeur absolue (vers l'infini), qui donne 8 et -8 dans l'exemple précédent.

En binaire, c'est la même chose. Par exemple, 11100,1010 peut s'arrondir en 11100 ou en 11101, suivant qu'on arrondisse vers le bas ou vers le haut, et la même chose est possible pour les nombres négatifs. Précisons que ces arrondis n'ont lieu que si le résultat du décalage n'est pas exact. Pour un décalage d'un rang, à savoir une division par deux, seuls les nombres impairs donnent un arrondi, pas les nombres pairs. De manière générale, pour un décalage de n rangs, les nombres divisibles par 2^n ne donnent pas d'arrondi, alors que les autres si.

Lors d'un décalage, les bits sortants sont simplement éliminés. On pourrait croire que cela signifie que l'arrondi se fait vers zéro (vers la valeur inférieure). C'est bien le cas pour les nombres positifs, mais pas pour les nombres négatifs pour lesquels le résultat dépend de la représentation. Pour les décalages logiques, peu importe la représentation, l'arrondi se fait vers zéro (vu que tous les nombres sont traités comme positifs). Mais pour les décalages arithmétiques, c'est autre chose. En complément à 1, l'arrondi se fait bien vers zéro : les nombres positifs sont arrondis à la valeur inférieure et les nombres négatifs à la valeur supérieure. Par contre, en complément à deux, les nombres positifs et nombres négatifs sont arrondis à la valeur inférieure. En clair, l'arrondi se fait vers moins l'infini. Ce qui peut causer quelques problèmes si l'on ne fait pas attention, le résultat du décalage et d'une division pouvant varier à cause des règles d'arrondis.

Les débordements d'entiers lors des décalages modifier

Outre les arrondis, les décalages peuvent causer ce qu'on appelle des débordements d'entier. Ce terme barbare recouvre toutes les situations où le résultat d'un calcul devient trop gros pour être codé. Pour donner un exemple, prenons une situation équivalente mais en décimal. On suppose que l'on manipule des données codées sur 5 chiffres décimaux, pas plus. Si on prend le nombre 4512, le décalage à gauche d'un cran donne 45120, qui tient sur 5 chiffres : on n'a pas de débordement. Mais si je prends le nombre 97426, un décalage à gauche d'un cran donne 974260, ce qui ne tient pas dans 5 chiffres : on a un débordement d’entier. Celui-ci se traduit par le fait qu'un chiffre non-nul sorte du nombre. La même chose a lieu en binaire, avec les décalages à gauche. Un débordement d'entier en binaire se traduit par le fait qu'au moins un bit non-nul sorte à gauche.

La manière habituelle de gérer les débordements d'entiers est simplement de ne rien faire, mais de prévenir qu'un débordement a eu lieu. Pour cela, le circuit qui effectue le décalage a une sortie qui indique qu'un débordement a eu lieu lors du décalage. Cette sortie fournit un simple bit qui vaut 1 en cas de débordements et 0 sinon (ou l'inverse). Une autre solution est de corriger le débordement, mais si cela est fait pour les opérations arithmétiques, cela n'est pas fait pour les décalages.

Toujours est-il que déterminer l’occurrence d'un débordement n'est pas compliqué. Pour les décalages logiques, il suffit de prendre les bits sortants et de vérifier qu'un au moins d'entre eux vaut 1. Une simple porte OU sur les bits sortants fait l'affaire. Pour les décalages arithmétiques, il faut aussi tenir compte de la présence du bit de signe. La valeur des bits sortants dépend du signe positif ou négatif du nombre. Si le nombre décalé est positif, seuls des zéros doivent sortir, la présence d'un 1 indiquant un débordement d'entier. Pour un nombre négatif, c'est l'inverse : seuls des 1 doivent sortir (du fait des règles d'extension de signe), alors que l’occurrence d'un zéro trahit un débordement d'entier. Pour résumer le tout, les bits sortants sont censés être égaux au bit de signe, un débordement a eu lieu dans le cas contraire. L’occurrence d'un débordement se détermine en décomposant le décalage en une succession de décalages de 1 bit. Si un seul de ces décalages de 1 rang altère le bit de signe (change sa valeur), alors on a un débordement.

Il est possible de déterminer l’occurrence d'un débordement en analysant l'opérande, sans même avoir à faire le décalage. Pour un décalage vers la gauche de rangs, on sait que les bits sortants sont les bits de poids fort de l'opérande. En clair, on peut déterminer si un débordement a lieu en sélectionnant seulement les bits de poids fort de l'opérande. Pour cela, on peut simplement prendre l'opérande et lui appliquer un masque adéquat. Par exemple, prenons le cas d'un débordement pour un décalage logique, qui a lieu si au moins un bit sortant est à 1. Il suffit de prendre l'opérande, conserver les rangs bits de poids fort et mettre les autres à zéro, puis faire un ET entre les bits du résultat. La même logique prévaut pour les décalages arithmétiques, même s'il faut faire quelques adaptations.

Calcul du bit de débordement pour un décalage à gauche de trois rangs.

Toujours est-il que le calcul des débordements peut se faire en parallèle du décalage, ce qui est utile. Précisons que le masque se calcule dans un circuit à part, qui ressemble beaucoup à un encodeur. Le masque calculé peut être utilisé sur certains circuits de décalages, pour transformer des rotations en décalage logiques, par exemple. Mais nous verrons cela plus tard.

Les décaleurs et rotateurs élémentaires modifier

Décaleur - interface

Pour commencer, nous allons voir deux types de circuits : les décaleurs qui effectuent un décalage (logique ou arithmétique, peu importe) et les rotateurs qui effectuent une rotation. Les deux circuits sont conceptuellement séparés, même s’ils se ressemblent. Faire la distinction sera utile dans la suite du cours. Leur interface est la même pour tous les décaleurs et rotateurs élémentaires. On doit fournir l'opérande à décaler et le nombre de rangs qu'on veut décaler en entrée, et on récupère l'opérande décalé en sortie.

Nous allons d'abord voir comment créer un circuit capable de décaler un nombre (vers la droite ou la gauche, peu importe) d'un nombre de rangs variable : on pourra décaler notre nombre de 2 rangs, de 3 rangs, de 4 rangs, etc. Il faudra préciser le nombre de rangs sur une entrée. On peut faire une remarque simple : décaler de 6 rangs, c'est équivalent à décaler de 4 rangs et redécaler le tout de 2 rangs. Même chose pour 7 rangs : cela consiste à décaler de 4 rangs, redécaler de 2 rangs et enfin redécaler d'un rang. En suivant notre idée jusqu'au bout, on se rend compte qu'on peut créer un décaleur à partir de décaleurs plus simples, reliés en cascade, qu'on active ou désactive suivant le nombre de rangs. L'idée est de prendre des décaleurs élémentaires qui décalent par 1, 2, 4, 8, etc ; bref : par une puissance de 2. La raison à cela est que le nombre de rangs par lequel on va devoir décaler est un nombre codé en binaire, qui s'écrit donc sous la forme d'une somme de puissances de deux. Chaque bit du nombre de rang servira à actionner le décaleur qui déplace d'un nombre égal à sa valeur (la puissance de deux qui correspond en binaire).

Décaleur logique - principe

La même logique s'applique pour les rotateurs, la seule différence étant qu'il faut remplacer les décaleurs par 1, 2, 4, 8, etc ; par des rotateurs par 1, 2, 4, 8, etc.

Reste à savoir comment créer ces décaleurs qu'on peut activer ou désactiver à la demande. Surtout que le circuit n'est pas le même selon que l'on parle d'un décalage logique, d'un décalage arithmétique ou d'une rotation. Néanmoins, tous les circuits de décalage/rotation sont fabriqués avec des multiplexeurs à deux entrées et une sortie.

Le circuit décaleur logique modifier

Commençons par étudier le cas du décalage logique. On va prendre comme exemple un décaleur par 4 à droite, mais ce que je vais dire peut être adapté pour créer des décaleurs par 1, par 2, par 8, etc. La sortie vaudra soit le nombre tel qu'il est passé en entrée (le décaleur est inactif), soit le nombre décalé de 4 rangs. Ainsi, si je prends un nombre A, composé des bits a7, a6, a5, a4, a3, a2, a1, a0 ; (cités dans l'ordre), le résultat sera :

  • soit le nombre composé des chiffres a7, a6, a5, a4, a3, a2, a1, a0 (on n'effectue pas de décalage) ;
  • soit le nombre composé des chiffres 0, 0, 0, 0, a7, a6, a5, a4 (on effectue un décalage par 4).

Chaque bit de sortie peut prendre deux valeurs, qui valent soit zéro, soit un bit du nombre d'entrée. On peut donc utiliser un multiplexeur pour choisir quel bit envoyer sur la sortie. Par exemple, pour le choix du bit de poids fort du résultat, celui-ci vaut soit a7, soit 0 : il suffit d’utiliser un multiplexeur prenant le bit a7 sur son entrée 1, et un 0 sur son entrée 0. Il suffit de faire la même chose pour tous les autres bits, et le tour est joué.

Exemple d'un décaleur par 4.

En utilisant des décaleurs basiques par 4, 2 et 1 bit, on obtient le circuit suivant :

Décaleur logique 8 bits.

Le circuit décaleur arithmétique modifier

Les décalages arithmétiques sont basés sur le même principe, à une différence près : on n'envoie pas un zéro dans les bits de poids fort, mais le bit de signe (le bit de poids fort du nombre d'entrée). Un décaleur arithmétique ressemble beaucoup à un décaleur logique, la seule différence étant que c'est le bit de poids fort qui est relié aux entrées des multiplexeurs, là où il y avait un zéro avec le décaleur logique. Par exemple, reprenons un nombre A, composé des bits a7, a6, a5, a4, a3, a2, a1, a0 ; (cités dans l'ordre). La sortie d'un décaleur arithmétique par 4 sera :

  • soit le nombre composé des chiffres a7, a6, a5, a4, a3, a2, a1, a0 (on n'effectue pas de décalage) ;
  • soit le nombre composé des chiffres a7, a7, a7, a7, a7, a6, a5, a4 (on effectue un décalage arithmétique par 4).
Exemple d'un décaleur arithmétique par 4

En combinant des décaleurs basiques par 4, 2 et 1 bits, on obtient le circuit suivant :

Décaleur arithmétique 8 bits

Le circuit rotateur modifier

Les rotations sont elles aussi basées sur le même principe, sauf que ce sont les bits de poids faible qu'on injecte dans les bits de poids forts, au lieu d'un zéro ou du bit de signe. Le circuit est donc le même, sauf que les connexions ne sont pas identiques. Là où il y avait un zéro sur les entrées des multiplexeurs, on doit envoyer le bon bit de poids faible. Par exemple, reprenons un nombre A, composé des bits a7, a6, a5, a4, a3, a2, a1, a0 ; (cités dans l'ordre). La sortie d'un rotateur arithmétique par 4 sera :

  • soit le nombre composé des chiffres a7, a6, a5, a4, a3, a2, a1, a0 (on n'effectue pas de décalage) ;
  • soit le nombre composé des chiffres a3, a2, a1, a0, a7, a6, a5, a4 (on effectue un décalage arithmétique par 4).

Les barell shifters unidirectionnels modifier

Barrel shifter - interface

Dans ce qui précède, on a appris à créer un circuit qui fait des décalages logiques, un autre pour les décalages arithmétiques et un autre pour les rotations. Il nous reste à voir les décaleurs-rotateurs, aussi appelés des barrel shifters, qui sont capables de faire à la fois des décalages et des rotations. Certains décaleur-rotateurs sont capables de faire des rotations et des décalages logiques, d'autres savent aussi réaliser les décalages arithmétiques en plus. Un tel circuit a la même interface qu'un décaleur, sauf qu'on rajoute une entrée qui précise quelle opération faire. Cette entrée indique s'il faut faire un décalage logique, un décalage arithmétique ou une rotation.

Précisons dès maintenant qu'il faut faire la différence entre un barrel shifter unidirectionnel et un barrel shifter bidirectionnel. La différence entre les deux tient dans le sens possible des décalages. Le barrel shifter unidirectionnel ne peut faire que des décalages à gauche ou que des décalages à droite, mais pas les deux. À l'inverse, un barrel shifter bidirectionnel peut faire des décalages à droite et à gauche, suivant ce qu'on lui demande. Dans ce qui va suivre, nous allons nous concentrer sur les barrel shifters qui font des décalages/rotations vers la droite. Les explications seront valides aussi pour des décalages/rotations à gauche, avec quelques petites modifications triviales. Mais nous ne verrons pas comment fabriquer des barrel shifters bidirectionnels. En effet, de tels barrel shifters sont plus compliqués à fabriquer et sont de plus basés sur un barrel shifter unidirectionnel.

Il existe trois grandes méthodes pour fabriquer un décaleur-rotateur.

  • La manière la plus naïve est de prendre un décaleur logique, un décaleur arithmétique et un rotateur, et de prendre le résultat adéquat suivant l’opération voulue. Le choix du bon résultat est effectué par une couche de multiplexeur adaptée. Mais cette solution est inutilement gourmande en multiplexeurs. Après tout, les trois circuits se ressemblent et partagent une même structure.
  • Une autre solution, bien plus économe en multiplexeurs, élimine ces redondances en fusionnant les trois circuits en un seul. Elle part d'un circuit qui effectue des décalages logiques, auquel on ajoute des multiplexeurs pour le rendre capable de faire aussi les décalages arithmétiques et les rotations.
  • La dernière méthode part d'un rotateur et on lui ajoute de quoi faire des décalages logiques.

Le décaleur-rotateur à base de multiplexeurs modifier

Avec la seconde méthode, on part d'un circuit qui effectue des décalages logiques, auquel on ajoute des multiplexeurs pour le rendre capable de faire aussi les décalages arithmétiques et les rotations. Ces nouveaux multiplexeurs ne font que choisir les bits à envoyer sur les entrées des décaleurs. Par exemple, prenons un décalage/rotation par 4 crans. La seule différence entre décalage logique, arithmétique et rotation est ce qu'on met sur les 4 bits de poids fort : un 0 pour un décalage logique, le bit de poids fort pour un décalage arithmétique et les 4 bits de poids faible pour une rotation. Pour choisir entre ces trois valeurs, il suffit de rajouter des multiplexeurs.

La prise en charge des rotations modifier

Nous allons d'abord ajouter des multiplexeurs pour prendre en charge les rotations, un peu de la même manière qu'on modifie un décaleur logique pour lui faire faire aussi des décalages arithmétiques. Pour cela, prenons un décaleur par 4 et étudions les 4 bits de poids fort. Suivant le type de décalage, on doit envoyer soit un zéro, soit le bit de poids faible adéquat sur certaines entrées. Ce choix peut être réalisé par un multiplexeur, tant qu'il est commandé correctement. En clair, il suffit d'ajouter un ou plusieurs multiplexeurs pour chaque décaleur élémentaire par 1, 2, 4, etc. Ces multiplexeurs choisissent quoi envoyer sur l'entrée de l'ancienne couche : soit un 0 (décalage logique), soit le bit de poids faible (rotation). Notons qu'on doit utiliser un multiplexeur par entrée, contrairement au décaleur complet. La raison est qu'un décalage arithmétique envoie toujours le même bit dans les entrées de poids fort, alors qu'une rotation envoie un bit différent sur chaque entrée de poids fort, ce qui demande un multiplexeur par entrée.

Décaleur-rotateur par 4.

La prise en charge des décalages arithmétiques modifier

Il est possible d'étendre le décaleur logique pour lui permettre de faire des décalages arithmétiques. Pour cela, même recette que dans le cas précédent. Encore une fois, suivant le type de décalage, on doit envoyer soit un zéro, soit le bit de poids fort sur certaines entrées. Il est possible d'utiliser un seul multiplexeur dans ce cas précis, car on envoie le même bit sur les entrées de poids fort.

Exemple avec un décaleur par 4.

En combinant des décaleurs basiques par 4, 2 et 1 bits, on obtient un circuit qui fait tous les types de décalages. Pas étonnant que ce circuit soit nommé un décaleur complet. Notons qu'on peut se contenter d'un seul mutiplexeur pour tout le barrel shifter, en utilisant le câblage astucieusement. Après tout, le choix entre 0 ou bit de poids fort est le même pour toutes les entrées concernées. Autant ne le faire qu'une seule fois et connecter toutes les entrées concernées au multiplexeur.

Décaleur complet 8 bits

Le barrel shifter complet modifier

En utilisant les deux modifications en même temps, on se retrouve avec un barrel-shifter complet, capable de faire des décalages et rotations sur 4 bits.

Circuit de rotation partiel.

Les mask barrel shifters modifier

Il est temps de voir la dernière manière possible pour fabriquer un décaleur-rotateur. Celle-ci se base sur les masques, vus au chapitre précédent. L'idée est de faire une rotation et de corriger le résultat si c'est un décalage qui est demandé. La correction à effectuer dépend du type de décalage demandé, suivant qu'il soit logique ou arithmétique. Le circuit complet est organisé comme illustré ci-dessous.

Pour un décalage logique, il suffit de mettre les n bits de poids fort à zéro pour un décalage de n bits vers la droite (inversement, les n bits de poids faible pour un décalage vers la gauche). Et pour mettre des bits de poids fort à zéro sous une certaine condition, on doit utiliser un masque, comme vu précédemment. Le masque en question est le même que celui calculé pour le bit de débordement d'entier. Le masque est calculé par un circuit dédié, avant d'être appliqué au résultat du rotateur. Le circuit de calcul du masque est un encodeur modifié, qu'on peut concevoir avec les techniques des chapitres précédents.

Le circuit d'application du masque est composé d'une couche de portes ET et d'une couche de multiplexeurs. La couche de portes ET applique le masque sur le résultat du rotateur. Les multiplexeurs choisissent entre le résultat du rotateur et le résultat avec masque appliqué. Les multiplexeurs sont commandés par un bit de commande qui indique s'il faut faire un décalage ou une rotation.

Décaleur-rotateur basé sur un masque.

Les barrel shifters bidirectionnels (à double sens de décalage/rotation) modifier

Le circuit précédent est capable d'effectuer des décalages et rotations, mais seulement vers la droite. On peut évidemment concevoir un circuit similaire capable de faire des décalages/rotations vers la gauche, mais il est intéressant d'essayer de créer un circuit capable de faire les deux. Un tel circuit est appelé un barrel shifter bidirectionnel. Notons qu'on doit obligatoirement fournir un bit qui indique dans quelle direction faire le décalage. Précédemment, nous avons vu qu'il existe deux méthodes pour créer un barrel shifter. La première se base sur un décaleur auquel on ajoute de quoi faire les rotations, alors que l'autre se base sur l'application d'un masque en sortie d'un rotateur. Dans ce qui va suivre, nous allons voir comment ces deux types de circuits peuvent être rendus bidirectionnels.

Barrel shifter bidirectionnel - interface

Les barrel shifters bidirectionnels basé sur des multiplexeurs modifier

Commençons par voir comment rendre bidirectionnel un barrel shifter basé sur des multiplexeurs. Pour rappel, ces derniers sont basés sur un décaleur qu'on rend capable de faire des rotations en ajoutant des multiplexeurs.

Une première solution est d'utiliser des barrel shifters bidirectionnels série, série signifiant que les deux sens sont calculés en série, l'un après l'autre. Ils sont composés de décaleurs qui sont capables de faire des décalages/rotations vers la gauche et vers la droite. De tels décaleurs peuvent se concevoir de diverses façons, mais la plus simple se base sur le principe qui veut qu'un décaleur est composé de décaleurs de 1, 2, 4, 8 bits, etc. Chaque décaleur est en double : une version qui décale vers la gauche, et une autre qui décale vers la droite. Lors d'un décalage vers la droite, les décaleurs élémentaire à gauche sont désactivés alors que les décaleurs vers la droite sont actifs (et réciproquement lors d'un décalage à gauche). Le bit qui indique la direction du décalage est envoyé à chaque décaleur et lui indique s'il doit décaler ou non.

Décaleur bidirectionnel

Une autre solution, bien plus simple, est de prendre un décaleur/rotateur vers la gauche et un autre vers la droite, et de prendre la sortie adéquate en fonction de l'opération demandée. Le choix du résultat se fait encore une fois avec une couche de multiplexeurs. Le résultat est ce qu'on appelle un barrel shifter bidirectionnel parallèle, parallèle signifiant que les deux sens sont calculés en parallèle, en même temps. Notons que cette solution ressemble beaucoup à la précédente. À vrai dire, si on prend la première solution et qu'on regroupe ensemble les décaleur/rotateurs allant dans la même direction, on retombe sur un circuit presque identique à un barrel shifter bidirectionnel parallèle.

Les deux techniques précédentes utilisent beaucoup de portes logiques et il est possible de faire bien plus efficace. L'idée est simplement d'inverser l'ordre des bits avant de faire le décalage ou la rotation, puis de remettre le résultat dans l'ordre. Par exemple, pour faire un décalage à gauche, on inverse les bits du nombre à décaler, on fait un décalage à droite, puis on remet les bits dans l'ordre originel, et voilà ! Pour cela, il suffit de prendre un décaleur/rotateur à droite, et d'ajouter deux circuits qui inversent l'ordre des bits : un avant le décaleur/rotateur, un après. Ce circuit d'inversion est une simple couche de multiplexeurs. Le résultat est ce qu'on appelle un barrel shifter bidirectionnel à inversion de bits.

Barrel shifter à inversion de bits.

Le décaleur-rotateur bidirectionnel basé sur des masques modifier

Dans cette section, nous allons voir concevoir un rotateur bidirectionnel avec des masques. Pour cela, il faut juste créer un rotateur bidirectionnel et utiliser des masques pour obtenir des décalages.

Le rotateur bidirectionnel modifier

Pour créer le rotateur bidirectionnel, nous allons devoir étudier ce qui se passe quand on enchaine deux rotations successives. N'allons pas par quatre chemins : l'enchainement de deux rotations successives donne un résultat qui aurait pu être obtenu en ne faisant qu'une seule rotation. Le résultat issu de la succession de deux rotations est identique à celui d'une rotation équivalente. Et on peut calculer le nombre de rangs de la rotation équivalente à partir des rangs des deux rotations initiales. Pour cela, il suffit d'additionner les rangs en question. Par exemple, faire une rotation à droite par 5 rangs suivie d'une rotation à droite de 8 rangs est équivalent à faire une rotation à droite de 5+8 rangs, soit 13 rangs.

La logique est la même quand on enchaine des rotations à droite et à gauche. Il suffit de compter les rangs d'une rotation en les comptant positifs pour une rotation à droite et négatifs pour une rotation à gauche. Par exemple, une rotation de -5 rangs sera une rotation à gauche de 5 rangs, alors qu'une rotation de 10 rangs sera une rotation à droite de 10 rangs. On pourrait faire l'inverse, mais prenons cette convention pour l'explication qui suit. Toujours est-il qu'avec cette convention, l'addition des rangs donne le bon résultat pour la rotation équivalente. Par exemple, si je fais une rotation à droite de 15 rangs et une rotation à gauche de 6 rangs, le résultat sera une rotation de 15-6 rangs : c'est équivalent à une rotation à droite de 9 rangs.

Faisons dès maintenant remarquer quelque chose d'important. Prenons un nombre de n bits. Avec un peu de logique et quelques expériences, on remarque facilement qu'une rotation par ne fait rien, dans le sens où les bits reviennent à leur place initiale. Une rotation par est donc égale à pas de rotation du tout, ce qui est équivalent à faire une rotation par zéro rangs. Ce détail sera utilisé par la suite. Pour le moment, il nous permet de gérer le cas où l'addition de deux rangs donne un résultat supérieur à . Par exemple, prenons une rotation par 56 rangs pour un nombre de 9 bits. La division nous dit que 56 = 9*6 + 2. En clair, faire un décalage par 56 rangs est équivalent à faire 6 rotations totales par 9, suivie d'une rotation par 2 rangs. Les rotations par 9 ne comptant pas, cela revient en fait à faire une rotation par 2 rangs. Le même raisonnement fonctionne dans le cas général, et revient à faire ce qu'on appelle l'addition modulo n. C'est à dire qu'une fois le résultat de l'addition connu, on le divise par et l'on garde le reste de la division. Avec cette méthode, le nombre de rangs de la rotation équivalente est compris entre 0 et .

Les additions modulo n seront notées comme suit : .

Armé de ces explications, on peut maintenant expliquer comment fonctionne le rotateur bidirectionnel. L'idée derrière ce circuit est de remplacer une rotation à droite par une rotation équivalente. Dans ce qui suit, nous utiliserons la notation suivante : est le nombre de rangs de la rotation équivalente, la taille du nombre à décaler et le nombre de rangs du décalage initial. En soi, ce n'est pas compliqué de trouver une rotation équivalente : une rotation à droite de rangs est équivalente à une rotation de rangs, à une rotation de rangs, et de manière générale à toute rotation de rangs. La raison est que les rotations par n ne comptent pas, elles sont éliminées par la division par . Mais les propriétés des calculs modulo n font que cela marche aussi quand on retranche n. Les bizarreries de l'arithmétique modulaire font que, quand on fait les additions modulo n, on peut remplacer tout nombre positif r par sans changer les résultats. Pour résumer, on a :

L'équation précédente dit qu'il suffit d'ajouter ou de retrancher n autant de fois qu'on veut au nombre de rangs initial, pour obtenir le nombre de rangs équivalent. Mais tous les cas possibles ne nous intéressent pas. En effet, on sait que le nombre de rangs de la rotation équivalente est compris entre 0 et . Le résultat que l'on recherche doit donc être compris entre 0 et . Et seul un cas respecte cette contrainte : celui où l'on retranche n une seule fois. On a alors :

L'équation nous dit qu'il est possible de remplacer une rotation à droite par une rotation à gauche équivalente. Par exemple, sur 8 bits et pour une rotation à droite de 6 bits, on a . En clair, la rotation équivalente est ici une rotation à gauche de 2 crans. Vous pouvez essayer avec d'autres exemples, vous trouverez la même chose. Par exemple, sur 16 bits, une rotation à gauche de 3 rangs est équivalente à une rotation à droite de 13 rangs.

Le calcul ci-dessus peut être simplifié en utilisant quelques astuces. Sur la plupart des ordinateurs, n est égal à 8, 16, 32, 64, ou toute autre nombre de la forme . Les cas où n vaut 3, 7, 14 ou autres sont tellement rares que l'on peut les considérer comme anecdotiques. De plus, est compris entre 0 et . On peut donc coder le rang sur un nombre bien précis de bits, tel que n est la valeur haute de débordement (en clair, n-1 est la plus grande valeur codable, n entraine un débordement d'entier). Grâce à cela, on peut coder le nombre de rangs en complément à un ou en complément à deux. Rappelons que ces deux représentations des nombres utilisent l'arithmétique modulaire, c'est à dire que l'addition et la soustraction se font modulo n, et que leur principe est de représenter tout n négatif par un n positif équivalent. Ainsi, tout négatif est codé par un positif équivalent. Et dans ces représentations, on a obligatoirement . En appliquant cette formule dans l'équation précédente, on a :

Reprenons l'exemple d'une rotation à gauche de 2 crans pour un nombre de 8 bits, ce qui est équivalent à une rotation de 6 crans à droite: on a bien 6 = -2 en complément à deux. Reste à faire le calcul ci-dessus par le circuit de rotation.

En complément à un, le calcul de l'opposé d'un nombre consiste simplement à inverser les bits de . En conséquence, le circuit est plus simple en complément à un. Le calcul du nombre de rangs demande juste un inverseur commandable, qu'on sait fabriquer depuis quelques chapitres.

Rotateur bidirectionnel en complément à un.

En complément à deux, le calcul est le suivant :

On pourrait utiliser un circuit pour faire l'addition, mais il y a une autre manière plus simple de faire. L'idée est simplement de prendre le circuit en complément à un et d'y ajouter de quoi corriger le résultat final. En clair, on fait le calcul comme en complément à un, mais la rotation effectuée ne sera pas équivalente, du fait du +1 dans le calcul. Ce +1 indique simplement qu'il faut décaler le résultat obtenu d'un cran supplémentaire. Pour cela, on ajoute un rotateur d'un cran à la fin du circuit.

Rotateur bidirectionnel en complément à deux.

Le circuit final modifier

On peut transformer ce circuit en décaleur-rotateur en appliquant la méthode vue plus haut, à savoir en appliquant un masque en sortie du rotateur. Le circuit obtenu est le suivant :

Décaleur rotateur bidirectionnel basé sur un masque.


Tout circuit de calcul peut être conçu par les méthodes vues dans les chapitres précédents. Mais les circuits de calcul actuels manipulent des nombres de 32 ou 64 bits, ce qui demanderait des tables de vérité démesurément grandes : plus de 4 milliards de lignes en 32 bits ! Il faut donc ruser, pour créer des circuits économes en transistors et rapides. Dans ce chapitre, nous allons voir les circuits capables de faire une addition ou une soustraction, ainsi que quelques circuits spécialisés, comme les additionneurs multi-opérande. Précisons cependant que les constructeurs de processeurs, ainsi que des chercheurs en arithmétique des ordinateurs, travaillent d'arrache-pied pour trouver des moyens de rendre ces circuits de calcul plus rapides et plus économes en énergie. Autant vous dire que les circuits que vous allez voir sont vraiment des circuit qui font pâle figure comparé à ce que l'on peut trouver dans un vrai processeur commercial !

Les circuits pour additionner 2 ou 3 bits modifier

Pour rappel, l'addition se fait en binaire de la même manière qu'en décimal. On additionne les chiffres/bits colonne par colonne, une éventuelle retenue est propagée à la colonne d'à côté. La soustraction fonctionne sur le même principe, sur le même modèle qu'en décimal.

Exemple d'addition en binaire.

En clair, additionner deux nombres demande de savoir additionner 2 bits et une retenue sur chaque colonne, et de propager les retenues d'une colonne à l'autre. La propagation des retenues est quelque chose de simple en apparence, mais qui est sujet à des optimisations extraordinairement nombreuses. Aussi, pour simplifier l'exposition, nous allons voir comment gérer une colonne avant de voir comment sont propagées les retenues. Le fait que les additionneurs soient organisés de manière à séparer les deux nous aidera grandement. En effet, tout additionneur est composé d'additionneurs plus simples, capables d'additionner deux ou trois bits suivant la situation. Ceux-ci gèrent ce qui se passe sur une colonne.

Le demi-additionneur modifier

Un additionneur deux bits implémente la table d'addition, qui est très simple en binaire. Jugez plutôt :

  • 0 + 0 = 0, retenue = 0 ;
  • 0 + 1 = 1, retenue = 0 ;
  • 1 + 0 = 1, retenue = 0 ;
  • 1 + 1 = 0, retenue = 1.

Un circuit capable d'additionner deux bits est donc simple à construire avec les techniques vues dans les premiers chapitres. On voit immédiatement que la colonne des retenues donne une porte ET, alors que celle du bit de somme est calculé par un XOR. Le circuit obtenu est appelé un demi-additionneur. Le voici :

Demi-addtionneur.
Demi-addtionneur.
Circuit d'un demi-addtionneur.
Circuit d'un demi-addtionneur.

L'additionneur complet modifier

Additionneur complet.

Si on effectue une addition en colonne, on doit additionner les deux bits sur la colonne, mais aussi additionner une éventuelle retenue. Il faut donc créer un circuit qui additionne trois bits : deux bits de données, plus une retenue. Ce circuit qui additionne trois bits est appelé un additionneur complet. Voici sa table de vérité :

Retenue entrante Opérande 1 Opérande 2 Retenue sortante Résultat
0 0 0 0 0
0 0 1 0 1
0 1 0 0 1
0 1 1 1 0
1 0 0 0 1
1 0 1 1 0
1 1 0 1 0
1 1 1 1 1

Il est possible d'en concevoir de plusieurs manières différentes, qui donnent des circuits équivalents, mais avec une organisation en portes logiques différentes. La plus simple consiste à utiliser un tableau de Karnaugh, mais elle donne un résultat sous-optimal. Il existe des circuits équivalents composés seulement de portes NAND ou NOR, plus simples à fabriquer en technologie CMOS.

Additionneur complet fabriqué avec des portes NAND.
Additionneur complet fabriqué avec des portes NOR.

D'autres méthodes donnent des résultars plus compréhensibles.

L'additionneur complet conçu avec deux demi-additionneurs modifier

La solution la plus simple consiste à enchaîner deux demi-additionneurs : un qui additionne les deux bits de données, et un second qui additionne la retenue au résultat. La retenue finale se calcule en combinant les sorties de retenue des deux demi-additionneurs, avec une porte OU. Pour vous en convaincre, établissez la table de vérité de ce circuit, vous verrez que ça marche.

Composition d'un additionneur complet. On voit bien que celui-ci est composé de deux demi-additionneurs, en rouge et en bleu, auxquels on a ajouté une porte OU pour calculer la retenue finale. Circuit d'un additionneur complet.

Avec ce circuit, la somme est calculée avec deux portes XOR l'une à la suite de l'autre. La retenue sortante, quant à elle, est calculée avec un circuit à quatre portes logiques. Les autres versions de l'additionneur complet que nous allons voir sont des dérivés de ce circuit, auquel on a appliqué quelques simplifications. Les simplifications portent surtout sur le circuit de calcul de la retenue. En effet, le calcul de la somme n'est pas simplifié, car on ne peut pas vraiment simplifier deux portes XOR qui se suivent simplement. Par contre, le calcul de la retenue sortante est un cas particulier d'un calcul qui revient souvent en électronique, comme nous allons le voir dans la section suivante.

L'additionneur complet basé sur une porte à majorité modifier

Il est possible de calculer la retenue sortante assez simplement, mais à condition de remarquer quelque chose. La retenue sortante vaut 1 seulement si une condition particulière est respectée : au moins 2 des 3 bits d'entrée doivent être à 1. Dit autrement, plus de la moitié des bits d'entrées doivent être à 1. Or,n il existe une porte logique qui fonctionne comme cela : elle met sa sortie à 1 si plus de moitié des entrées vaut 1, et sort un 0 sinon. Cette porte logique complexe s'appelle une porte à majorité. On obtient donc un additionneur en combinant deux portes XOR pour calculer la somme, et une porte à majorité pour la retenue sortante.

Additionneur crée avec une porte à majorité

L'additionneur complet basé sur un multiplexeur modifier

Il est aussi possible de fabriquer un additionneur complet en remplaçant la porte à majorité par un multiplexeur. Le câblage du circuit est cependant totalement différent.

Additionneur crée avec un multiplexeur

L'additionneur complet basé sur la propagation et la génération de retenue modifier

Une autre solution calcule la retenue finale d'une autre manière, en combinant le résultat de deux circuits séparés. Le premier vérifie si l'addition génère une retenue, l'autre si la retenue en entrée est propagée en sortie.

  • Une retenue est dite générée si l'addition donne une retenue, quelle que soit la retenue envoyée en entrée (sous-entendu, même si celle-ci vaut 0). Cela arrive quand les bits additionnés valent tous deux 1 : la retenue sera alors de 1, seul le bit du résultat changera. On peut donc calculer si une retenue est générée en faisant un ET entre les deux bits d'entrée.
  • Une retenue est propagée si la retenue en sortie est égale à la retenue en entrée. En clair, la retenue n'existe que si on envoie une retenue en entrée. Dans ce cas, la retenue finale vaut 1 quand un seul des deux bits d'entrée vaut 1, et vaut 0 sinon. En clair, on peut déterminer si une retenue est propagée en faisant un XOR entre les deux bits d'entrée.

Vous remarquerez que les signaux P et G sont calculés par le premier demi-additionneur.

Ces deux circuits fournissent deux signaux : un signal G qui indique si une retenue est générée, et un signal P qui indique si une retenue est propagée. En combinant les deux, on peut calculer la retenue finale. Elle vaut 1 soit quand la retenue est générée, soit quand la retenue d'entrée vaut 1 et qu'elle est propagée. Dans les autres cas, elle vaut zéro. La traduction en équation logique dit qu'il suffit de faire un ET entre la retenue d'entrée et le bit P, puis de faire un OU avec le bit G. Le circuit obtenu est strictement identique au circuit précédent.

Additionneur complet avec propagation et génération de retenue.
Additionneur complet avec propagation et génération de retenue.

L'additionneur en Manchester carry chain est une modification de l'additionneur précédent, où les portes logiques en orange dans le schéma précédent sont remplacées par un circuit plus simple, composé de quelques transistors et d'une porte NON. Néanmoins, ce circuit fonctionne en pass transistor logic, avec tous les défauts que cela implique. Le principal est que, vu que la retenue d'entrée est envoyée sur la sortie à travers des interrupteurs/transistors, la tension sur la retenue de sortie est plus faible que la tension de la retenue d'entrée. Ce qui pose des problèmes quand on doit enchainer plusieurs additionneurs de ce type, mais laissons cela pour plus tard. Il existe une version de cet additionneur en logique dynamique, où les transistors sont utilisés comme des condensateurs et sont préchargés avant de faire leurs calculs, mais nous n'en parlerons pas ici.

Manchester carry chain

Il existe des additionneurs qui fournissent les signaux P et G en plus du résultat et de la retenue sortante. Mais il en existe qui ne calculent pas la retenue finale. Par contre, ils calculent les signaux P et G qui disent si l'addition de deux bits génère une retenue, ou si elle propage une retenue provenant d'une colonne précédente. Ils fournissent ces deux signaux sur deux sorties P et G pour indiquer s'il y a propagation et génération de retenue. Un tel additionneur est appelé un additionneur P/G (P/G pour propagation/génération). Ils seront très utiles pour créer des circuits additionneurs comme on le verra plus bas.

Additionneur P/G : entrées et sorties. Additionneur P/G : circuit de génération des signaux P et G.

L'additionneur complet basé sur une modification de la retenue sortante modifier

Dans les circuits précédents, la retenue sortante et le bit du résultat sont calculés séparément, même si quelques portes logiques sont partagées entre les deux. Une autre méthode, utilisée dans l'unité de calcul de l'Intel 4004 et de l'Intel 8008, fonctionnait autrement. Avec elle, la retenue sortante et calculée en premier, puis on détermine le bit du résultat à partir de la retenue sortante. En effet, le bit du résultat est l'inverse de la retenue sortante, sauf dans deux cas : les trois bits d'entrée sont à 0, où ils sont tous à 1. Dans les deux cas d'exception, le bit du résultat vaut 0, quelque soit la retenue sortante. L'implémentation de cette idée en circuit est assez simple.

Au circuit de calcul de la retenue sortante, il faut ajouter deux circuits, un pour vérifie si tous les bits additionner valent 0, l'autre s'ils valent tous 1. Leur sortie vaut 1 si c'est le cas. Le premier est une simple porte ET, l'autre une porte NOR. Ensuite, on combine le résultat des trois circuits précédents pour obtenir le résultat final. Si un seul des trois circuits a sa sortie à 1, alors la sortie finale doit être à 0. Elle est à 1 sinon. C'est donc une porte NOR qu'il faut utiliser. Le circuit final est donc celui-ci.

Full adder basé sur une modification de la retenue
Notons qu'on peut encore optimiser le circuit en fusionnant les deux portes NOR entre elles, mais c'est là un détail.

A ce stade, vous êtes certainement étonné qu'un tel circuit ait pu être utilisé. Il utilise beaucoup plus de portes logiques que le circuit précédent, a une profondeur logique supérieure : il n'a rien d'avantageux. Sauf qu'il était utilisé sur d'anciens processeurs, qui utilisaient des transistors de type TLL, et non des transistors CMOS comme les microprocesseurs actuels. Et avec ces transistors, il est possible d'implémenter des portes logiques complexes, comme les portes ET/OU/NON que nous avons évoqué rapidement dans le chapitre sur les transistors ! Un additionneur complet construit ainsi ne prenait que deux à trois transistors.

Par exemple, l'unité de calcul de l'Intel 8008 utilisait trois transistors : un pour une porte NAND, et deux qui implémentaient chacun une porte ET-OU-NON à plusieurs entrées. Après, l'ALU de ce processeur utilisait un circuit légèrement différent, pour deux raisons. La première est que le circuit prend en entrée l'inverse des bits à additionner, pour des raisons obscures. L'autre est qu'il était capable d'additionner trois bits, mais aussi de faire des opérations logiques dessus, comme des XOR, des ET ou des OU, sans ajouter un seul transistor. le Z-80 utilisait aussi un additionneur de ce type. Et nous reparlerons de ce circuit dans le chapitre suivant.

Les autres implémentations modifier

Additionneur complet fabriqué avec 24 transistors.

Les implémentations précédentes utilisent des portes logiques, ce qui est simple et facile à comprendre, mais pas forcément le top du top en termes de performances. Mais les additionneurs des processeurs modernes sont nettement plus optimisés que ça. Et pour cela, ils sont optimisés directement au niveau des transistors. Cela permet de les rendre plus rapides, notamment au niveau du calcul de la retenue. Là où l'addition des deux bits d'opérande se doit d'être rapide, le calcul de la retenue doit absolument être le plus rapide possible, c'est crucial pour les circuits qui vont suivre. Outre les optimisations en termes de rapidité, une implémentation à base de transistors peut économiser des transistors. Tout dépend de l'objectif visé, certains circuit optimisant à fond pour la vitesse, d'autres pour le nombre de transistors, d'autres font un compromis entre les deux. Les circuits de ce genre sont très nombreux, trop pour qu'on puisse les citer.

Mais il existe aussi des cas plus rares, où l'implémentation des additionneurs fait au plus simple. C'est le cas sur le premier processeur ARM, l'ARM1, qui implémente ses additionneurs avec des multiplexeurs et des portes NON. Pour rappel, on peut implémenter n'importe quel circuit avec des multiplexeurs, et les additionneurs ne font pas exception. Sur l'ARM1, les concepteurs ont décidé d'implémenter certains circuits avec des multiplexeurs, additionneurs inclus. La raison n'est pas une question de performance ou d'économie de transistors, juste que c'était plus pratique à fabriquer, sachant que le processeur était le premier CPU ARM de l'entreprise. Pour en savoir plus à ce sujet, voici un lien intéressant :

L'implémentation de l'additionneur complet utilisait des multiplexeurs 2 vers 1, mais une version équivalente est la suivante :

Additionneur complet fabriqué avec des MUX 4 vers 1.

L'addition non signée modifier

Voyons maintenant un circuit capable d'additionner deux nombres entiers: l'additionneur. Dans la version qu'on va voir, ce circuit manipulera des nombres strictement positifs (et donc les nombres codés en complètement à deux, ou en complément à un).

L'additionneur série modifier

Avec un additionneur complet, il est possible d'additionner deux nombres bit par bit. Dit autrement, on peut effectuer l'addition colonne par colonne. Cela demande de coupler l'additionneur avec plusieurs registres à décalages. Les opérandes vont être placées chacune dans un registre à décalage, afin de passer à chaque cycle d'un bit au suivant, d'une colonne à la suivante. Même chose pour le résultat. La retenue de l'addition est stockée dans une bascule de 1 bit, en attente du prochain cycle d'horloge. Un tel additionneur est appelé un additionneur série.

Additionneur série.

Bien que très simple et économe en transistors, cet additionneur est cependant peu performant. Le temps de calcul est proportionnel à la taille des opérandes. Par exemple, additionner deux nombres de 32 bits prendra deux fois plus de temps que l'addition de deux nombres de 16 bits. L'addition étant une opération fréquente, il vaut mieux utiliser d'autres méthodes d'addition, plus rapides. C'est pour cela que la totalité des autres additionneurs préfère utiliser plus de circuits, quitte à gagner quelque peu en rapidité.

L'additionneur à propagation de retenue modifier

L'additionneur à propagation de retenue pose l'addition comme en décimal, en additionnant les bits colonne par colonne avec une éventuelle retenue. Évidemment, on commence par les bits les plus à droite, comme en décimal. Il suffit ainsi de câbler des additionneurs complets les uns à la suite des autres.

Additionneur à propagation de retenue.

Notons la présence de la retenue sortante, qui est utilisée pour détecter les débordements d'entier, ainsi que pour d'autres opérations. Le bit de retenue final est souvent stocké dans un registre spécial du processeur (généralement appelé carry flag).

Notez aussi, sur le schéma précédent, la présence de l’entrée de retenue sur l'additionneur. L'additionneur le plus à droite est bien un additionneur complet, et non un demi-additionneur,c e qui fait qui l'additionneur a une entrée de retenue. Tous les additionneurs ont une entrée de retenue de ce type. Elle est très utile pour l'implémentation de certaines opérations comme l'inversion de signe, la soustraction, l'incrémentation, etc. Certains processeurs sont capables de faire une opération appelée ADC, ADDC ou autre nom signifiant Addition with Carry, qui permet de faire le calcul A + B + Retenue (la retenue en question est la retenue sortante de l'addition précédente, stockée dans le registre carry flag). Son utilité principale est de permettre des additions d'entiers plus grands que ceux supportés par le processeur. Par exemple, cela permet de faire des additions d'entiers 32 bits sur un processeur 16 bits.

Propagation de retenue dans l'additionneur.

Ce circuit utilise plus de portes logiques que l'additionneur série, mais est un peu plus rapide. Il utilise très peu de portes logiques et est assez économe en transistors, ce qui fait qu'il était utilisé sur les tous premiers processeurs 8 et 16 bits. Un défaut de ce circuit est que le calcul des retenues s'effectue en série, l'une après l'autre. En effet, chaque additionneur doit attendre que la retenue de l'addition précédente soit disponible pour donner son résultat. Les retenues doivent se propager à travers le circuit, du premier additionneur jusqu'au dernier. On garde donc un défaut de l'additionneur série, à savoir : le fait que le temps de calcul est proportionnel à la taille des opérandes. Pour éviter cela, les autres additionneurs utilisent diverses solutions : soit calculer les retenues en parallèle, soit éliminer certaines opérations inutiles quand c'est possible.

Une solution est d'utiliser des additionneurs complets de type Manchester carry chain, qui propagent la retenue très rapidement. Néanmoins, l'additionneur de type Manchester carry chain fonctionne en pass transistor logic, avec tous les défauts que cela implique quand on en enchaine plusieurs. L'entrée de retenue est directement connectée sur la sortie de retenue, sans passage par une porte logique. Donc, la tension d'entrée est envoyée directement sur la sortie, sans amplification ou régénération. Si on enchaine plusieurs additionneurs complets de ce type, la tension diminue à chaque passage dans un additionneur complet. En conséquence, on peut difficilement enchainer plus de 4 à 8 additionneurs de type Manchester carry chain à la suite.

Il reste alors à trouver d'autres solutions pour résoudre ce problème de la propagation de retenue. Pour cela, il existe globalement plusieurs solutions, qui donnent quatre types d'additionneurs. Pour résumer ces solutions, en voici une liste rapide :

  • Détecter quand le résultat est disponible, plutôt que d'attendre suffisamment pour couvrir le pire des cas.
  • Limiter la propagation des retenues sur un petit nombre de bits et concevoir l'additionneur avec cette contrainte.
  • Accélérer le calcul de la retenue avec des techniques d'anticipation de retenue.
  • Utiliser des représentations binaires qui permettent de se passer de la propagation de retenue.

Les trois premières méthodes donnent, respectivement, les additionneurs à saut de retenue, à sélection de retenue, et à anticipation de retenue. Nous allons les voir dans les sections suivantes. La quatrième méthode sera vue dans le chapitre suivant, qui abordera l'addition multiopérande.

Les accélérations de la propagation de retenue modifier

La propagation de la retenue est lente, mais il existe de nombreux moyens de la rendre plus rapide. Dans cette section, nous allons voir quelques additionneurs qui visent à accélérer la propagation de la retenue, mais en gardant la base de l'additionneur de propagation de retenue.

Additionneur 4 bits, un bloc.

Avant de poursuivre, partons du principe que l'additionneur est conçu en assemblant des additionneurs à plus simples, qui additionnent environ 4 à 5 bits, parfois plus, parfois moins. Ces additionneurs simples seront nommés blocs dans ce qui suit, et l'un d'entre eux est illustré ci-contre. Chaque bloc prend en entrée les deux opérandes à additionner, mais aussi une retenue d'entrée. Il fournit en sortie non seulement le résultat de l'addition, codé sur 4/5 bits, mais aussi une retenue sortante.

En enchaînant plusieurs blocs les uns à la suite des autres, la retenue est propagée d'un bloc au suivant. La retenue sortante d'un bloc est connectée sur l'entrée de retenue du bloc suivant. L'enjeu est, pour un bloc, de calculer les retenues rapidement, plus rapidement qu'un additionneur à propagation de retenue. Le calcul de l'addition dans un bloc n'a pas besoin d'être accéléré, on garde des additionneurs à propagation de retenue.

Les blocs sont tous identiques dans le cas le plus simple, mais il est possible d'utiliser des blocs de taille variable. Par exemple, le premier bloc peut avoir des opérandes de 6 bits, le second des opérandes de 7 bits, etc. Faire ainsi permet de gagner un petit peu en performances, si la taille de chaque bloc est bien choisie. La raison est une question de temps de propagation des retenues. La retenue met plus de temps à se propager à travers 8 blocs qu'à travers 4, ce qui prend plus de temps qu'à travers 2 blocs, etc. En tenir compte fait que la taille des blocs tend à augmenter ou diminuer quand on se rapproche des bits de poids fort.

Le calcul parallèle de la retenue modifier

4008 Functional Diagram

L'optimisation la plus évidente est de calculer la retenue sortante en parallèle de l'addition. Chaque bloc contient, à côté de l'additionneur à propagation de retenue, on trouve un circuit qui calcule la retenue sortante. Il existe de nombreuses manières de calculer la retenue sortante. La plus simple consiste à établir la table de vérité de l'entrée de retenue, puis d'utiliser les techniques du chapitre sur les circuits combinatoire. Cela marche si les blocs sont de petite taille, mais elle devient difficile si le bloc a des opérandes de 2/3 bits ou plus. Mais des techniques alternatives existent.

Un exemple est celui de l'additionneur CMOS 4008, un additionneur de 4 bit. Il est intéressant de voir comment fonctionne ce circuit. Aussi, voici son implémentation. Le circuit est décomposé en trois sections. Une première couche de demi-additionneurs, le circuit de calcul de la retenue sortante, le reste du circuit qui finit l'addition. Le reste du circuit fait que le calcul de l'addition se fait en propageant la retenue, ce qui en fait un additionneur à propagation de retenue. Le circuit de calcul de la retenue sortante prend les résultats des demi-additionneurs, et les utilise pour calculer la retenue sortante. C'est là une constante de tous les circuits qui vont suivre.

CMOS 4008, circuit découpé en sections

Le point important à comprendre est que les demi-additionneurs génèrent les signaux P et G, qui disent si l'additionneur propage ou génère une retenue. Ces signaux sont alors combinés pour déterminer la retenue sortante. La méthode de combinaison des signaux P et G dépend fortement de l'additionneur utilisé. La méthode utilisée sur le 4008 utilise à la fois les signaux P et G, ce qui fait que c'est un hybride entre un additionneur à propagation de retenue, et un additionneur à anticipation de retenue qui sera vu dans la suite du chapitre. Mais il existe des techniques alternatives pour calculer la retenue sortante. La plus simple d'entre elle n'utilise que les signaux de propagation P, sans les signaux de génération. Et nous allons les voir immédiatement.

L'additionneur à saut de retenue modifier

L'additionneur à saut de retenue (carry-skip adder) est un additionneur dont le temps de calcul est variable, qui dépend des nombres à additionner. Le calcul prendra quelques cycles d'horloges avec certains opérandes, tandis qu'il sera aussi long qu'avec un additionneur à propagation de retenue avec d'autres. Il n'améliore pas le pire des cas, dans lequel la retenue doit être propagée du début à la fin, du bit de poids faible au bit de poids fort. Dans tous les autres cas, le circuit détecte quand le résultat de l'addition est disponible, quand la retenue a fini de se propager. Il permet d'avoir le résultat en avance, plutôt que d'attendre suffisamment pour couvrir le pire des cas.

L'additionneur à saut de retenue peut, sous certaines conditions, sauter complètement la propagation de la retenue dans le bloc. L'idée est de savoir si, dans le bloc, une retenue est générée par l'addition, ou simplement propagée. Dans le second cas, le bloc ne fait que propager la retenue entrante, sans en générer. La retenue entrante est simplement recopiée sur la retenue sortante. La propagation de retenue dans le bloc est alors skippée (mais elle a quand même lieu). Si une retenue est générée dans le bloc, on envoie cette retenue sur la retenue sortante. Le choix entre les deux est le fait s'un multiplexeur.

Carry skip adder : principe de base

Toute la difficulté est de savoir comment commander le multiplexeur. Pour cela, on doit savoir si le circuit propage une retenue ou non. Le bloc propage une retenue si chaque additionneur complet propage la retenue. Les additionneurs complets doivent donc fournir le résultat, mais aussi indiquer s'ils propagent la retenue d'entrée ou non. Le signal de commande du multiplexeur est généré assez simplement : il vaut 1 si tous les additionneurs complets du bloc propagent la retenue précédente. C'est donc un vulgaire ET entre tous ces signaux.

Calcul de la commande du MUX.

L'additionneur à saut de retenue est construit en assemblant plusieurs blocs de ce type.

Additionneur à saut de retenue.

L'additionneur à sélection de retenue modifier

L'additionneur à sélection de retenue découper les opérandes en blocs, qui sont additionnés indépendamment. L'addition se fait en deux versions : une avec la retenue du bloc précédent valant zéro, et une autre version avec la retenue du bloc précédent valant 1. Il suffira alors de choisir le bon résultat avec un multiplexeur, une fois cette retenue connue. On gagne ainsi du temps en calculant à l'avance les valeurs de certains bits du résultat, sans connaître la valeur de la retenue. Petit détail : sur certains additionneurs à sélection de retenue, les blocs de base n'ont pas la même taille. Cela permet de tenir compte des temps de propagation des retenues entre les blocs.

Additionneur à sélection de retenue avec seulement deux blocs.

Dans les exemples du dessus, chaque sous-additionneur étaient des additionneurs à propagation de retenue. Mais ce n'est pas une obligation, et tout autre type d’additionneur peut être utilisé. Par exemple, on peut faire en sorte que les sous-additionneurs soient eux-mêmes des additionneurs à sélection de retenue, et poursuivre ainsi de suite, récursivement. On obtient alors un additionneur à somme conditionnelle, plus rapide que l'additionneur à sélection de retenue, mais qui utilise beaucoup plus de portes logiques.

Les additionneurs à anticipation de retenue modifier

Les additionneurs à anticipation de retenue accélèrent le calcul des retenues en les calculant sans les propager. Au lieu de calculer les retenues une par une, ils calculent toutes les retenues en parallèle, à partir de la valeur de tout ou partie des bits précédents. Une fois les retenues pré-calculées, il suffit de les additionner avec les deux bits adéquats, pour obtenir le résultat.

Additionneur à anticipation de retenue.

Ces additionneurs sont composés de deux parties :

  • un circuit qui pré-calcule la valeur de la retenue d'un étage ;
  • et d'un circuit qui additionne les deux bits et la retenue pré-calculée : il s'agit d'une couche d'additionneurs complets simplifiés, qui ne fournissent pas de retenue.
Additionneur à anticipation de retenue.

Le circuit qui détermine la valeur de la retenue est lui-même composé de deux grandes parties, qui ont chacune leur utilité. La première partie réutilise des additionneurs qui donnent les signaux de propagation et génération de retenue. L'additionneur commence donc à prendre forme, et est composé de trois parties :

  • un circuit qui crée les signaux P et G ;
  • un circuit qui déduit la retenue à partir des signaux P et G adéquats ;
  • et une couche d'additionneurs qui additionnent chacun deux bits et une retenue.
Circuit complet d'un additionneur à anticipation de retenue.

Il ne nous reste plus qu'à voir comment fabriquer le circuit qui reste. Pour cela, il faut remarquer que la retenue est égale :

  • à 1 si l'addition des deux bits génère une retenue ;
  • à 1 si l'addition des deux bits propage une retenue ;
  • à zéro sinon.

Ainsi, l'addition des bits de rangs i va produire une retenue Ci, qui est égale à Gi+(Pi·Ci−1). Si on utilisait cette formule sans trop réfléchir, on retomberait sur un additionneur à propagation de retenue inutilement compliqué. L'astuce des additionneurs à anticipation de retenue consiste à remplacer le terme Ci−1 par sa valeur calculée avant. Par exemple, je prends un additionneur 4 bits. Je dispose de deux nombres A et B, contenant chacun 4 bits : A3, A2, A1, et A0 pour le nombre A, et B3, B2, B1, et B0 pour le nombre B. Si j'effectue les remplacements, j'obtiens les formules suivantes :

  • C1 = G0 + ( P0 · C0 ) ;
  • C2 = G1 + ( P1 · G0 ) + ( P1 · P0 · C0 ) ;
  • C3 = G2 + ( P2 · G1 ) + ( P2 · P1 · G0 ) + ( P2 · P1 · P0 · C0 ) ;
  • C4 = G3 + ( P3 · G2 ) + ( P3 · P2 · G1 ) + ( P3 · P2 · P1 · G0 ) + ( P3 · P2 · P1 · P0 · C0 ).

Ces formules nous permettent de déduire la valeur d'une retenue directement : il reste alors à créer un circuit qui implémente ces formules, et le tour est joué. On peut même simplifier le tout en fusionnant les deux couches d'additionneurs.

Additionneur à anticipation de retenue de 4 bits.

Ces additionneurs sont plus rapides que les additionneurs à propagation de retenue. Ceci dit, utiliser un additionneur à anticipation de retenue sur des nombres très grands (16/32bits) utiliserait trop de portes logiques. Pour éviter tout problème, nos additionneurs à anticipation de retenue sont souvent découpés en blocs, avec soit une anticipation de retenue entre les blocs et une propagation de retenue dans les blocs, soit l'inverse.

Additionneur à anticipation de retenue de 64 bits.

L'additionneur à calcul parallèle de préfixes modifier

Les additionneurs à calcul parallèle de préfixes sont des additionneurs à anticipation de retenue quelque peu améliorés, pour gagner en performances. Ceux-ci sont toujours découpés en trois couches :

  • un circuit qui crée les signaux P et G ;
  • un circuit qui déduit la retenue à partir des signaux P et G adéquats ;
  • et une couche d'additionneurs qui additionnent chacun deux bits et une retenue.

Simplement, ils vont concevoir le circuit de calcul des retenues différemment. Avec eux, le calcul Gi + (Pi · Ci−1) va être modifié pour prendre en entrée non pas la retenue Ci−1, mais les signaux Gi−1 et Pi−1. Dans ce qui va suivre, nous allons noter ce petit calcul o. On peut ainsi écrire que :

Ci = ((((Gi , Pi) o (Gi−1 , Pi−1) ) o (Gi−2 , Pi−2 )) o (Gi−3 , Pi−3)) …

Si on utilisait cette formule sans trop réfléchir, on retomberait sur un additionneur à propagation de retenue inutilement compliqué. Le truc, c'est que o est associatif, et que cela peut permettre de créer pas mal d'optimisations : il suffit de réorganiser les parenthèses. Cette réorganisation peut se faire de diverses manières qui donnent des additionneurs différents. Les diverses réorganisations donnent l'additionneur de Ladner-Fisher, l'additionneur de Brent-Kung, l'additionneur de Kogge-Stone, ou tout design hybride. L'additionneur de Brent-Kung est le plus lent de tous les additionneurs cités, mais c'est celui qui utilise le moins de portes logiques. L'additionneur de Ladner-Fisher est théoriquement le plus rapide de tous, mais aussi celui qui utilise le plus de portes logiques. Les autres sont des intermédiaires.

Additionneur de Kogge-Stone.
Additionneur de Ladner-Fisher.

L'addition signée et la soustraction modifier

Après avoir vu l'addition, il est logique de passer à la soustraction, les deux opérations étant très proches. Si on sait câbler une addition entre entiers positifs, câbler une soustraction n'est pas très compliqué. De plus, la soustraction permet de faire des additions de nombres signés.

Le soustracteur pour opérandes entiers modifier

Pour soustraire deux nombres entiers, on peut adapter l'algorithme e soustraction utilisé en décimal, celui que vous avez appris à l'école. Celui-ci ressemble fortement à l'algorithme d'addition : on soustrait les bits de même poids, et on propage éventuellement une retenue sur la colonne suivante. La différence est que la retenue est soustraite, et non ajoutée. La table de soustraction nous dit quel est le résultat de la soustraction de deux bits. La voici :

  • 0 - 0 = 0 ;
  • 0 - 1 = 1 et une retenue ;
  • 1 - 0 = 1 ;
  • 1 - 1 = 0.

Cette table de soustraction peut servir de table de vérité pour construire un circuit qui soustrait deux bits. Celui-ci est appelé un demi-soustracteur.

Demi-soustracteur.

Celui-ci peut être complété afin de prendre en compte une éventuelle retenue, ce qui donne un soustracteur complet. On remarque que le soustracteur complet et composé de deux demi-soustracteurs placés en série. Le calcul de la retenue se fait en combinant les deux retenues des demi-soustracteurs avec une porte OU.

Soustracteur complet.

Celui-ci permet de créer des soustracteurs sur le même patron que pour les additionneurs. On peut ainsi créer un soustracteur série, un soustracteur à propagation de retenue, et ainsi de suite.

L'additionneur-soustracteur pour opérandes codées en complément à deux modifier

Étudions maintenant le cas de la soustraction en complément à deux, dans l'objectif de créer un circuit soustracteur. Vous savez sûrement que a−b et a+(−b) sont deux expressions équivalentes. Et en complément à deux, − b = not(b) + 1. Dit autrement, a − b = a + not(b) + 1. On pourrait se dire qu'il faut deux additionneurs pour faire le calcul, mais la majorité des additionneurs possède une entrée de retenue pour incrémenter le résultat de l'addition. Un soustracteur en complément à deux est donc simplement composé d'un additionneur et d'un inverseur.

Soustracteur en complément à deux.

Il est possible de créer un circuit capable d'effectuer soit une addition, soit une soustraction : il suffit de remplacer l'inverseur par un inverseur commandable, qui peut être désactivé. On a vu comment créer un tel inverseur commandable dans le chapitre sur les circuits combinatoires. On peut remarquer que l'entrée de retenue et l'entrée de commande de l'inverseur sont activées en même temps : on peut fusionner les deux signaux en un seul.

Additionneur-soustracteur en complément à deux.

Une implémentation alternative est la suivante. Elle remplace l'inverseur commandable par un multiplexeur.

Additionneur-soustracteur en complément à deux, version alternative.

L'additionneur-soustracteur pour opérandes codées en signe-magnitude modifier

Passons maintenant aux nombres codés en signe-valeur absolue.

Étudions tout d'abord un circuit d'addition de deux opérandes en signe-magnitude, les deux opérandes étant notée A et B. Suivant les signes des deux opérandes, on a quatre cas possibles : A + B, A − B (B négatif), −A + B (A négatif) et −A − B (A et B négatifs). On remarque que B − A est égal à − (A − B), et − A − B vaut − (A + B). Ainsi, le circuit n'a besoin que de calculer A + B et A − B : il peut les inverser pour obtenir − A − B ou B − A. A + B et A − B peuvent se calculer avec un additionneur-soustracteur. Il suffit de lui ajouter un inverseur commandable pour obtenir le circuit d'addition finale. On peut transformer ce circuit en additionneur-soustracteur en signe-valeur absolue, mais le circuit combinatoire devient plus complexe.

Additionneur en signe-valeur absolue.

Toute la difficulté tient dans le calcul du bit de signe du résultat, quand interviennent des soustractions. Autant l'addition de deux nombres de même signe ne pose aucun problème, autant l'addition de nombres de signes différents ou les soustractions posent problème. Suivant que ou que , le signe du résultat ne sera pas le même. Intuitivement, on se dit qu'il faut ajouter des comparateurs pour déterminer le signe du résultat. Diverses optimisations permettent cependant de limiter la casse et d'utiliser moins de circuits que prévu. Mais rien d'extraordinaire.

L'additionneur-soustracteur pour opérandes codées en représentation par excès modifier

Passons maintenant aux nombres codés en représentation par excès. On pourrait croire que ces nombres s'additionnent comme des nombres non-signés, mais ce serait oublier la présence du biais, qui pose problème. Dans les cas de nombres signés gérés avec un biais, voyons ce que donne l'addition de deux nombres :

Or, le résultat correct serait :

En effectuant l'addition telle quelle, le biais est compté deux fois. On doit donc le soustraire après l'addition pour obtenir le résultat correct.

Même chose pour la soustraction qui donne ceci :

Or, le résultat correct serait :

Il faut rajouter le biais pour obtenir l'exposant correct.

On a donc besoin de deux additionneurs/soustracteurs : un pour additionner/soustraire les représentations binaires des opérandes, et un autre pour ajouter/retirer le biais en trop/manquant.

L'incrémenteur modifier

Maintenant, nous allons voir un circuit capable d'incrémenter un nombre, appelé l'incrémenteur. Les circuits incrémenteurs étaient très utilisés sur les premiers processeurs 8 bits, comme le Z-80, le 6502, les premiers processeurs x86 comme le 8008, le 8086, le 8085, et bien d'autres. Il s'agit d'un circuit assez simple, mais qu'il peut être intéressant d'étudier.

Le circuit incrémenteur se construit sur la même base qu'un additionneur, qu'on simplifie. En effet, incrémenter un nombre A revient à calculer A + 1. En clair, l'opération effectuée est la suivante :

           
+  0  0  0  0  0  0  0  1
------------------------------

Le calcul alors très simple : il suffit d'additionner 1 au bit de poids faible, sur la colonne la plus à droite, et propager les retenues pour les autres colonnes. En clair, on n'additionne que deux bits à chaque colonne : un 1 sur celle tout à droite, la retenue de la colonne précédente pour les autres. En clair : un incrémenteur est juste un additionneur normal, dont on a remplacé les additionneurs complets par des demi-additionneurs. Le 1 le plus à droite est injecté sur l'entrée de retenue entrante de l'additionneur. Et cela marche avec tous les types d'additionneurs, que ce soit des additionneurs à propagation de retenue, à anticipation de retenue, etc.

L'incrémenteur à propagation de retenue modifier

Un incrémenteur à propagation de retenue est donc constitué de demi-additionneurs enchaînés les uns à la suite des autres. Le circuit incrémenteur basique est équivalent à un additionneur à propagation de retenue, mais où on aurait remplacé tous les additionneurs complets par des demi-additionneurs. L'entrée de retenue entrante est forcément mise à 1, sans quoi l'incrémentation n'a pas lieu.

Circuit incrémenteur.

L'incrémenteur à propagation de retenue était utilisé sur le processeur Intel 8085, avec cependant une optimisation très intéressante. Pour la comprendre, rappelons que les portes logiques sont construites à partir de transistors. Les portes les plus simples à implémenter avec des transistors CMOS et TTL sont les portes NON, NAND et NOR. Le demi-additionneur est donc construit comme ci-dessous

Demi-additionneur en CMOS, les portes coloriées en jaunes sont construites avec un seul transistor CMOS/TTL.

Les ingénieurs ont tenté de se débarrasser de la porte NON, et ont réussit à s'en débarrasser pour une colonne sur deux. L'idée est de prendre les demi-additionneurs deux par deux, par paires. On peut alors regrouper les portes logiques comme ceci :

Brique de base de l'incrémenteur du 8085

Les trois portes sont fusionnées, de manière à donner une porte NOR couplée à une porte NON.

Brique de base de l'incrémenteur du 8085 - les portes en jaune sont faites avec un seul transistor
On peut optimiser le tout en fusionnant la porte XOR avec la porte NON pour le calcul de la somme, la porte XOR étant une porte composite. Mais nous n'en parlerons pas plus que ça ici.

Le résultat est que la propagation de la retenue est plus rapide. Au lieu de passer par une porte NAND et une porte NON, il traverse une seule porte : une porte NAND pour les colonnes paires, une porte NOR pour les colonnes impaires. Avec cette optimisation, la retenue se propage presque deux fois plus vite. Mine de rien, cette optimisation économisait des portes logiques et rendait le circuit deux fois plus rapide.

Les incrémenteurs plus complexes sont rares modifier

Pour résumer, ce circuit ne paye pas de mine, mais il était largement suffisant sur les premiers microprocesseurs. Ils utilisaient généralement un incrémenteur capable de traiter des nombres de 8 bits, guère plus. Ces processeurs étaient très peu puissants, et fonctionnaient à une fréquence très faible. Ainsi, ils n'avaient pas besoin d'utiliser de circuits plus complexes pour incrémenter un nombre, et se contentaient d'un incrémenteur à propagation de retenues.

Il existe cependant des processeurs qui utilisaient des incrémenteurs complexes, avec anticipation de retenues, voir du carry skip. Par exemple, le processeur Z-80 de Zilog utilisait un incrémenteur pour des nombres de 16 bits, ce qui demandait des performances assez élevées. Et cet incrémenteur utilisait à la fois anticipation de retenues et carry skip. Pour ceux qui veulent en savoir plus sur cet incrémenteur, voici un lien sur le sujet :

L'additionneur BCD modifier

Maintenant, voyons un additionneur qui additionne deux entiers au format BCD. Pour cela, nous allons devoir passer par deux étapes. La première est de créer un circuit capable d'additionneur deux chiffres BCD. Ensuite, nous allons voir comment enchaîner ces circuits pour créer un additionneur BCD complet.

L'additionneur BCD qui fait l'opération chiffre par chiffre modifier

Nous allons commencer par voir un additionneur qui additionne deux chiffres en BCD. Il fournit un résultat sur 4 bits et une retenue qui est mise à 1 si le résultat dépasse 10 (la limite d'un chiffre BCD). L'additionneur BCD se base sur un additionneur normal, pour des entiers codés en binaire, auquel on ajoute des circuits pour gérer le format BCD. Les deux chiffres sont codés sur 4 bits et sont additionnés en binaire par un additionneur des plus normal, similaire à ceux vus plus haut. Le résultat est alors un entier codé en binaire, sur 5 bits, qu'on cherche à corriger/convertir pour obtenir un chiffre BCD.

Pour corriger le résultat, une idée intuitive serait de prendre le résultat et de faire une division par 10. Le quotient donne la retenue, alors que le reste est le résultat, le chiffre BCD . Mais faire ainsi prendrait beaucoup de circuits, ce qui ne vaut pas le coup. Il existe une autre méthode beaucoup plus simple.

Une autre méthode détecte si le résultat est égal ou supérieur à 10, ce qui correspond à un "débordement" (on dépasse les limites d'un chiffre BCD). Si le résultat est plus petit que 10, il n'y a rien à faire : le résultat est bon et la retenue est de zéro. Par contre, si le résultat vaut 10 ou plus, il faut corriger le résultat et générer une retenue à 1.

Il faut donc ajouter un circuit qui détecte si le résultat est supérieur à 9. La retenue s'obtient facilement : le circuit qui détecte si le résultat est supérieur à 9 donne directement la retenue. Ce circuit peut se fabriquer simplement à partir de sa table de vérité, ou en utilisant les techniques que nous verrons dans un chapitre ultérieur sur les comparateurs. La solution la plus simple est clairement d'utiliser la table de vérité, ce qui est très simple, assez pour être laissé en exercice au lecteur.

Pour comprendre comment corriger le résultat, établissons une table de vérité qui associe le résultat et le résultat corrigé. L'entrée vaut au minimum 10 et au maximum 9 + 9 = 18. On considère la sortie comme un tout, la retenue étant un 5ème bit, le bit de poids fort.

Entrée Retenue Résultat corrigé (sans retenue) interprétation de la sortie en binaire (retenue inclue)
0 1 0 1 0 (10) 1 0000 (16)
0 1 0 1 1 (11) 1 0001 (17)
0 1 1 0 0 (12) 1 0010 (18)
0 1 1 0 1 (13) 1 0011 (19)
0 1 1 1 0 (14) 1 0100 (20)
0 1 1 1 1 (15) 1 0101 (21)
1 0 0 0 0 (16) 1 0110 (22)
1 0 0 0 1 (17) 1 0111 (23)
1 0 0 1 0 (18) 1 1000 (24)

En analysant le tableau, on voit que pour corriger le résultat, il suffit d'ajouter 6. La raison est que le résultat déborde d'un nibble à 16 en binaire, mais à 10 en décimal : il suffit d'ajouter la différence entre les deux, à savoir 6, et le débordement binaire fait son travail. Donc, la correction après une addition est très simple : si le résultat dépasse 9, on ajoute 6.

On peut maintenant implémenter l'additionneur BCD au complet, en combinant le comparateur de débordement, le circuit de correction, et l'additionneur. La première solution calcule deux versions du résultat : la version corrigée, la version normale. Le choxi entre les deux est réalisée par un multiplexeur, commandé par le comparateur.

Additionneur BCD

L'autre solution utilise un circuit commandable qui soit additionne 6, soit ne fait rien. Le choix entre les deux est commandé par un bit de commande dédié, calculé par le comparateur.

Additionneur BCD, seconde version.

Une version assez compliquée du circuit final est illustrée ci-dessous. Le circuit commandable est un additionneur, précédé d'un circuit comparateur qui détecte si le résultat est supérieur à 9. Si c'est le cas, ce circuit génère l'opérande 6, et l'envoie en entrée de l'additionneur. Le circuit est simple à concevoir, mais gaspille beaucoup de circuit. Idéalement, il vaudrait mieux utiliser un circuit combinatoire d'addition avec une constante conçu pour.

Additionneur BCD, circuit complet.

Pour obtenir un additionneur BCD complet, il suffit d’enchaîner les additionneurs précédents, comme on le ferait avec les additionneurs complets dans un additionneur à propagation de retenue. La seule chose importante est que le circuit précédent est altéré de manière à ce qu'on puisse prendre en compte la retenue. Pour cela, rien de plus simple : il suffit d'utiliser l'entrée de retenue de l'additionneur binaire.

Au final, l'additionneur BCD est beaucoup plus compliqué qu'un additionneur normal. Il rajoute des circuits à un additionneur normal, à savoir un circuit pour détecter si chaque chiffre binaire est >9, un petit additionneur pour ajouter 6 et un multiplexeur. De plus, il est difficile d'appliquer les optimisations disponibles sur les additionneurs non-BCD. Notamment, les circuits d'anticipation de retenue sont totalement à refaire et le résultat est relativement compliqué. c'est ce qui explique pourquoi le BCD a progressivement été abandonné au profit du binaire simple.

L'additionneur BCD par ajustement décimal modifier

L'additionneur BCD précédent effectuait son travail chiffre BCD par chiffre BCD. Il additionne et corrige le résultat un chiffre BCD après l'autre, en commençant par le chiffre BCD de poids faible. Mais il existe des additionneurs BCD qui font autrement. L'idée est d’additionner les deux opérandes avec une addition binaire normale, puis de corriger le résultat. Le tout se fait donc en deux étapes : l'addition, puis la correction du résultat. Les deux étapes traitent des opérandes complètes, de 8, 16, voire 32 bits, et non des chiffres BCD seuls.

Une telle technique était utilisée dans le processeur Intel 8085, et de manière générale sur les premiers processeurs x86. Sur ce processeur, les deux étapes d'addition et de correction du résultat étaient séparées dans deux opérations distinctes. Il n'y avait pas d'opération d'addition BCD proprement dit, seulement une addition binaire normale. Par contre, l'addition était secondée par une opération dite d'ajustement décimal qui transformait un nombre binaire en nombre codé en BCD. Effectuer une addition BCD demandait donc de faire deux opérations à la suite : une addition binaire simple, suivie par l'opération d'ajustement décimal. Cela permettait de gérer des nombres entiers en binaire usuel et des entiers BCD sans avoir deux instructions d'addition séparées pour les deux, sans compter que cela simplifiait aussi les circuits d'addition. L'opération d'ajustement décimal lisait l'opérande à manipuler dans un registre (l’accumulateur), et mémorisait le résultat dans ce même registre. Elle prenait une opérande de 8 bits, soit deux chiffres BCD, et fournissait un résultat de la même taille. Elle avait son propre circuit, assez simple, que nous allons voir dans ce qui suit.

L'ajustement décimal s'effectue en ajoutant une constante bien précise à l'opérande à convertir en BCD. L'idée est que la constante est découpée en morceaux de 4 bits, correspondant chacun à un chiffre BCD de l'opérande, chaque morceau contenant soit un 0, soit 6. Cela permet d'ajouter soit 0, soit, à chaque chiffre BCD, et donc de le corriger. La propagation des retenues d'un chiffre à l'autre est effectuée automatiquement par l'addition binaire de la constante. La constante est calculée en deux étapes, sur un principe similaire à celui vu dans l'additionneur précédent. D'abord, on découpe l'opérande en nibbles et on vérifie si chaque nibble est supérieur ou égal à 10. Ensuite, la seconde étape rend les résultats de ces comparaisons et détermine la valeur de chaque nibble de la constante finale. Par exemple, si je prends l'opérande 1001 1110, le nibble de poids faible déborde, alors que celui de poids fort non. La constante sera donc 0000 0110 : 0x06. Inversement, si le nibble de poids fort déborde et pas celui de poids faible, la constante sera alors 0x60. Et la constante est de 0x66 si les deux nibbles débordent, de 0x00 si aucun ne déborde.

Le circuit d’ajustement décimal est donc composé de trois étapes : deux étapes pour calculer la constante, et un circuit d'addition pour additionner cette constante au nombre de départ. La première étape découpe l'opérande en morceaux de 4 bits, en chiffres BCD, et vérifie si chacun d'entre eux vaut 10 ou plus. La seconde étape prend les résultats de la première étape, et les combine pour calculer la constante. Enfin, on trouve l'addition finale, qui était réalisée par un circuit d'addition utilisé à la fois pour l'ajustement décimal et l'addition binaire. La différence entre une addition normale et une opération d'ajustement décimal tient dans le fait que les deux premières étapes sont désactivées dans une addition normale.

Additionneur BCD parallèle

Les débordements d'entier lors d'une addition/soustraction modifier

Les instructions arithmétiques et quelques autres manipulent des entiers de taille fixe, qui ne peuvent prendre leurs valeurs que dans un intervalle. Pour les nombres positifs, un ordinateur qui code ses entiers sur n bits pourra coder tous les entiers allant de 0 à . Tout nombre en dehors de cet intervalle ne peut pas être représenté. Dans le cas où l'ordinateur gère les nombres négatifs, l'intervalle est différent. Dans le cas général, l'ordinateur peut coder les valeurs comprises de à . Si le résultat d'un calcul sort de cet intervalle, il ne peut pas être représenté par l'ordinateur et il se produit ce qu'on appelle un débordement d'entier.

La valeur haute de débordement désigne la première valeur qui est trop grande pour être représentée par l'ordinateur. Par exemple, pour un ordinateur qui peut coder tous les nombres entre 0 et 7, la valeur haute de débordement est égale à 8. On peut aussi définir la valeur basse de débordement, qui est la première valeur trop petite pour être codée par l'ordinateur. Par exemple, pour un ordinateur qui peut coder tous les nombres entre 8 et 250, la valeur basse de débordement est égale à 7. Pour les nombres entiers, la valeur haute de débordement vaut , alors que la valeur basse vaut (avec et respectivement la plus grande et la plus petite valeur codable par l'ordinateur).

La correction des débordements d'entier : l'arithmétique saturée modifier

Quand un débordement d'entier survient, tous les circuits de calcul ne procèdent pas de la même manière. Dans les grandes lignes, il y a deux réactions possibles : soit on corrige automatiquement le résultat du débordement, soit on ne fait rien et on se contente de détecter le débordement.

Si le débordement n'est pas corrigé automatiquement par le circuit, celui-ci ne conserve que les bits de poids faibles du résultat. Les bits en trop sont simplement ignorés. On dit qu'on utilise l'arithmétique modulaire. Le problème avec ce genre d'arithmétique, c'est qu'une opération entre deux grands nombres peut donner un résultat très petit. Par exemple, si je dispose de registres 4 bits et que je souhaite faire l'addition 1111 + 0010 (ce qui donne 15 + 2), le résultat est censé être 10001 (17), ce qui est un résultat plus grand que la taille d'un registre. En conservant les 4 bits de poids faible, j’obtiens 0001 (1). En clair, un résultat très grand est transformé en un résultat très petit. Cela peut poser problèmes si on travaille uniquement avec des nombres positifs, mais c'est aussi utilisé pour coder des nombres en complément à deux. En clair, faire ainsi est parfois une bonne idée, parfois non.

D'autres circuits utilisent ce qu'on appelle l'arithmétique saturée : si un résultat est trop grand au point de générer un débordement, on arrondi le résultat au plus grand entier supporté par le circuit. Les circuits capables de calculer en arithmétique saturée sont un peu tout petit peu plus complexes que leurs collègues qui ne travaillent pas en arithmétique saturée, vu qu'il faut rajouter des circuits pour corriger le résultat en cas de débordement. Il suffit généralement de rajouter un circuit de saturation, qui prend en entrée le résultat en fournit en sortie une version saturée en cas de débordement. Ce circuit de saturation met la valeur maximale en sortie si un débordement survient, mais se contente de recopier le résultat du calcul sur sa sortie s'il n'y a pas de débordement. Typiquement, il est composé d'une ou de deux couches de multiplexeurs, qui sélectionnent quelle valeur mettre sur la sortie : soit le résultat du calcul, soit le plus grand nombre entier géré par le processeur, soit le plus petit (pour les nombres négatifs/soustractions).

L'arithmétique saturée est surtout utilisée pour les additions et soustractions, mais c'est plus rare pour les multiplications/divisions. Une des raisons est que le résultat d'une addition/soustraction prend un bit de plus que le résultat, là où les multiplications doublent le nombre de bits. Cette large différence se traduit par une grande différence pour les résultats qui débordent. Quand une addition déborde, le résultat réel est proche de la valeur maximale codable. mais quand une multiplication déborde, le résultat peut parfois valoir 200 à 60000 fois plus que la valeur maximale codable. Les calculs avec une valeur saturée/corrigée sont donc crédibles pour une suite d'additions, mais pas pour une suite de multiplications. Raison pour laquelle l'arithmétique saturée est utilisée pour les additions/soustractions, là où on préfère corriger les multiplications/divisions par des méthodes logicielles.

La détection des débordements d'entier modifier

Et quand un débordement d'entier a eu lieu, il vaut mieux que le circuit prévienne ! Pour cela, les circuits de calculs ont une sortie nommée Overflow, dont la valeur indique si le calcul a donné un débordement d'entier ou non. Reste que détecter un débordement ne se fait pas de la même manière selon que l'on parle d'un additionneur non-signé, d'un additionneur signé, d'un multiplieur non-signé, etc.

Les opérations sur des nombres non-signés modifier

Pour le cas des nombres positifs, la détection des débordements dépend assez peu de l'opération. L'idée générale est que le circuit de calcul calcule tous les bits du résultat, quitte à dépasser ce qui est supporté par l'ordinateur. Par exemple, un additionneur 32 bits fournit un résultat sur 33 bits, un multiplieur 32 bits fournit des résultats sur 64 bits, etc. Le circuit de calcul a donc des bits qui sont en trop et doivent être oubliés. Un débordement a lieu quand ces bits oubliés sont pertinents, à savoir quand au moins l'un d'eux est à 1. Par exemple, une addition sur 32 bits déborde quand le 33ème bit est à 1, une multiplication sur 32 bits déborde quand un des 32 bits de poids fort est à 1, etc.

Pour les additionneurs non-signés, la sortie Overflow n'est autre que la retenue finale, celle fournie par le dernier additionneur complet. De plus, le seul type de débordement possible est un débordement par le haut, où le résultat dépasse la valeur maximale. Le circuit de saturation est alors très simple. Il consiste au pire en une seule couche de multiplexeurs. Une solution encore plus simple consiste à utiliser le circuit de mise à la valeur maximale vu dans le chapitre sur les opérations bits à bits.

Gestion des débordements d'entiers lors d'une addition non-signée.

Les opérations signées modifier

Pour les additionneurs non-signés, la gestion des débordements d'entiers dépend fortement de la représentation signée. Dans les grandes lignes, rien ne change avec les représentations en signe-magnitude et par excès, dont les débordements sont gérés de la même manière que pour les nombres positifs. Par contre, il n'en est pas de même pour le complément à deux. Si vous vous rappelez le chapitre 1, j'ai clairement dit que les calculs sur des nombres en complètement à deux utilisent les règles de l'arithmétique modulaire : les calculs sont faits avec un nombre de bits fixé une fois pour toute. Si un résultat dépasse ce nombre de bits fixé, on ne conserve pas les bits en trop. C'est une condition nécessaire pour pouvoir faire nos calculs. À priori, on peut donc penser que dans ces conditions, les débordements d'entiers sont une chose parfaitement normale, qui nous permet d'avoir des résultats corrects. Néanmoins, certains débordements d'entiers peuvent survenir malgré tout et produire des bugs assez ennuyeux.

Si l'on tient en compte les règles du complément à deux, on sait que le bit de poids fort (le plus à gauche) permet de déterminer si le nombre est positif ou négatif : il indique le signe du nombre. Tout se passe comme si les entiers en complément à deux étaient codés sur un bit de moins, et avaient leur longueur amputé du bit de poids fort. Si le résultat d'un calcul a besoin d'un bit de plus que cette longueur, amputée du bit de poids fort, le bit de poids fort sera écrasé; donnant un débordements d'entiers. Il existe une règle simple qui permet de détecter ces débordements d'entiers. L'addition (ou la multiplication) de deux nombres positifs ne peut pas être un nombre négatif : on additionne deux nombres dont le bit de signe est à 0 et que le bit de signe du résultat est à 1, on est certain d'être en face d'un débordements d'entiers. Même chose pour deux nombres négatif : le résultat de l'addition ne peut pas être positif. On peut résumer cela en une phrase : si deux nombres de même signe sont ajoutés, un débordement a lieu quand le bit du signe du résultat a le signe opposé. On peut préciser que cette règle s'applique aussi pour les nombres codés en complément à 1, pour les mêmes raisons que pour le codage en complément à deux. Cette règle est aussi valable pour d'autres opérations, comme les multiplications.

Modifier les circuits d'au-dessus pour qu'ils détectent les débordements en complément à deux est simple comme bonjour : il suffit créer un petit circuit combinatoire qui prenne en entrée les bits de signe des opérandes et du résultat, et qui fasse le calcul de l'indicateur de débordements. Si l'on rédige sa table de vérité, on doit se retrouver avec la table suivante :

Entrées Sortie
000 0
001 1
010 0
011 0
100 0
101 0
110 1
111 0

L'équation de ce circuit est la suivante, avec et les signes des deux opérandes, et la retenue de la colonne précédente :

En simplifiant, on obtient alors :

Or, il se trouve que est tout simplement la retenue en sortie du dernier additionneur, que nous noterons . On trouve donc :

Il suffit donc de faire un XOR entre la dernière retenue et la précédente pour obtenir le bit de débordement.


Pour le moment, nous savons faire des additions, des soustractions, des décalages et rotations, ainsi que des opérations bit à bit. Chaque opération est réalisée par un circuit séparé. Cependant, il est possible de les fusionner en un seul circuit appelé une unité de calcul arithmétique et logique, abrévié ALU (Arithmetic and Logical Unit). Comme son nom l'indique, elle effectue des opérations arithmétiques et des opérations logiques (bit à bit). Tous les processeurs contiennent une ALU très similaire à celle qu'on va voit dans ce qui suit. La plupart des ALUs ne gèrent donc pas les opérations compliquées, comme les multiplications ou les divisions, de même que les décalages et rotation, et vous comprendrez pourquoi dans ce qui suit.

L'interface d'une unité de calcul et sa conception modifier

L'interface d'une ALU est assez simple. Il y a évidemment les entrées pour les opérandes et la sortie pour le résultat, mais aussi une entrée de commande qui permet de choisir l'instruction à effectuer. Sur cette entrée, on place une suite de bits qui précise l'instruction à effectuer. La suite de bit est très variable d'une ALU à l'autre. Elle peut être très structuré, chaque bit configurant une portion de l'ALU, ou être totalement arbitraire. La suite de bit peut être vu est aussi appelée l'opcode, ce qui est un diminution de code opération.

De plus, l'ALU a des sorties pour la retenue de sortie, les bits qui indiquent que le calcul a entrainé un débordement d'entier, etc. Ces bits sont appelés des flags, ou indicateurs. Les plus fréquents sont la retenue de sortie, un bit qui est à 1 si un débordement d'entier a eu lieu, un bit qui est à 1 si un débordement d'entier a eu lieu pour une addition signée (débordement en complètement à deux), un bit qui indique si le résultat est zéro, et quelques autres. Les flags sont calculés avec les circuits vus dans le chapitre précédent, dans la section sur la détection des débordements d'entiers.

Interface d'une ALU

Le bit-slicing modifier

Avant l'invention des premiers microprocesseurs, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Le processeur était composé de plusieurs circuits intégrés, placés sur la même carte mère et connectés ensemble par des fils métalliques. Et l'ALU était un de ces circuits intégrés.

Les ALUs en pièces détachée de l'épique étaient assez simples et géraient 2, 4, 8 bits, rarement 16 bits. Mais il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes. Par exemple, on pouvait combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. 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.

Le bit slicing est utilisé pour des ALU capables de gérer les opérations bit à bit, l'addition, la soustraction, mais guère plus. Les ALU en bit-slice qui gére les multiplications existent, mais sont rares. 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 moindres qu'avec des additionneurs modernes, à anticipation de retenue, mais ce n'était pas un problème pour l'époque.

L'implémentation des opérations bit à bit avec une ALU bit-slice est très simple, la seule complexité est l'addition. Si on combine deux ALU de 4 bits, la première calcule 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 à la seconde ALU. Pour cela, l'ALU doit transmettre un bit de retenue sortant à l'ALU suivante, qui doit elle accepter celui-ci sur une entrée. 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.

Pour cela, l'ALU doit avoir une interface compatible : il faut qu'elle ait une entrée de retenue, et une sortie pour la retenue sortante. La retenue passée en entrée est automatiquement prise en compte lors d'une addition par l'ALU. 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.

L'intérieur d'une unité de calcul modifier

Les unités de calcul les plus simples contiennent un circuit différent pour chaque opération possible. L’entrée de sélection commande des multiplexeurs pour sélectionner le bon circuit.

Unité de calcul conçue avec des sous-ALU reliées par des multiplexeurs.

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 allons voir plusieurs exemples d'unités de calcul configurable dans ce chapitre. Pour le moment, sachez qu'un simple additionneur-soustracteur est un circuit configurable de ce genre.

ALU composée de sous-ALU configurables.

Les ALU sérielles modifier

Les ALU sérielles effectuent leurs calculs 1 bit à la fois, bit par bit. Le circuit est alors très simple : il contient un circuit de calcul très simple, de 1 bit, couplé à trois registres à décalage : un par opérande, un pour le résultat. Le circuit de calcul prend trois bits en entrées et fournit un résultat d'un bit en sortie, avec éventuellement une retenue en sortie. Une bascule est ajoutée au circuit, pour propager les retenues des additions/soustractions, elle ne sert pas pour les opérations bit à bit.

ALU sérielle

Les ALU sérielles ne payent pas de mine, mais elles étaient très utilisées autrefois, sur les tout premiers processeurs. Les ordinateurs antérieurs aux années 50 utilisaient des ALU de ce genre. L'avantage de ces ALU est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes. Il suffit de prévoir des registres à décalage suffisamment longs, ce qui est tout sauf un problème. Par contre, elles sont assez lentes pour faire leur calcul, vu que les calculs se font bit par bit. Elles sont d'autant plus lentes que les opérandes sont longs.

Les ALU entières basées sur un additionneur-soustracteur modifier

Il est possible d'obtenir une ALU entière simple en modifiant un additionneur-soustracteur simple. Pour rappel, un additionneur soustracteur est fait en combinant un additionneur avec un inverseur commandable.

Additionneur soustracteur

Il est possible de modifier l'additionneur, mais aussi les circuits situés juste avant. L'idée est d'ajouter un circuit commandable de mise à zéro la seconde entrée d'opérande. Il est aussi possible de commander l'entrée de retenue entrante de l'additionneur séparément, via sa propre entrée.

ALU basée sur un additionneur soustracteur modifié

Les opérations que peut faire cette ALU sont assez nombreuses. Déjà, elle supporte l'addition et la soustraction, quand la seconde opérande n'est pas inversée. En inversant la seconde opérande, on peut gérer deux opérations. La première est l'identité (on recopie l'opérande d'entrée sur la sortie), qui se fait en désactivant l'inverseur. En activant l'inverseur, on a l'opération d'inversion NOT, à savoir que les bits de l'opérande sont inversés. En jouant sur l'entrée de retenue, on peut aussi émuler d'autres opérations, comme l'incrémentation. Les 8 opérations possibles sont les suivantes :

Reset Invert Retenue entrante Sortie de l'ALU
0 0 0 A + B
0 0 1 A + B + 1
0 1 0 A + = A - B - 1
0 1 1 A + + 1 = A - B
1 0 0 B
1 0 1 B + 1
1 1 0
1 1 1 + 1

Les ALU basées sur un additionneur, avec manipulation des retenues modifier

Maintenant que nous avons vu ce qu'il est possible de faire en modifiant ce qu'il y a avant l'additionneur, nous allons modifier l'additionneur lui-même.

L'implémentation du XOR/NXOR modifier

Dans cette section, nous allons nous intéresser à un circuit qui effectue un XOR en plus de l'addition. Le choix d'utiliser à la fois une addition et un XOR peut sembler bizarre, mais s'explique par le fait qu'une opération XOR est identique à une addition binaire dont on ne tiendrait pas compte des retenues.

Pour rappel, un additionneur complet additionne trois bits, en faisant deux XOR :

Si on met la retenue entrante à zéro, on a :

En clair, en manipulant les entrées de retenue des additionneurs complets, on peut avoir un XOR à partir de l'addition. Pour cela, on ajoute un circuit de masquage, comme vu dans le chapitre sur les opérations bit à bit, pour mettre les entrées à 0, on a le circuit ci-dessous. Le choix de l'opération est le fait d'une entrée de commande, mise à 0 pour un XOR et à 1 pour l'addition. Cette méthode marche avec tous les additionneurs, mais elle est plus simple à implémenter avec les additionneurs à anticipation de retenue.

Circuit qui fait ADD et XOR.

Si un XOR est équivalent à une addition où les retenues sont mises à 0, on peut se demander ce qu'il se passe quand on les met à 1. Dans ce cas, pour chaque additionneur complet, le bit du résultat est :

Sachant que , on se rend compte que le circuit calcule le NXOR des deux entrées.

En clair, en masquant les retenues entrantes, on peut transformer une addition en XOR ou en NXOR. Il suffit d'insérer des circuits de masquage avant les entrées de retenue. Le circuit de masquage soit recopie le bit d'entrée (pour l'addition), soit force les entrées de retenue à 0, soit les force à 1. Et on a déjà vu le circuit adéquat dans le chapitre sur les opérations bit à bit, à savoir la porte 1 bit universelle. Pour rappel, c'est un circuit avec deux bits de commandes, qui prend un bit en entrée et fournit sur la sortie : soit 0, soit 1, soit le bit d'entrée, soit son inverse. Il suffit donc d'ajouter une porte universelle 1 bit juste avant l'entrée de retenue entrante, et le tour est joué !

Additionneur modifiée en ALU entière capable de faire des XOR et NXOR

L'implémentation du ET/OU avec une addition, en utilisant les retenues sortantes modifier

Maintenant, faisons la même chose, mais regardons les retenues de sortie.

Retenue entrante Opérande 1 Opérande 2 Retenue sortante
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 1
1 0 0 0
1 0 1 1
1 1 0 1
1 1 1 1

On remarque deux choses :

  • si la retenue d'entrée est à 0, la retenue de sortie est un ET entre les deux bits d'opérandes.
  • si on met la retenue entrante à 1, alors la retenue sortante sera un OU entre les deux bits d'opérandes

Pour implémenter le circuit, il faut connecter la sortie soit aux bits de résultat, soit aux entrées de retenue. Dans le circuit précédent, si la couche d'additionneurs finale est une couche d'additionneurs complets, on peut extraire le ET et le OU des deux opérandes. Un simple MUX placé à la suite permet de choisir si l'on regarde les bits du résultat ou la "retenue sortante" de ces additionneurs complets.

Implémentation d'une ALU entière simple

L'implémentation du NOT avec une addition, en manipulant retenues et opérandes modifier

Il est enfin possible d'implémenter l'opération NOT avec seulement un additionneur, pas besoin qu'il soit un additionneur-soustracteur. En effet, on obtient le NOT d'une opérande, via deux manières légèrement différentes.

Un additionneur complet peut se comporter comme une porte NOT à condition que l'une des entrées soit à 0 et l'autre à 1. La raison à cela est que l'additionneur complet fait un double XOR : le bit à inverser est XORé avec 0, ce qui le recopie, puis avec 1, ce qui l'inverse. Peu importe l'ordre, vu que le XOR est commutatif et associatif.

Cela donne deux possibilités : soit on met le second opérande à 0 et les retenues à 1, soit on fait l'inverse. Le résultat est disponible sur les bits de résultat. Pour cela, la solution consiste à ajouter un circuit qui met à 0 la seconde opérande, on a déjà le circuit pour manipuler les retenues. Mais cette solution est rarement utilisée, vu que la majorité des ALU utilise un additionneur-soustracteur, qui permet d'implémenter l'opération NOT facilement, tout en permettant d'implémenter aussi la soustraction, pour un budget en transistor mineur et des performances quasiment identiques.

Les ALU basées sur des ALU 1 bit modifier

Les ALU précédentes sont basées sur des additionneurs auxquels on a rajouté des circuits. Mais les additionneurs complets eux-même n'ont pas été modifiés. Les ALU que l'on va voir dans ce qui suit fonctionnent sur un principe différent. Au lieu d'ajouter des circuits autour des additionneurs complets, elles modifient les additionneurs complets de manière à ce qu'il puisse faire des opérations logiques ET/OU/XOR/NXOR. Ils deviennent alors de véritables ALU de 1 bit, qui sont assemblées pour donner des ALU entières.

En plus d'assembler des ALU 1 bit, il faut aussi gérer les retenues, et les différentes manières de faire ressemblent beaucoup à ce qui se fait avec les additionneurs. Par exemple, on peut relier chaque ALU 1 bit comme dans un additionneur à propagation de retenue, où chaque ALU 1 bit envoie sa retenue sortante sur l'entrée de retenue de l'ALU 1 bit suivante. Une autre possibilité est d'utiliser un circuit de calcul des retenues, comme pour un additionneur à anticipation de retenue.

ALU parallèle fabriquée à partir d'ALU 1 bit.

L'exemple de l'ALU du processeur 8086 d'Intel modifier

Voyons maintenant l'exemple du processeur 8086 d'Intel, un des tout premier de la marque. L'additionneur de cette ALU est un additionneur à propagation de retenue, avec une Manchester Carry Chain pour accélérer la propagation des retenues. Pour rappel, un additionneur Manchester carry chain génère en interne deux signaux : un signal de propagation de retenue et un signal de génération de retenue. Les deux sont combinés avec la retenue entrante pour calculer le résultat et la retenue de sortie, la combinaison se faisant avec un circuit basé sur des transistors. Les deux signaux sont déterminés par une unique porte logique, qui prend en entrée les deux bits d'opérande : une porte logique détermine si l'addition génère une retenue, un autre si elle propage la retenue d'entrée sur la colonne suivante. Un tel additionneur est illustré ci-dessous, pour rappel.

Manchester carry chain

Sur le 8086, ces deux portes sont remplacées par une porte logique universelle commandable 2 bit, à savoir un circuit qui peut remplacer toutes les portes logiques 2 bit existantes. Comme vu dans le chapitre sur les opérations bit à bit, cette porte universelle est un simple multiplexeur configuré convenablement. En conséquence, le signal de propagation et de génération de retenue sont configurables et on peut les utiliser pour calculer autre chose. Par exemple, la première porte XOR peut être remplacée par une porte ET, OU, NOR, FALSE (elle donne toujours zéro en sortie), OUI (recopie un bit d'entrée sur la sortie), etc.

ALU du 8086 (bloc de 1 bit)

La gestion des additions et soustractions est alors triviale. Il suffit de configurer les deux portes universelles de manière à obtenir le circuit d'un additionneur complet ou d'un soustracteur complet.

Lors des opérations bit à bit et des décalages, les deux signaux sont configurés de manière à ce qu'au moins l'un d'entre elle ait sa sortie mise à 0. C'est-à-dire que l'une des deux portes universelles sera configurée de manière à devenir une porte FALSE. En faisant cela, la porte XOR aura une entrée à 0, ce qui fait qu'elle recopiera l'autre entrée sur sa sortie. Elle se comportera comme une porte OUi pour l'autre entrée, celle pas mise à 0.

Pour les opérations logiques, l'une des portes universelle est configurée de manière à avoir la porte logique voulue, l'autre est mise à 0, la porte XOR recopie l'entrée de la première. La porte logique mise à 0 est celle qui génère les retenues. La porte qui calcule le signal de propagation de la retenue, celle qui additionne les deux bits d'opérande, est alors configurée pour donner la porte voulue : soit un ET, soit un OU, soit un XOR, soit...

ALU du 8086 lors d'une opération logique

L'ALU du 8086 supporte aussi les décalages d'un rang vers la gauche, qui sont équivalents à une multiplication par deux. L'opérande décalée est envoyé sur les entrées A de chaque additionneur complet. Pour effectuer, ils utilisent une solution très simple : chaque additionneur envoie le bit de l'opérande sur la sortie de retenue. De plus, les entrées d'opérandes ne sont pas additionnées. Pour résumer, il faut que le signal de propagation de retenue soit mis à zéro, alors que le signal de génération de retenue soit égal au bit d'entrée de l'opérande. Les deux portes logiques universelles sont alors configurées pour : la porte de propagation se comporte comme une porte FALSE, l'autre comme une porte OUI qui recopie l'entrée A.

ALU du 8086 lors d'un décalage à gauche d'un rang

En somme, l'ALU fait son travail en configurant les deux portes universelles. Pour configurer les portes, l'ALU contient un petit circuit combinatoire qui traduit l'opcode en signaux envoyés aux portes universelles.

Pour ceux qui veulent en savoir plus sur les circuits de calcul de l'Intel 8086, voici un lien :

L'exemple de l'ALU du processeur Intel x86 8008 modifier

L'ALU du processeur Intel x86 8008 est une ALU 8 bits (les opérandes sont de 8 bits), qui implémente 4 opérations : l'addition, ET, OU, XOR. L'addition est réalisée par un circuit d'anticipation de retenue, chose assez rare sur les processeurs de l'époque. Il n'était pas possible de placer beaucoup de transistors sur les puces de l'époque, ce qui fait que les concepteurs de processeurs tournaient à l'économie et privilégiaient des additionneurs à propagation de retenue.

Comme beaucoup d'ALU des processeurs assez anciens, elle est construite en assemblant plusieurs ALU de 1 bits, chacune étant un additionneur complet amélioré. L'ALU de 1 bit utilise des additionneurs complets implémentés avec le circuit suivant :

Full adder basé sur une modification de la retenue

L'additionneur précédent est modifié pour gérer les trois opérations XOR, ET, OU. Pour gérer le XOR, il suffit de mettre la retenue d'entrée à 0, ce qui est réalisé avec une vulgaire porte ET pour chaque additionneur complet, placée en aval de l'entrée de retenue. Pour gérer les deux autres opérations logiques, le circuit ne suit pas la logique précédente et n'utilise pas de multiplexeur. Le résultat du ET/OU est bien disponible sur la sortie de résultat, non sur la sortie de retenue. A la place, le circuit utilise la porte ET et la porte OU de l'additionneur complet, et désactive la porte inutile. Pour un ET/OU, le circuit met à zéro la retenue entrante. De plus, elle met aussi à zéro la retenue sortante, sans quoi le circuit donne des résultats invalides.

Dans les faits, l'implémentation exacte était légèrement plus complexe, vu que ce circuit était conçu à partir de portes TTL AND-OR-NAND, qui regroupe une porte ET, une porte OU et une porte NAND en une seule. Pour ceux qui veulent en savoir plus sur les circuits de calcul de l'Intel 8008, voici un lien qui pourrait vous intéresser :

L'exemple de l'unité de calcul 74181 modifier

Afin d'illustrer ce qui a été dit plus haut, nous allons étudier un exemple d'unité de calcul : 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 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, car il gérait un bit de retenue en entrée et fournissait une sortie pour la retenue du résultat.

Les opérations gérées par l'ALU 74181 modifier

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. 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.

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.

Schéma fonctionnel du 74181.

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.

L'implémentation de l'ALU 74181 modifier

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.

L'implémentation de ce circuit est, sur le papier, très simple. On prend un additionneur à anticipation de retenue, et chaque additionneur complet est précédé par une porte logique universelle 2 bit, réalisée avec un multiplexeur, qui implémente les 16 opérations logiques. Le circuit est cependant très optimisé, dans le sens où l'additionneur complet est fusionné avec la porte logique universelle. Comme sur le 8086, on modifie la manière dont les signaux de propagation et de génération de retenue sont calculés. Sauf qu'ici, on n'utilise qu'une seule porte logique universelle, très modifiée.

Le 74181 est composé de circuits assez semblables à une porte logique universelle de 2 bits, sauf qu'elle fournit deux sorties : un signal de propagation de retenue, un signal de génération de retenue. Pour comprendre comment il fonctionne, le mieux est d'établir sa table de vérité. On part du principe que le circuit a deux entrées A et B, et calcule A + f(A,B), avec f(A,B) une opération bit à bit.

A B A PLUS f(a,b) P G
0 0 0+f(0,0) f(0,0) 0
0 1 0+f(0,1) f(0,0) 0
1 0 1+f(1,0) 1 f(1,0)
1 1 1+f(1,1) 1 f(1,1)

Sur le 74181, il faut imaginer que le circuit qui calcule f(A,B) est une porte universelle commandable 2 bits, réalisée avec un multiplexeur. Les bits du résultat sont envoyés sur les 4 entrées du multiplexeur, et le multiplexeur choisit le bon bit à partir des entrées A et B (qui sont envoyés sur son entrée de commande. Les 4 entrées du multiplexeur sont notées S0, S1, S2 et S3. On a alors :

A B A PLUS f(a,b) P G
0 0 0+f(0,0) S1 0
0 1 0+f(0,1) S0 0
1 0 1+f(1,0) 1 S2
1 1 1+f(1,1) 1 S3

Le circuit pour faire cela est le suivant :

Circuit de base du 74181, avant l'additionneur

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é. La première couche dans le schéma ci-dessous correspond au circuit qui calcule les signaux P et G. La seconde couche est composée du reste de l'additionneur, à savoir du circuit qui combine les signaux de propagation et de génération des retenues finales.

Schéma des portes logique de l'ALU 74181.

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 :

L'implémentation naïve, avec des multiplexeurs modifier

Pour obtenir une ALU, vous avez peut-être pensé à la solution ci-dessous. L'idée est d'utiliser un circuit par opération et de choisir le résultat adéquat avec des multiplexeurs. Le multiplexeur est évidemment commandé par l'entrée de commande, il reçoit l'opcode. Un exemple avec une ALU de 2 bits est conné ci-dessous.

2-bit ALU

Mais cette solution peut être mélangée avec les solutions précédentes. Par exemple, il est possible de mélanger cette idée avec une ALU basée sur un additionner-soustracteur. Pour obtenir un additionneur-soustracteur, on doit placer un inverseur commandable avant l'additionneur, en aval d'un opérande. L'idée est de relier l'inverseur commandable non seulement à l'additionneur, mais aussi aux portes ET/OU/XOR. Cela permet de faire les opérations NOR/NAND/NXOR, gratuitement, juste en changeant le câblage du circuit. Il est aussi possible d'ajouter un circuit pour mettre à zéro l'éoprande non inversée, comme vu plus haut. Le résultat est celui vu plus haut.

ALU simplifiée.


Additionneur multiopérande de 4 bits, pour 3 opérandes.

Dans ce chapitre, nous allons voir les circuits pour additionner entre eux plus de deux nombres en même temps. Additiuonneur plus de deux opérandes est appelé une addition multiopérande, terme que nous utiliserons dans la suite de ce cours. C'est une opération assez utilisée, qui est notamment à la base de la multiplication binaire, qui sera vue au chapitre suivant. Mais elle est aussi utilisée pour d'autres opérations, comme certaines opérations vectorielles utilisées dans le rendu 3D (et les moteurs de jeux vidéo).

Il existe de nombreux types de circuits capables d'effectuer une addition multiopérande. Ce sont tous des additionneurs normaux modifiés. L'interface de ces additionneurs est la même que celle des additionneurs normaux, sauf qu'ils disposent de plusieurs entrées pour les opérandes. L'un d'entre eux est illustré ci-contre, pour l'addition de trois opérandes de 4 bits. Il est préférable de voir les circuits d'addition multiopérande séparémment des circuits pour la multiplication, pour diverses raisons pédagogiques, ce qui est fait dans ce cours.

Les implémentations naïves, non-optimisées modifier

A cet instant du cours, nous ne disposons que d'additionneurs 2-opérandes, à savoir qu'ils additionnent deux nombres. Pour créer un additionneur multiopérandes, l'intuition nous dit de combiner plusieurs additionneurs 2-opérandes. Et c'est en effet une solution, qui peut se mettre en œuvre de deux manières.

Additionneur multiopérande série versus parallèle.

La première solution utilise des additionneurs en série, placés l'un après l'autre. La seconde solution effectue certaines additions en parallèle d'autres : on obtient alors un additionneur parallèle. Avec la seconde solution, le résultat est alors connu plus tôt, car il y a moins d'additionneurs à traverser en partant des opérandes pour arriver au résultat. Dans les deux cas, on utilise presque autant d'additionneurs que d'opérandes (un additionneur en moins, pour être précis).

Les additionneurs utilisés peuvent être n'importe quel type d'additionneurs. Pour donner un exemple d'additionneurs en série, nous allons prendre un additionneur 4-opérandes (qui additionne 8 opérandes différentes). Par exemple, si on utilise des additionneurs à propagation de retenue, le circuit d'un additionneur 4 bits est celui-ci :

Multiplieur en chaine fait avec des additionneurs à propagation de retenues

Bizarrement, l'usage d'additionneurs à propagation de retenue donne de bonnes performances, tout en économisant beaucoup de portes logiques.

L'addition carry save modifier

Le problème de l'addition, qu'elle soit multiopérande ou non, est la propagation des retenues. La propagation des retenues prend du temps, le reste d'un additionneur étant simplement composé de portes XOR. Mais il se trouve qu'il est possible d'additionner un nombre arbitraire d'opérandes et de ne propager les retenues qu'une seule fois ! Pour cela, on additionne les opérandes entre elles avec une addition carry-save, une addition qui ne propage pas les retenues.

L'addition carry save de trois opérandes modifier

L'addition carry-save fournit deux résultats : un résultat obtenu en effectuant l'addition sans tenir compte des retenues, et un autre composé uniquement des retenues. Pour que cela soit plus concret, nous allons étudier le cas où l'on additionne trois opérandes entre elles. Par exemple, 1000 + 1010 + 1110 donne 1010 pour les retenues, et 1100 pour la somme sans retenue. L'addition se fait comme en binaire normal, colonne par colonne, sauf que les retenues ne sont pas propagées.

Carry save (addition)

Une fois le résultat en carry-save obtenu, il faut le convertir en résultat final. Pour cela, il faut faire une addition normale, avec les retenues placées sur la bonne colonne (les retenues sont ajoutées sur la colonne suivante). L'additionneur carry save est donc suivi par un additionneur normal. L'avantage de cette organisation se comprend quand on compare la même organisation sans carry save. Sans carry save, on devrait utiliser deux additionneurs normaux. Avec, on utilise un additionneur normal et un additionneur carry save plus simple et plus rapide.

L'adder compressor 3:2 modifier

Reste à voir comment faire l'addition en carry save. Notez que les calculs se font indépendamment, colonne par colonne. Cela vient du fait que la table d'addition en binaire, pour 3 bits, le permet :

  • 0 + 0 + 0 = 0, retenue = 0 ;
  • 0 + 0 + 1 = 1, retenue = 0 ;
  • 0 + 1 + 0 = 1, retenue = 0 ;
  • 0 + 1 + 1 = 0, retenue = 1 ;
  • 1 + 0 + 0 = 1, retenue = 0 ;
  • 1 + 0 + 1 = 0, retenue = 1 ;
  • 1 + 1 + 0 = 0, retenue = 1 ;
  • 1 + 1 + 1 = 1, retenue = 1.

La seule contrainte est de pouvoir additionner trois bits. Il se trouve que l'on a déjà un circuit capable de le faire : l'additionneur complet ! Un circuit d'addition de trois opérandes en carry save est donc composé de plusieurs additionneurs complets indépendants, chacun additionnant le contenu d'une colonne. Le tout est illustré ci-dessous.

Additionneur carry-save.

Les additionneurs carry save en général modifier

Les additionneurs carry save additionnent plusieurs opérandes et fournissent un résultat en carry save. Faire une addition multiopérande demande donc : d'additionner les opérandes en carry save, puis de convertir le résultat carry save en résultat final. Le tout demande juste un additionneur carry save et un additionneur normal. L'avantage est que l'additionneur carry save économise beaucoup de portes logiques et est aussi plus rapide.

Additionneur multiopérande en carry save

Les additionneurs carry save sériels modifier

Dans le cas le plus fréquent, un additionneur carry save se conçoit en combinant des additionneurs carry save 3-opérandes. La manière la plus simple est d'enchainer les additionneurs 3-opérandes les uns à la suite des autres, comme illustré ci-dessous.

Adder carry save 5 opérandes séquentiel

Prenez garde : les retenues sont décalées d'un rang pour être additionnées. En conséquence, le circuit ressemble à ceci :

Implémentation d'un additionneur multiopérande avec des additionneur carry-save 3:2.

Mais cette méthode est généralement peu efficace, et on préfére utiliser une organisation parallèle, en arbre, avec des additions faites en parallèle. Les deux méthodes les plus connues donnent les additionneurs en arbres de Wallace, ou en arbres Dadda.

Les arbres de Wallace modifier

Les arbres les plus simples à construire sont les arbres de Wallace. Le principe est d'ajouter des couches d'additionneurs carry-save, et de capturer un maximum de nombres lors de l'ajout de chaque couche. On commence par additionner un maximum de nombres avec des additionneurs carry-save. Pour additionner n nombres, on commence par utiliser n/3 additionneurs carry-save. Si jamais n n'est pas divisible par 3, on laisse tranquille les 1 ou 2 nombres restants. On se retrouve ainsi avec une couche d'additionneurs carry-save. On répète cette étape sur les sorties des additionneurs ainsi ajoutés : on rajoute une nouvelle couche. Il suffit de répéter cette étape jusqu'à ce qu'il ne reste plus que deux résultats : on se retrouve avec une couche finale composée d'un seul additionneur carry-save. Là, on rajoute un additionneur normal, pour additionner retenues et sommes.

Arbre de Wallace pour l'addition de 8 nombres de 8 bits.

Les arbres de Dadda modifier

Les arbres de Dadda sont plus difficiles à comprendre. Contrairement à l'arbre de Wallace qui cherche à réduire la hauteur de l'arbre le plus vite possible, l'arbre de Dadda cherche à diminuer le nombre d'additionneurs carry-save utilisés. Pour cela, l'arbre de Dadda se base sur un principe mathématique simple : un additionneur carry-save peut additionner trois nombres, pas plus. Cela implique que l'utilisation d'un arbre de Wallace gaspille des additionneurs si on additionne n nombres, avec n non multiple de trois.

L'arbre de Dadda résout ce problème d'une manière simple :

  • si n est multiple de trois, on ajoute une couche complète d'additionneurs carry-save ;
  • si n n'est pas multiple de trois, on ajoute seulement 1 ou 2 additionneur carry-save : le but est de faire en sorte que la couche suivante fournisse un nombre d'opérandes multiple de trois.

Et on répète cette étape d'ajout de couche jusqu'à ce qu'il ne reste plus que deux résultats : on se retrouve avec une couche finale composée d'un seul additionneur carry-save. Là, on rajoute un additionneur normal, pour additionner retenues et sommes.

Arbre de Dadda pour l'addition de 8 nombres de 8 bits.

Les adders compressors modifier

Dans les circuits précédents, il n'est pas rare d'enchainer plusieurs additionneurs complets l'un à la suite des autres. Mais il est possible de les regrouper par 2 ou 3, ce qui permet de faire des simplifications. Les regroupements en question sont appelés des adder compressors. Ils prennent en entrée plusieurs bits, provenant d'opérandes différents, et les additionnent. Le résultat est évidemment en carry save, ce qui fait qu'il est codé sur deux bits : un bit de retenue, un bit de résultat.

Les mal-nommés adders compressors 4:2 modifier

Le résultat de la fusion de deux additionneurs complets donne un mal nommé "adder compressor 4:2". Ils disposent de 5 entrées et fournissent 3 sorties. Plus précisément, ils prennent en entrée 4 bits pour les 4 opérandes et un bit pour la retenue d'entrée. Pour la sortie, ils fournissent 3 sorties : le bit de retenue du résultat final, les deux retenues des additions intermédiaires.

La manière la plus simple de les fabriquer est d'utiliser deux additionneurs complets l'un à la suite de l'autre. Le premier additionne trois bits d'opérande et fournit un résultat et des retenues, le résultat est ensuite additionné avec les deux opérandes restantes. Au final, on se retrouve avec deux retenues, et un résultat.

Adder compressor 4-2

Il est cependant possible d'améliorer le circuit pour le rendre légèrement plus rapide. Le problème du circuit précédent est que les portes XOR sont enchainées l'une après l'autre, alors que l'on peut faire comme suit :

Adder compressor 4-2 optimisé

L'additionneur multiopérande itératif modifier

L'additionneur multiopérande itératif additionne les opérandes une par une, le résultat temporaire étant stocké dans le registre. Il porte le nom d'additionneur (multi-opérande) itératif. Il est composé d'un additionneur couplé à un registre, le tout entouré de circuits de contrôle (non-représentés dans les schémas suivants). Le circuit de contrôle est, dans le cas le plus simple, un simple compteur initialisé avec le nombre d'opérandes à additionner.

Additionneur multi-opérande itératif.

Il peut s'implémenter avec des additionneurs carry save. Par exemple, un additionneur itératif peut utiliser un additionneur carry save et un registre pour additionner les opérandes, avant d'envoyer le résultat final à un additionneur normal pour calculer le résultat final.

Additionneur multi-operande itératif en carry save

L'avantage de ce circuit est que le nombre d'opérandes à additionner est variable. Par exemple, prenons un additionneur itératif qui permet d'additionner maximum 127 opérandes (le compteur des circuits de contrôle est de 7 bits). Dans ce cas, qui peut le plus peut le moins : il peut additionner seulement 16 opérandes, seulement 7, seulement 20, etc. Et le résultat est alors connu plus vite : moins on a d'opérandes à additionner, moins le circuit fera d'additions. Par exemple, l'addition de 7 opérandes demandera seulement 6 additions, pas une de plus ou de moins. Les autres circuits que nous verrons dans ce chapitre sont moins flexibles. Ils additionnent toujours le même nombre d'opérandes, ce qui n'est pas un problème dans les cas où ils sont utilisés. Rien n'empêche de mettre certaines opérandes à 0, ce qui permet de faire moins, de faire des calculs avec moins d'opérandes.

Un cas particulier d'addition multi-opérande : le calcul de population count modifier

Voyons maintenant un cas très particulier d'addition multi-opérande : le calcul de la population count d'un nombre, aussi appelée poids de Hamming. Il s'agit d'un calcul qui prend en entrée un nombre, une opérande tout ce qu'il y a de plus normale, codée sur plusieurs bits. La population count calcule le nombre de bits de l'opérande qui sont à 1. C'est donc une addition multi-opérande, sauf que l'on additionne des bits individuels.

Elle est très utilisée quand il faut manipuler la représentation binaire d'un nombre, ou quand on manipule des tableaux de bits. Les deux cas sont peu fréquents en-dehors des codes très bas niveau, mais tout programmeur a déjà eu l'occasion d'en manipuler. C'est une opération très courante dans les algorithmes de correction d'erreur, utilisés dans les transmissions réseaux, que nous verrons dans quelques chapitres. Elle est aussi utilisée pour le codage/décodage vidéo/audio, quand il faut crypter/décrypter des données, etc. Les réseaux de neurones artificiels, notamment ceux utilisés dans l'intelligence artificielle, font aussi usage de cette opération.

Elle peut se calculer de beaucoup de manières différentes. La plus simple s'obtient avec le raisonnement est alors le suivant : si on découpe un nombre en deux parties, la population count du nombre est la somme des population count de chaque partie. Il est possible d'appliquer ce raisonnement de manière récursive sur chaque morceau, jusqu'à réduire chaque morceau 1 bit. Or, la population count d'un bit est égale au bit lui-même, par définition. On en déduit qu'il suffit d'utiliser une série d'additionneurs enchainés en arbre, comme illustré ci-dessous.

Circuit de calcul de population count.

Le circuit est alors composé de plusieurs couches d'additionneurs différents. La première couche additionne deux bits entre eux, elle est donc composée uniquement de demi-additionneurs. La seconde couche est composée d'additionneurs qui prennent des opérandes de deux bits (le résultat des demi-additionneurs), la couche suivante est faite d'additionneurs pour opérandes de 3 bits, etc.

Illustration de la première couche du circuit de POPCNT.

Il est naturellement possible d'utiliser des additionneurs carry save pour économiser des circuits.


Exemple de multiplication en binaire.

Nous allons maintenant aborder un circuit appelé le multiplieur, qui multiplie deux opérandes. La multiplication se fait en binaire de la même façon qu'on a appris à le faire en primaire, si ce n'est que la table de multiplication est vraiment très simple en binaire, jugez plutôt !

  • 0 × 0 = 0.
  • 0 × 1 = 0.
  • 1 × 0 = 0.
  • 1 × 1 = 1.

Pour commencer, petite précision de vocabulaire : une multiplication s'effectue sur deux nombres, le multiplicande et le multiplicateur. Une multiplication génère des résultats temporaires, chacun provenant de la multiplication du multiplicande par un chiffre du multiplicateur : ces résultats temporaires sont appelés des produits partiels. Multiplier deux nombres en binaire demande de générer les produits partiels, de les décaler, avant de les additionner.

La multiplication non-signée modifier

Nous allons d'abord commencer par les multiplieurs qui font de la multiplication non-signée.

Les multiplieurs à base d’additionneur multi-opérandes non-itératif modifier

Une première solution calcule tous les produits partiels en parallèle, en même temps, avant de les additionner avec un additionneur multi-opérandes non-itératif, composé d'additionneurs carry-save. C'est une solution simple, qui utilise beaucoup de circuits, mais est très rapide. C'est la solution utilisée dans les processeurs haute performance moderne, dans presque tous les processeurs grand public, depuis plusieurs décennies.

Multiplieur en arbre.
Multiplieur tableau.

Les multiplieurs itératifs modifier

Les multiplieurs les plus simples génèrent les produits partiels les uns après les autres, et les additionnent au fur et à mesure. Le multiplicateur et le multiplicande sont mémorisés dans des registres. Le reste du circuit est composé d'un circuit de génération des produits partiels, suivi d'un additionneur multiopérande itératif. La multiplication est finie quand tous les bits du multiplicateur ont étés traités (ce qui peut se détermine avec un compteur).

Circuit itératif de multiplication sans optimisation.

Il existe plusieurs multiplieurs itératifs, qui différent par la façon dont ils génèrent le produit partiel : certains traitent les bits du multiplicateur de droite à gauche, les autres dans le sens inverse. Dans les deux cas, on décale le multiplicateur d'un cran à chaque cycle, et on prend son bit de poids faible. Pour cela, on stocke le multiplicateur dans un registre à décalage.

Circuit itératif de multiplication sans optimisation, détaillée.

Voici comment se déroule une multiplication avec un multiplieur qui fait le calcul de droite à gauche, qui commence par les bits de poids faible du multiplicateur :

Fonctionnement multiplieur.

On peut remarquer une chose assez intéressante : quand le produit partiel est nul, le circuit fait quand même l'addition. Cela arrive quand le bit du multiplicateur qui génère le produit partiel est 0 : le produit partiel est naturellement nul. Mais si le produit partiel est nul, pourquoi l'additionner ? Une petite optimisation permet d'éviter cela : si le bit du multiplicateur est nul, on peut se passer de l'addition et ne faire que les décalages. Cela demande de rajouter quelques circuits.

On peut encore optimiser le circuit en utilisant des produits partiels sur n bits. Pour cela, on fait le calcul en commençant par les bits de poids fort du multiplicateur : on parcourt le multiplicateur de droite à gauche au lieu le faire de gauche à droite. L'astuce, c'est qu'on additionne le produit partiel avec les bits de poids fort du registre pour le résultat, et non les bits de poids faible. Le contenu du registre est décalé d'un cran à droite à chaque cycle, ce qui décale automatiquement les produits partiels comme il faut.

Circuit itératif de multiplication, avec optimisation de la taille des produits partiels.

Il est même possible de ruser encore plus : on peut se passer du registre pour le multiplicateur. Il suffit d'initialiser les bits de poids faible du registre résultat avec le multiplicateur au démarrage de la multiplication. Le bit du multiplicateur choisi pour le calcul du produit partiel est simplement le bit de poids faible du résultat.

Multiplieur partagé

Il est possible d'optimiser les multiplieurs précédents en calculant et en additionnant plusieurs produits partiels à la fois. Il suffit d'un additionneur multi-opérande et de plusieurs circuits de génération de produits partiels. Toutefois, cette technique demande de prendre en compte plusieurs bits du multiplicateur à chaque cycle : le nombre de rangs à décaler augmente, sans compter que la génération du produit partiel est un peu plus complexe.

Les multiplieurs diviser pour régner modifier

Il existe enfin un tout dernier type de multiplieurs : les multiplieurs diviser pour régner. Pour comprendre le principe, nous allons prendre un multiplieur qui multiplie deux nombres de 32 bits. Les deux opérandes A et B peuvent être décomposées en deux morceaux de 16 bits, qu'il suffit de multiplier entre eux pour obtenir les produits partiels voulus : une seule multiplication 32 bits se transforme en quatre multiplications d'opérandes de 16 bits. En clair, ces multiplieurs sont composés de multiplieurs qui travaillent sur des opérandes plus petites, associés à des additionneurs.

La multiplication de nombres signés modifier

Tous les circuits qu'on a vus plus haut sont capables de multiplier des nombres entiers positifs, mais on peut les adapter pour qu'ils fassent des calculs sur des entiers signés. Et la manière de faire la multiplication dépend de la représentation utilisée. Les nombres en signe-magnitude ne se multiplient pas de la même manière que ceux en complément à deux ou en représentation par excès. Dans ce qui va suivre, nous allons voir ce qu'il en est pour la représentation signe-magnitude et pour le complément à deux. La représentation par excès est volontairement mise de côté, car ce cas est assez compliqué à gérer et qu'il n'existe pas de solutions simples à ce problème. Cela explique le peu d'utilisation de cette représentation, qui est limitée aux cas où l'on sait qu'on ne fera que des additions/multiplications, le cas de l'exposant des nombres flottants en étant un cas particulier.

La multiplication en représentation signe-magnitude modifier

Pour les entiers en signe-valeur absolue, le calcul de la valeur absolue et du signe sont indépendants. Mathématiquement, la valeur absolue du résultat est le produit des valeurs absolues des opérandes. Quant au signe, on apprend dans les petites classes le tableau suivant :

Signe du multiplicande Signe du multiplieur Signe du résultat
+ + +
- + -
+ - -
- - +

En traduisant ce tableau en binaire, avec la convention + = 0 et - = 1, on trouve la table de vérité d'une porte XOR.

Pour résumer, il suffit de multiplier les valeurs absolues et de déduire le signe du résultat avec un vulgaire XOR entre les bits de signe des nombres à multiplier.

Multiplication en signe-magnitude

La multiplication signée en complément à deux modifier

Dans ce qui va suivre, nous allons nous intéresser à la multiplication signée en complément à deux.

Adapter les multiplieurs non-signés pour la multiplication signée modifier

Les multiplieurs vus plus haut fonctionnent parfaitement quand les deux opérandes ont le même signe, mais pas quand un des deux opérandes est négatif. Avec un multiplicande négatif, le produit partiel est censé être négatif. Mais dans les multiplieurs vus plus haut, les bits inconnus du produit partiel sont remplis avec des zéros, et donc positifs. Pour résoudre ce problème, il suffit d'utiliser une extension de signe sur les produits partiels. Pour cela, il faut faire en sorte que le décalage du résultat soit un décalage arithmétique. Pour traiter les multiplicateurs négatifs, on ne doit pas ajouter le produit partiel, mais le soustraire (l'explication du pourquoi est assez dure à comprendre, aussi je vous épargne les détails). L'additionneur doit donc être remplacé par un additionneur-soustracteur.

Multiplieur itératif pour entiers signés.

Les multiplieurs de Booth modifier

Il existe une autre façon, nettement plus élégante, inventée par un chercheur en cristallographie du nom de Booth : l'algorithme de Booth. Le principe de cet algorithme est que des suites de bits à 1 consécutives dans l'écriture binaire d'un nombre entier peuvent donner lieu à des simplifications. Si vous vous rappelez, les nombres de la forme 01111…111 sont des nombres qui valent 2n − 1. Donc, X × (2^n − 1) = (X × 2^n) − X. Cela se calcule avec un décalage (multiplication par 2^n) et une soustraction. Ce principe peut s'appliquer aux suites de 1 consécutifs dans un nombre entier, avec quelques modifications. Prenons un nombre composé d'une suite de 1 qui commence au n-ième bit, et qui termine au X-ième bit : celle-ci forme un nombre qui vaut 2^n − 2^n−x. Par exemple, 0011 1100 = 0011 1111 − 0000 0011, ce qui donne (2^7 − 1) − (2^2 − 1). Au lieu de faire des séries d'additions de produits partiels et de décalages, on peut remplacer le tout par des décalages et des soustractions.

C'est le principe qui se cache derrière l’algorithme de Booth : il regarde le bit du multiplicateur à traiter et celui qui précède, pour déterminer s'il faut soustraire, additionner, ou ne rien faire. Si les deux bits valent zéro, alors pas besoin de soustraire : le produit partiel vaut zéro. Si les deux bits valent 1, alors c'est que l'on est au beau milieu d'une suite de 1 consécutifs, et qu'il n'y a pas besoin de soustraire. Par contre, si ces deux bits valent 01 ou 10, alors on est au bord d'une suite de 1 consécutifs, et l'on doit soustraire ou additionner. Si les deux bits valent 10 alors c'est qu'on est au début d'une suite de 1 consécutifs : on doit soustraire le multiplicande multiplié par 2^n-x. Si les deux bits valent 01, alors on est à la fin d'une suite de bits, et on doit additionner le multiplicande multiplié par 2^n. On peut remarquer que si le registre utilisé pour le résultat décale vers la droite, il n'y a pas besoin de faire la multiplication par la puissance de deux : se contenter d’additionner ou de soustraire le multiplicande suffit.

Reste qu'il y a un problème pour le bit de poids faible : quel est le bit précédent ? Pour cela, le multiplicateur est stocké dans un registre qui contient un bit de plus qu'il n'en faut. On remarque que pour obtenir un bon résultat, ce bit précédent doit mis à 0. Le multiplicateur est placé dans les bits de poids fort, tandis que le bit de poids faible est mis à zéro. Cet algorithme gère les signes convenablement. Le cas où le multiplicande est négatif est géré par le fait que le registre du résultat subit un décalage arithmétique vers la droite à chaque cycle. La gestion du multiplicateur négatif est plus complexe à comprendre mathématiquement, mais je peux vous certifier que cet algorithme gère convenablement ce cas.

Les division signée et non-signée modifier

En binaire, l'opération de division ressemble beaucoup à l’opération de multiplication. L'algorithme le plus simple que l'on puisse créer pour exécuter une division consiste à faire la division exactement comme en décimal. Pour simplifier, la méthode de division est identique à celle de la multiplication, si ce n'est que les additions sont remplacées par des soustractions. Néanmoins, implémenter cet algorithme sous la forme de circuit est quelque peu compliqué. Il existe plusieurs méthodes de division matérielle, la première étant la division avec restauration, par laquelle nous allons commencer.

Division en binaire.

La division avec restauration modifier

Introduisons la division avec restauration par un exemple. Nous allons cherche à diviser 100011001111 (2255 en décimal) par 111 (7 en décimal). Pour commencer, nous allons commencer par sélectionner le bit de poids fort du dividende (le nombre qu'on veut diviser par le diviseur), et voir combien de fois on trouve le diviseur dans ce bit. Pour ce faire, il faut soustraire le diviseur à ce bit, et voir le signe du résultat. Si le résultat de cette soustraction est négatif, alors le diviseur est plus grand que ce qu'on a sélectionné dans notre dividende. On place alors un zéro dans le quotient. Dans notre exemple, cela fait zéro : on pose donc un zéro dans le quotient. Ensuite, on abaisse le bit juste à côté du bit qu'on vient de tester, et on recommence. On continue ainsi tant que le résultat de la soustraction obtenue est négatif. Quand le résultat de la soustraction n'est pas négatif, on met un 1 à la droite du quotient, et on recommence en partant du reste. Et on continue ainsi de suite.

Division avec restauration.

Notre algorithme semble se dessiner peu à peu : on voir qu'on devra utiliser des décalages et des soustractions, ainsi que des comparaisons. L'implémentation de cet algorithme dans un circuit est super simple : il suffit de prendre trois registres : un pour conserver le "reste partiel" (ce qui reste une fois qu'on a soustrait le diviseur dans chaque étape), un pour le quotient, et un pour le diviseur. L'ensemble est secondé par un additionneur/soustracteur, et par un peu de logique combinatoire. Voici ce que cela donne sur un schéma (la logique combinatoire est omise).

Circuit de division.

L'algorithme de division se déroule assez simplement. Tout d'abord, on initialise les registres, avec le registre du reste partiel qui est initialisé avec le dividende. Ensuite, on soustrait le diviseur de ce "reste" et on stocke le résultat dans le registre qui stocke le reste. Deux cas de figure se présentent alors : le reste partiel est négatif ou positif. Dans les deux cas, on réussit trouver le signe du reste partiel en regardant simplement le bit de signe du résultat. Reste à savoir quoi faire.

  • Le résultat est négatif : cela signifie que le reste est plus petit que le diviseur et qu'on aurait pas du soustraire. Vu que notre soustraction a été effectuée par erreur, on doit remettre le reste tel qu'il était. Ce qui est fait en effectuant une addition. Il faut aussi mettre le bit de poids faible du quotient à zéro et le décaler d'un rang vers la gauche.
  • Le résultat est positif : dans ce cas, on met le bit de poids faible du quotient à 1 avant de le décaler, sans compter qu'il faut décaler le reste partiel pour mettre le diviseur à la bonne place (sous le reste partiel) lors des soustractions.

Et on continue ainsi de suite jusqu'à ce que le reste partiel soit inférieur au diviseur.

La division sans restauration modifier

La méthode précédente a toutefois un léger défaut : on a besoin de remettre le reste comme il faut lorsqu'on a soustrait le diviseur du reste alors qu'on aurait pas du et que le résultat obtenu est négatif. On fait cela en rajoutant le diviseur au reste. Et il y a moyen de se passer de cette restauration du reste partiel à son état originel. On peut très bien continuer de calculer avec ce reste faux, pour ensuite modifier le quotient final obtenu de façon simple, pour obtenir le bon résultat. Il suffit simplement de multiplier le quotient par deux, et d'ajouter 1. Ça parait vraiment bizarre, mais c'est ainsi. Cette méthode consistant à ne pas restaurer le reste comme il faut et simplement bidouiller le quotient s'appelle la division sans restauration.

La division SRT modifier

On peut encore améliorer cette méthode en ne traitant pas notre dividende bit par bit, mais en le manipulant par groupe de deux, trois, quatre bits, voire plus encore. Ce principe est (en partie) à la base de l'algorithme de division SRT. C'est cette méthode qui est utilisée dans les circuits de notre processeur pour la division entière. Sur certains processeurs, le résultat de la division par un groupe 2,3,4,... bits est accéléré par une petite mémoire qui précalcule certains résultats utiles. Bien sûr, il faut faire attention quand on remplit cette mémoire : si vous oubliez certaines possibilités ou que vous y mettez des résultats erronés, vous obtiendrez un quotient faux pour votre division. Et si vous croyez que les constructeurs de processeurs n'ont jamais fait cette erreur, vous vous trompez : cela arrive même aux meilleurs ! Intel en a d'ailleurs fait les frais sur le Pentium 1. L'unité en charge des divisions flottantes utilisait un algorithme similaire à celui vu au-dessus (les mantisses des nombres flottants étaient divisées ainsi), et la mémoire qui permettait de calculer les bits du quotient contenait quelques valeurs fausses. Résultat : certaines divisions donnaient des résultats incorrects !


Maintenant, nous allons voir dans les grandes lignes comment fonctionnent les circuits capables de calculer avec des nombres flottants. Nous allons aborder les calculs en virgule flottante, mais aussi les calculs avec les flottants à virgule fixe et les nombres flottants logarithmiques.

Les nombres à virgule fixe modifier

Pour les flottants à virgule fixe, les opérations sont similaires à ce qu'on a avec des nombres entiers si ce n'est qu'il faut souvent ajouter une division (ou un décalage si le facteur de conversion est bien choisi). Les circuits de calculs sont donc les mêmes. Cependant, certaines opérations impossibles avec des entiers deviennent possibles avec de tels flottants. C'est le cas du calcul des fonctions trigonométriques. Il est possible de créer des circuits qui effectuent des opérations trigonométriques, mais ceux-ci sont peu utilisés dans les ordinateurs actuels. La raison est que les calculs trigonométriques sont assez rares et ne sont réellement utilisés que dans les jeux vidéos (pour les calculs des moteurs physique et graphique), dans les applications graphiques de rendu 3D et dans les applications de calcul scientifique. Ils sont par contre plus courants dans les systèmes embarqués, bien que leur utilisation reste quand même assez peu fréquente. Malgré leur rareté, il est intéressant de voir comment sont conçus ces circuits de calcul trigonométrique en virgule fixe.

L'usage d'une mémoire à interpolation modifier

Les circuits de calcul trigonométriques les plus simples utilisent ce qu'on appelle une mémoire de précalcul. Avec cette technique, il n'y a pas de circuit de calcul proprement dit, mais une ROM qui contient les résultats des calculs possibles. L'opérande du calcul sert d'adresse mémoire, et le mot mémoire associé à cette adresse contient le résultat du calcul demandé. Cette technique peut s'appliquer pour n'importe quelle opération, au point que tout circuit combinatoire existant peut être remplacé par une mémoire ROM.

ALU fabriquée à base de ROM

Cependant, si on utilisait cette technique telle qu'elle, la mémoire ROM serait trop importante pour être implémentée. Rien qu'avec des nombres à virgule fixe de plus de 16 bits, il faudrait une mémoire de 2^16 cases mémoire, chacune faisant 16 bits, et ce pour une seule opération. Ne parlons même pas du cas avec des nombres de 32 ou 64 bits ! Pour cela, on va donc devoir ruser pour réduire la taille de cette ROM.

Mais qui dit réduire la taille de la ROM signifie que certains résultats ne seront pas connus. Il y aura forcément des opérandes pour lesquelles la ROM n'aura pas mémorisé le résultat et pour lesquels la mémoire de précalcul seule ne peut rien faire. La solution sera alors de calculer ces résultats à partir d'autres résultats connus et mémorisés dans la mémoire ROM. La mémoire ROM n'a donc pas besoin de stocker tous les résultats et peu se contenter de ne mémoriser que les résultats essentiels, ceux qui permettent de calculer tous les autres. On doit distinguer deux types d'opérandes : celles dont le résultat est stocké dans la ROM, celles dont le résultat est calculé à partir des précédentes. Les opérandes dont le résultat est en ROM seront appelées des opérandes précalculés dans ce qui suit, alors que les autres seront appelées les opérandes non-précalculés.

Une première optimisation : les identités trigonométriques modifier

La première manière de ruser est d'utiliser les identités trigonométriques pour calculer les résultats non-précalculés.

Par exemple, on sait que , ce qui permet d'éliminer la moitié des valeurs stocker dans la ROM. On a juste à utiliser des inverseurs commandables commandés par le bit de signe pour faire le calcul de à partir de celui de .

De même, l'identité permet de calculer des cosinus à partir de sinus déjà connus, ce qui élimine le besoin d'utiliser une mémoire séparée pour les cosinus.

Enfin, l'identité permet de calculer la moitié des sinus quand l'autre est connue.

Et on peut penser à utiliser d'autres identités trigonométriques, mais pas les trois précédentes sont déjà assez intéressantes. Le problème est qu'on ne peut pas envoyer les opérandes non-précalculés. À la place, on doit transformer l'opérande non-précalculées pour obtenir un opérande précalculé, géré par la mémoire ROM. Il faut donc des circuits qui se chargent de détecter ces opérandes , de les transformer en opérandes reconnus par la ROM, puis de corriger la donnée lue en ROM pour obtenir le résultat adéquat. Les circuits en question dépendent de l'identité trigonométrique utilisée, aussi on ne peut pas faire de généralités sur le sujet.

Une seconde optimisation : l'interpolation linéaire modifier

Interpolation memory - principe

La seconde ruse n'utilise pas d'identités trigonométriques qui donnent un résultat exact, mais calcule une approximation du résultat, sauf pour les opérandes précalculés. L'idée est de prendre les deux (ou trois, ou quatre, peu importe) résultats précalculés les plus proches du résultat voulu, et de les utiliser pour faire une approximation.

L'approximation du résultat se calcule en faisant une interpolation linéaire, à savoir une moyenne pondérée des deux résultats les plus proches. Par exemple, si on connaît le résultat pour sin(45°) et pour sin(50°), alors on peut calculer sin(47,5°), sin(47°), sin(45,5°), sin(46,5°) ou encore sin(46°) en faisant une moyenne pondérée des deux résultats. Une telle approximation est largement suffisante pour beaucoup d'applications.

Le circuit qui permet de faire cela est appelée une mémoire à interpolation. Le schéma de principe du circuit est illustré ci-contre, alors que le schéma détaillé est illustré ci-dessous.

Interpolation memory.

L'algorithme CORDIC modifier

Sur du matériel peu puissant, les fonctions trigonométriques peuvent être calculées avec l'algorithme CORDIC. Celui-ci est notamment très utilisé dans les calculatrices modernes, qui possèdent un circuit séquentiel ou un logiciel pour exécuter cet algorithme. Cet algorithme fonctionne par approximations successives, chaque itération de l'algorithme permettant de s’approcher du résultat final. Il utilise les mathématiques du cercle trigonométrique (qui sont considérées acquises dans ce qui suit). Cet algorithme représente un angle par un vecteur unitaire dans le cercle trigonométrique, plus précisément par l'angle que forme le vecteur avec l'axe des abscisses. Le cosinus et le sinus de l'angle sont tout simplement les coordonnées x et y du vecteur, par définition. En travaillant donc directement avec les coordonnées du vecteur, l'algorithme peut connaître à chaque itération le cosinus et le sinus de l'angle. Dit autrement, pour un vecteur de coordonnées (x,y) et d'ange , on a :

CORDIC Vector Rotation 1

L'algorithme CORDIC part d'un principe simple : il va décomposer un angle en angles plus petits, dont il connaît le cosinus et le sinus. Ces angles sont choisis de manière à avoir une propriété assez particulière : leur tangente est une puissance de deux. Ainsi, par définition de la tangente, on a : . Vous aurez deviné que cette propriété se marie bien avec le codage binaire et permet de simplifier fortement les calculs. Nous verrons plus en détail pourquoi dans ce qui suit. Toujours est-il que nous pouvons dire que les angles qui respectent cette propriété sont les suivants : 45°, 26.565°, 14.036°, 7,125°, ... , 0.0009°, 0.0004°, etc.

L'algorithme part d'un angle de 0°, qu'il met à jour à chaque itération, de manière à se rapprocher de plus en plus du résultat. Plus précisément, cet algorithme ajoute ou retranche un angle précédemment cité à chaque itération. Typiquement, on commence par faire une rotation de 45°, puis une rotation de 26.565°, puis de 14.036°, et ainsi de suite jusqu’à tomber sur l'angle qu'on souhaite. À chaque itération, on vérifie si la valeur de l'angle obtenue est égale inférieure ou supérieure à l'angle voulu. Si l'angle obtenu est supérieur, la prochaine itération retranchera l'angle précalculé suivant. Si l'angle obtenu est inférieur, on ajoute l'angle précalculé. Et enfin, si les deux sont égaux, on se contente de prendre les coordonnées x et y du vecteur, pour obtenir le cosinus et le sinus de l'angle voulu.

CORDIC-illustration

Du principe aux calculs modifier

Cette rotation peut se calculer assez simplement. Pour un vecteur de coordonnées , la rotation doit donner un nouveau vecteur de coordonnées . Pour une rotation d'angle , on peut calculer le second vecteur à partir du premier en multipliant par une matrice assez spéciale (nous ne ferons pas de rappels sur la multiplication matricielle ou les vecteurs dans ce cours). Voici cette matrice :

Une première idée serait de pré-calculer les valeurs des cosinus et sinus, vu que les angles utilisés sont connus. Mais ce pré-calcul demanderait une mémoire assez imposante, aussi il faut trouver autre chose. Une première étape est de simplifier la matrice. En factorisant le terme , la multiplication devient celle-ci (les signes +/- dépendent de si on retranche ou ajoute l'angle) :

Encore une fois, la technique du précalcul serait utilisable, mais demanderait une mémoire trop importante. Rappelons maintenant que la tangente de chaque angle est une puissance de deux. Ainsi, la multiplication par devient un simple décalage ! Autant dire que les calculs deviennent alors nettement plus simples. L'équation précédente se simplifie alors en :

Le terme sera noté , ce qui donne :

Il faut noter que la constante peut être omise dans le calcul, tant qu'on effectue la multiplication à la toute fin de l'algorithme. À la fin de l'algorithme, on devra calculer le produit de tous les et y multiplier le résultat. Or, le produit de tous les est une constante, approximativement égale à 0,60725. Cette omission donne :

Le tout se simplifie en :

On peut alors simplifier les multiplications pour les transformer en décalages, ce qui donne :

Les circuits CORDIC modifier

Ainsi, une rotation demande juste de décaler x et y et d'ajouter le tout aux valeurs avant décalage d'une certaine manière. Voici le circuit qui dérive de la matrice précédente. Ce circuit prend les coordonnées du vecteur et lui ajoute/retranche un angle précis. On obtient ainsi le circuit de base de CORDIC.

CORDIC base circuits

Pour effectuer plusieurs itérations, il est possible de procéder de deux manières. La plus évidente est d'ajouter un compteur et des circuits à la brique de base, afin qu'elle puisse enchainer les itérations les unes après les autres.

CORDIC (Bit-Parallel, Iterative, Circular Rotation)

La seconde méthode est d'utiliser autant de briques de base pour chaque itération.

CORDIC (Bit-Parallel, Unrolled, Circular Rotation)

Les nombres flottants IEEE754 modifier

Il est temps de voir comment créer des circuits de calculs qui manipulent des nombres flottants au format IEEE754. Rappelons que la norme IEEE754 précise le comportement de 5 opérations: l'addition, la soustraction, la multiplication et la division. Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Dans ce qui va suivre, nous allons d'abord parler des procédés d'arrondis des résultats, applicables à toutes les opérations, avant de poursuivre par l'étude des opérations simples (multiplications, divisions, racines carrées), avant de terminer avec les opérations compliquées (addition et soustraction).

La normalisation et les arrondis flottants modifier

Normalisation in circuit

Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.

La prénormalisation modifier

La prénormalisation gère le bit implicite. Lorsqu'un circuit de calcul fournit son résultat, celui-ci n'a pas forcément son bit implicite à 1. On est obligé de décaler la mantisse du résultat de façon à ce que ce soit le cas. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés avant le 1 de poids fort, avec un circuit spécialisé. Ce circuit permet aussi de détecter si la mantisse vaut zéro.

Mais si on décale notre résultat de n rangs, cela signifie qu'on le multiplie par 2 à la puissance n. Il faut donc corriger l'exposant du résultat pour compenser le décalage de la mantisse. Il suffit pour cela de lui soustraire n, le nombre de rangs dont on a décalé la mantisse.

Circuit de prénormalisation.

La normalisation modifier

Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit de normalisation. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.

Circuit d'arrondi flottant basé sur une ROM.

Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.

Circuit de postnormalisation.

résumé modifier

Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :

Circuit de normalisation-arrondi

La multiplication flottante modifier

Prenons deux nombres flottants de mantisses et et les exposants et . Leur multiplication donne :

On regroupe les termes :

On simplifie la puissance :

En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants.

Il faut cependant penser à plusieurs choses pas forcément évidentes.

  • Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier. Sans cela, la mantisse résultat sera tout simplement fausse.
  • Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait que l'additionneur-soustracteur utilisé est un additionneur-soustracteur spécifiques à cette représentation.
  • Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
Multiplieur flottant avec normalisation

La division flottante modifier

La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.

Pour le démontrer, prenons deux flottants et et divisons le premier par le second. On a alors :

On applique les règles sur les fractions :

On simplifie la puissance de 2 :

On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.

La racine carrée flottante modifier

Le calcul de la racine carrée d'un flottant est relativement simple. Par définition, la racine carrée d'un flottant vaut :

La racine d'un produit est le produit des racines :

Vu que , on a :

On voit qu'il suffit de calculer la racine carrée de la mantisse et de diviser l'exposant par deux (ou le décaler d'un rang vers la droite ce qui est équivalent). Voici le circuit que cela doit donner :

Racine carrée FPU

L'addition et la soustraction flottante modifier

La somme de deux flottants n'est simplifiable que quand les exposants sont égaux : dans ce cas, il suffit d'additionner les mantisses. Il faut donc mettre les deux flottants au même exposant, l'exposant choisi étant souvent le plus grand exposant des deux flottants. Convertir le nombre dont l'exposant est le plus petit demande de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants. Pour comprendre pourquoi, il faut se souvenir que décaler vers la droite diminuer l'exposant du nombre de rangs. Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes (histoire d'envoyer le plus petit exposant dans le décaleur). Ce circuit d'échange est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.

Circuit de mise au même exposant.

Suivant les signes, il faudra additionner ou soustraire les opérandes.

Crcuit d'addition et de soustraction flottante.

Voici le circuit complet :

Additionneur flottant - circuit complet

Les fonctions trigonométriques modifier

L'implémentation des fonctions trigonométriques est quelque peu complexe, du moins pour ce qui est de créer des circuits de calcul du sinus, cosinus, tangente, etc. S'il est possible d'utiliser une mémoire à interpolation, la majorité des processeurs actuels réalise ce calcul à partir d'une suite d'additions et de multiplications, qui donne le même résultat. Cette suite peut être implémentée via le logiciel, un petit bout de programme s'occupant de faire les calculs. Il est aussi possible, bien que nettement plus rare, d'implémenter ce bout de logiciel directement sous la forme de circuits : la boucle de calcul est remplacée par un circuit séquentiel. Mais il faut avouer que cette solution n'est pas pratique et que faire les calculs au niveau logiciel est nettement plus simple, tout aussi performant (et oui !) et moins coûteux. La tactique habituelle consiste à utiliser une approximation de Taylor, largement suffisante pour calculer la majorité des fonctions trigonométriques.

Implémentation matérielle naïve et inefficace d'un calcul de sinus par série de Taylor.

Les flottants logarithmiques modifier

Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre Le codage des nombres, dans la section sur les flottants logarithmiques. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.

Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.

La multiplication et la division de deux flottants logarithmiques modifier

Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.

Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par , c'est multiplier par . Or, il faut se rappeler que . On obtient alors, en combinant ces deux expressions :

La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.

L'addition et la soustraction de deux flottants logarithmiques modifier

Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.

Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :

Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :

Pour rappel, les représentations de x et y en flottant logarithmique sont égales à et . En notant ces dernières et , on a :

Par définition, et . En injectant dans l'équation précédente, on obtient :

On simplifie la puissance de deux :

On a donc :

, avec f la fonction adéquate.

Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :

, avec g une fonction différente de f.

On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :

  • un circuit qui additionne/soustrait les deux opérandes ;
  • une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
  • et un autre additionneur pour le résultat.

Résumé modifier

Pour implémenter les quatre opérations, on a donc besoin :

  • de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
  • de deux autres additionneurs/soustracteur pour la multiplication et la division ;
  • et d'une ROM.

Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.

Unité de calcul logarithmique


Les comparateurs sont des circuits qui permettent de comparer deux nombres, à savoir s'ils sont égaux, si l'un est supérieur à l'autre, ou inférieur, ou différent, etc. Dans ce qui va suivre, nous allons voir quatre types de comparateurs. Le premier type de circuit vérifie si un nombre est nul ou différent de zéro. Le second vérifie si deux nombres sont égaux, tandis que le troisième vérifie s'ils sont différent. Enfin, le quatrième vérifie si un nombre est supérieur ou inférieur à un autre. Il faut signaler que nous avons déjà vu comment vérifier qu'un nombre est égal à une constante dans le chapitre sur les circuits combinatoires, aussi nous ne reviendrons pas dessus.

Le comparateur de 1 bit modifier

Comparateur 1 bit.

Maintenant, nous allons voir comment vérifier si deux bits sont égaux/différents, si le premier est inférieur au second ou supérieur. Le but est de créer un circuit qui prend en entrée deux bits A et B, et fournit en sortie quatre bits :

  • qui vaut 1 si le bit A est supérieur à B et 0 sinon ;
  • qui vaut 1 si le bit A est inférieur à B et 0 sinon ;
  • qui vaut 1 si le bit A est égal à B et 0 sinon ;
  • qui vaut 1 si le bit A est différent de B et 0 sinon.

Peut-être avez-vous remarqué l'absence de sortie qui indique si ou . Mais ces deux sorties peuvent se calculer en combinant la sortie avec les sorties et . De plu, vous pouvez remarquer que les deux dernières entrées sont l'inverse l'une de l'autre, ce qui n'est pas le cas des deux premières. Nous allons d'abord voir comment calculer les deux premières sorties, à savoir et . La raison à cela est que les deux autres sorties peuvent se calculer à partir de celles-ci.

Le comparateur de supériorité/infériorité stricte modifier

En premier lieu, nous allons voir comment vérifier qu'un bit A est strictement supérieur ou inférieur à un bit B. Pour cela, le mieux est d'établir la table de vérité des deux comparaisons. La table de vérité est la suivante :

Entrée A Entrée B Sortie < Sortie >
0 0 0 0
0 1 1 0
1 0 0 1
1 1 0 0

On obtient les équations suivantes :

  • Sortie > :
  • Sortie < :

On voit que quand les deux bits sont égaux, alors les deux sorties sont à zéro.

Le comparateur d'inégalité 1 bit modifier

La seconde étape est de concevoir un petit circuit qui vérifie si deux bits sont différents, inégaux. Pour cela, on peut combiner les deux signaux précédents. En effet, on sait que si A et B sont différents, alors soit A>B, soit A<B. En conséquence, on a juste à combiner les deux signaux vus précédemment avec une porte OU.

Comparateur d'inégalité 1 bit.

Ce circuit devrait vous dire quelque chose. Pour ceux qui n'ont pas déjà remarqué, le mieux est d'établir la table de vérité du circuit, ce qui donne :

Entrée 1 Entrée 2 Sortie
0 0 0
0 1 1
1 0 1
1 1 0

On voit rapidement qu'il s'agit d'une porte XOR.

Le comparateur d'égalité 1 bit modifier

Enfin, nous allons concevoir un petit circuit qui vérifie si deux bits sont égaux. La logique veut que l'égalité est l'inverse de l'inégalité, ce qui fait qu'il suffirait d'ajouter une porte NON en sortie du circuit. Le circuit précédent est alors facile à adapter : il suffit de remplacer la porte OU par une porte NOR. Et si le comparateur d'inégalité 1 bit était une porte XOR, on devine rapidement que le comparateur d'égalité 1 bit est quant à lui une porte NXOR. Ce qui se vérifie quand on regarde la table de vérité du comparateur :

A B A=B
0 0 1
0 1 0
1 0 0
1 1 1

L'interprétation de ce circuit est assez simple. Par définition, deux nombres sont égaux si les deux conditions et sont fausses. C’est pour cela que le circuit calcule d'abord les deux signaux et , puis qu'il les combine avec une porte NOR.

Le circuit complet modifier

Fort de ces résultats, nous pouvons fabriquer un comparateur de 1 bit. Celui-ci prend deux bits en entrée, et fournit trois bits en sortie : un pour l'égalité, un autre pour la supériorité et un autre pour l'infériorité. Voici le circuit au complet (sans le bit de différence). On pourrait rajouter le bit de différence assez simplement, en ajoutant une porte OU en sortie des signaux et , mais nous le ferons pas pour ne pas surcharger inutilement les schémas.

Comparateur de magnitude 1 bit.
Comparateur de magnitude 1 bit, alternatif.

Il est aussi possible de reformuler le schéma précédent pour supprimer une redondance invisible dans le circuit, au niveau de la porte NXOR qui calcule le bit d'égalité. À la place, il est possible de prendre les signaux et et de les combiner avec une porte NOR. Ou encore mieux : on utilise une porte OU et une porte NON, la sortie de la porte OU donnant directement le bit d'inégalité, celle de la porte NON donnant le bit d'égalité. Cela économise quelques transistors, mais rallonge un petit peu le circuit.

Comparateur 1 bit complet

Les comparateurs pour entiers non-signés modifier

Comparateur 4 Bits.

Dans ce qui va suivre, nous allons créer un circuit qui est capable de vérifier l'égalité, la supériorité ou l'infériorité de deux nombres. Celui-ci prend deux nombres en entrée, et fournit trois bits : un pour l'égalité des deux nombres, un autre pour préciser que le premier est supérieur au second, et un autre pour préciser que le premier est inférieur au second. Il existe deux méthodes pour créer des comparateurs : une basée sur une soustraction, l'autre sur des comparaisons bit par bit. Dans cette section, nous allons nous concentrer sur le second type, les comparateurs basés sur une soustraction étant vu à la fin du chapitre. De plus, nous allons nous concentrer sur des comparateurs qui testent deux opérandes non-signées (nulles ou positives, mais pas négatives).

Les comparateurs sériels modifier

La manière la plus simple est de faire la comparaison bit par bit avec un circuit séquentiel. Pour cela, on doit utiliser deux registres à décalage pour les opérandes. Les bits sortants à chaque cycle sont les bits de même poids et ils sont comparés par un comparateur 1 bit. Le résultat de la comparaison est mémorisé dans une bascule de quelques bits, qui mémorise le résultat temporaire de la comparaison. A chaque cycle, le comparateur de 1 bit fournit son résultat, qu'il faut combiner avec celui dans la bascule, ce qui permet de combiner les résultats temporaire avec les résultats de la colonne en cours de traitement. Et c'est une chose que le comparateur 1 bit précédent ne sait pas faire. Pour cela, nous allons devoir créer un circuit, nommé le comparateur complet.

Comparateur sériel.

Dans ce qui va suivre, la comparaison va s'effectuer en partant du bit de poids faible et se fera de gauche à droite, chose qui a son importance pour ce qui va suivre. Quel que soit le sens de parcours, on sait comment calculer le bit d'égalité : celui-ci est égal à un ET entre le bit d'égalité fournit en entrée, et le résultat de la comparaison A = B. Pour les bits de supériorité et d'infériorité, les choses changent : la colonne en cours de traitement supplante l'autre colonne. Dit autrement, les bits de poids fort donnent le résultat de la comparaison. Pour déterminer comment calculer ces bits, le mieux est encore d'établir la table de vérité du circuit pour ces deux bits. Nous allons postuler que les bits A > B et A < B ne peuvent être à 1 en même temps : il s'agit de supériorité et d'infériorité stricte. Voici la table de vérité :

Entrée > Entrée < A > B A < B Sortie > Sortie <
0 0 0 0 0 0
0 0 0 1 0 1
0 0 1 0 1 0
0 0 1 1 X X
0 1 0 0 0 1
0 1 0 1 0 1
0 1 1 0 1 0
0 1 1 1 X X
1 0 0 0 1 0
1 0 0 1 0 1
1 0 1 0 1 0
1 0 1 1 X X
1 1 0 0 X X
1 1 0 1 X X
1 1 1 0 X X
1 1 1 1 X X

Les équations logiques obtenues sont donc les suivantes :

  • Sortie (=) :
  • Sortie (>) :
  • Sortie (<) :

Il est possible de simplifier le circuit de manière à ce qu'il ne fasse pas la comparaison d'égalité, qui se déduit des deux autres comparaisons. Si les deux sorties < et > sont à 0, alors les deux bits sont égaux. Si on avait utilisé des sorties >= et <=, il aurait fallu que les deux bits soient à 1 pour montrer une égalité.

On peut aussi fabriquer un comparateur d'égalité sériel. Nous aurons à utiliser ce circuit plusieurs fois dans la suite du cours, notamment dans le chapitre sur les mémoires associatives. Le circuit est strictement identique au précédent, si ce n'est que l'on retire les portes pour les comparaisons non-voulues, et que la bascule n'a besoin que de mémoriser un seul bit. La comparaison d'égalité est réalisée par une porte NXOR, et le résultat est combiné avec le contenu de la bascule avec une porte ET.

Comparateur d'égalité sériel.

Les comparateurs parallèles modifier

Il est possible de dérouler le circuit précédent, de la même manière que l'on peut dérouler un additionneur sériel pour obtenir un additionneur à propagation de retenue. L'idée est de traiter les bits les uns après les autres, chacun avec un comparateur complet par colonne. Les comparateurs sont connectés de manière à traiter les opérandes dans un sens bien précis : soit des bits de poids faible vers ceux de poids fort, soit dans le sens inverse. Tout dépend du comparateur. Un tel circuit n'a pas l'air très optimisé et ne paye pas de mine, mais il a déjà été utilisé dans des processeurs commerciaux. Par exemple, c'est ce circuit qui était utilisé dans le processeur HP Nanoprocessor, un des tout premiers microprocesseurs.

Comparateur série

Un inconvénient de cette méthode est que le résultat est lent à calculer, car on doit traiter les opérande colonne par colonne, comme pour les additionneurs. Sur ces derniers, il fallait propager une retenue de colonne en colonne, mais diverses optimisations permettaient d'optimiser le tout, soit en accélérant la propagation de la retenue, soit en l’anticipant, soit en faisant les calculs de retenue en parallèle, etc. Pour les comparateurs, ce n'est pas une retenue qui se déplace, mais des résultats temporaires. Sauf qu'il n'y a pas de moyen simple pour faire comme avec les additionneurs/soustracteurs et d'anticiper ou calculer en parallèle ces résultats temporaires.

Il est cependant possible de faire des calculs en parallèle, mais en réduisant fortement les fonctionnalités du circuit. Notamment, les circuits qui vérifient si deux nombres sont égaux ou différents sont beaucoup plus simples que les autres. Ils sont juste composés de deux couches de portes logiques, et non de comparateurs enchainés en série. C'est la raison pour laquelle nous allons voir ces deux types de comparateurs ensemble. Les deux sont basés sur le même modèle et ont une structure similaire. Ils sont composés d'une couche de comparateurs de 1 bits, suivi par une porte logique à plusieurs entrées. La couche de comparateur 1 bits vérifie si les bits de même poids des deux opérandes sont différents ou identiques. La porte logique combine les résultats de ces comparaisons individuelles et en déduit le bit de sortie. Nous allons aussi voir un autre comparateur parallèle très spécialisé, qui vérifie si l'opérande d'entrée vaut zéro.

Le comparateur d'égalité modifier

Pour des entiers non-signés, ainsi que pour les entiers en complément à deux, l'égalité de deux nombres se détermine assez facilement. Deux nombres sont égaux s'ils ont la même représentation en binaire, ils sont différents sinon. Notons que cela ne marche pas dans les autres représentations pour les entiers signés. La raison est la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence sont donc légèrement différents suivant la représentation des nombres utilisée. La règle qui veut que deux nombres soient égaux s'ils ont la même représentation en binaire, s'ils sont codés par la même suite de bit, implique qu'un nombre n'est codé que d'une seule manière, qu'il n'y a pas de redondance. C'est le cas en binaire non-signé et en complément à deux, qui arrivent à coder un maximum de valeurs pour un nombre de bits déterminé, ce qui fait qu'elles ont le bon gout de ne pas avoir de redondance. Mais d'autres représentations volontairement redondantes violent cette règle.

Le circuit qui vérifie si deux nombres sont égaux est très simple : il prend les bits de même poids des deux opérandes (ceux à la même position, sur la même colonne) et vérifie qu'ils sont égaux. Le circuit est donc composé d'une première couche de portes NXOR qui vérifie l'égalité des paires de bits, suivie par une porte logique qui combine les résultats des porte NXOR. La logique dit que la sortie vaut 1 si tous les bits de sortie des NXOR valent 1. La seconde couche est donc, par définition, une porte ET à plusieurs entrées.

Comparateur d'égalité.

Une autre manière de concevoir ce circuit inverse la logique précédente. Au lieu de tester si les paires de bits sont égales, on va tester si elles sont différentes. On teste les bits de même poids et la moindre différence entre deux bits entraîne l'inégalité. Pour tester la différence de deux bits, une porte XOR suffit. Pour combiner les résultats des portes XOR, on utilise une porte NOR. En effet, le fait qu'une seule paire soit différente suffit à rendre les deux nombres inégaux. Dit autrement, si une porte XOR sort un 1, alors la sortie du comparateur d'égalité doit être de 0 : c'est le fonctionnement d'une porte NOR.

Notons qu'on peut passer d'un circuit à l'autre en utilisant la loi de de Morgan

Le circuit qui vérifie si deux nombres sont différents peut être construit à partir du circuit précédent et d'inverser son résultat avec une porte NON. Le résultat est que l'on change la porte ET finale du premier circuit par une porte NAND, la porte NOR du second circuit par une porte OU.

Le comparateur avec zéro, un circuit simple utilisé pour fabriquer d'autres comparateurs modifier

Le circuit que nous allons maintenant aborder ne compare pas deux nombres, mais vérifie si l'opérande d'entrée est nulle. Il fonctionne sur un principe simple : un nombre est nul si tous ses bits valent 0. Du moins, c'est le cas pour les entiers non-signés ou en complément à deux.

La solution la plus simple pour créer ce circuit est d'utiliser une porte NOR à plusieurs entrées. Par définition, la sortie d'une NOR vaut zéro si un seul bit de l'entrée est à 1 et 0 sinon, ce qui répond au cahier des charges.

Porte NOR utilisée comme comparateur avec zéro.

Il existe une autre possibilité strictement équivalente, qui inverse l'entrée avant de vérifier si tous les bits valent 1. Si l'entrée est nulle, tous les bits inversés valent tous 1, alors qu'une entrée non-nulle donnera au moins un bit à 0. Le circuit est donc conçu avec des portes NON pour inverser tous les bits, suivies d'une porte ET à plusieurs entrées qui vérifie si tous les bits sont à 1.

Circuit compare si l'opérande d'entrée est nulle.
Notons qu'en peut passer d'un circuit à l'autre en utilisant les lois de de Morgan.

Le circuit précédent marche parfaitement pour les entiers non-signés, mais aussi pour ceux codés en complément à deux.

Combiner plusieurs comparateurs modifier

Il est possible de combiner plusieurs comparateurs simples pour traiter des nombres assez longs. Par exemple, on peut enchainer 2 comparateurs série 4 bits pour obtenir un comparateur série 8 bits. Pour cela, il y a deux grandes solutions : soit on enchaine les comparateurs en série, soit on les fait travailler en parallèle.

La première méthode place les comparateurs en série, c'est à dire que le second comparateur prend le résultat du premier pour faire son travail. Le second comparateur doit avoir trois entrées nommées <, > et =, qui fournissent le résultat du comparateur précédent. On peut faire la même chose avec plus de 2 comparateurs, l'essentiel étant que les comparateurs se suivent.

Interface d'un comparateur parallèle avec "retenues".

Une autre solution est de faire les comparaisons en parallèle et de combiner les bits de résultats des différents comparateurs avec un dernier comparateur. Le calcul de la comparaison est alors légèrement plus rapide qu'avec les autres méthodes, mais le circuit devient plus compliqué à concevoir.

Comparateur entier parallèle.

Les comparateurs pour nombres signés modifier

Comparer deux nombres signés n'est pas la même chose que comparer deux nombres non-signés. Autant la comparaison d'égalité et de différence est strictement identique, autant les comparaisons de supériorité et d'infériorité ne le sont pas. Les comparateurs pour nombres signés sont naturellement différents des comparateurs vus précédemment. On verra plus tard qu'il en est de même avec les comparateurs pour nombres flottants. Il fut aussi faire attention : la comparaison ne s'effectue pas de la même manière selon que les nombres à comparer sont codés en signe-magnitude, en complément à deux, ou dans une autre représentation. Dans ce qui va suivre, nous allons aborder la comparaison en signe-magnitude et en complément à deux, mais pas les autres représentations.

Le comparateur signe-magnitude modifier

Un comparateur en signe-magnitude n'est pas trop différent d'un comparateur normal. Effectuer une comparaison entre deux nombres en signe-magnitude demande de comparer les valeurs absolues, ainsi que comparer les signes. Une fois cela fait, il faut combiner les résultats de ces deux comparaisons en un seul résultat.

Comparateur en signe-magnitude

Comment comparer les signes ? modifier

Comparer les signes est relativement simple : le circuit n'ayant que deux bits à comparer, celui-ci est naturellement simple à concevoir. On pourrait penser que l'on peut réutiliser le comparateur 1 bit vu précédemment dans ce chapitre, mais cela ne marcherait pas ! En effet, un nombre positif a un bit de signe nul alors qu'un nombre négatif a un bit de signe égal à 1 : les résultats sont inversés. Un bon moyen de s'en rendre compte est d'écrire la table de vérité de ce circuit, selon les signes des nombres à comparer. Nous allons supposer que ces deux nombres sont appelés A et B.

Bit de signe de A Bit de signe de B Sortie = Sortie < Sortie >
0 (+) 0 (+) 1 0 0
1 (-) 0 (+) 0 0 1
0 (+) 1 (-) 0 1 0
1 (-) 1 (-) 1 0 0

On obtient les équations suivantes :

  • Sortie = :
  • Sortie < :
  • Sortie > :

On peut remarquer que si le bit d'égalité est identique au comparateur 1 bit vu plus haut, les bits de supériorité et l'infériorité sont inversés, échangés. On peut donc réutiliser le comparateur à 1 bit, mais en intervertissant les deux sorties de supériorité et d'infériorité.

Comment combiner les résultats ? modifier

Une fois qu'on a le résultat de la comparaison des signes, on doit combiner ce résultat avec le résultat de la comparaison des valeurs absolues. Pour cela, on doit rajouter un circuit à la suite des deux comparateurs. On pourrait penser qu'il suffit d'établir la table de vérité de ce comparateur, mais il faut faire attention au cas où les deux opérandes sont nulles : dans ce cas, peut importe les signes, les deux opérandes sont égales. Pour le moment, mettons ce fait de côté et établissons la table de vérité du circuit. Dans tous les cas, la comparaison des bits de signe prend le pas sur la comparaison des valeurs absolues. Par exemple, 2 est supérieur à -255, bien que ce ne soit pas le cas pour les valeurs absolues. Ce fait n'est que l'expression du fait qu'un nombre positif est systématiquement supérieur à un négatif, de même qu'un négatif est systématiquement inférieur à un positif. Ce n'est que quand les deux bits de signes sont égaux que la comparaison des valeurs absolue est à prendre en compte. Cela devrait donner les équations suivantes. On note les résultats de la comparaison des bits de signe comme suit : , alors que les résultats de la comparaison des valeurs absolues sont notés : .

  • pour le résultat d'égalité ;
  • pour l’infériorité ;
  • pour la supériorité.

Néanmoins, il faut prendre en compte le cas où les deux opérandes sont nulles, ce qui complique un petit peu les équations Pour cela, il fat rajouter une sortie au comparateur de valeurs absolues, qui indique si les deux nombres valent tous deux zéro. Notons cette sortie . Les équations deviennent :

  • pour l'égalité ;
  • pour l’infériorité ;
  • pour la supériorité.

Les comparateurs en complément à un et en complément à deux modifier

La comparaison en complément à un ou à deux peut s'implémenter comme en signe-magnitude : on compare les valeurs absolues, puis les signes, avant de déterminer le résultat. Reste à calculer les valeurs absolues, ce qui est loin d'être simple.

Circuit de comparaison en complément à 1 et à 2

En complément à 1, il suffit d'inverser tous les bits si le bit de signe est de 1. E, complément à deux, il faut faire pareil, puis incrémenter le résultat.

Circuit de comparaison entière en complément à 1.

Les comparateurs basés sur un soustracteur modifier

Les comparateurs présents dans les ordinateurs modernes fonctionnent sur un principe totalement différents. Ils soustraient les deux opérandes et étudient le résultat. Le résultat peut avoir quatre propriétés intéressantes lors d'une comparaison : si le résultat est nul ou non, son bit de signe, s'il génère une retenue sortante (débordement entier non-signé) et s'il génère un débordement entier en complément à deux. Ces quatre propriétés sont extraites du résultat, et empaquetées dans 4 bits, appelés les 4 bits intermédiaires. En effectuant quelques opérations sur ces 4 bits intermédiaires, on peut déterminer toutes les conditions possibles : si la première opérande est supérieure à l'autre, si elle est égale, si elle est supérieure ou égale, etc. Le circuit est donc composé de trois sous-circuits : le soustracteur, un circuit qui calcule les 4 bits intermédiaires, puis un autre qui calcule la ou les conditions voulues.

L'avantage de ce circuit est qu'il est plus rapide que les autres comparateurs. Rappelons que les comparateurs parallèles testent les opérandes colonne par colonne, et qu'on a pas de moyens simples pour éviter cela. Avec un soustracteur, on peut utiliser des techniques d'anticipation de retenue pour accélérer les calculs. Les calculs sont donc faits en parallèle, et non colonne par colonne, ce qui est beaucoup plus rapide, surtout quand les opérandes sont un peu longues. Ce qui fait que tous les processeurs modernes utilisent le circuit suivant pour faire des comparaisons. Il faut dire qu'il n'a que des avantages : plus rapide, il utilise un peu plus de portes logiques mais cela reste parfaitement supportable, il permet de générer des bits intermédiaires utiles que les autres comparateurs peinent à fournir, etc. De plus, il peut comparer aussi bien des entiers codés en complément à deux que des entiers non-signés, là où les comparateurs précédents ne le permettaient pas.

La génération des conditions modifier

La génération des 4 bits intermédiaire est simple et demande peu de calculs. Déterminer si le résultat est nul demande juste d'utiliser un comparateur avec zéro, qui prend en entrée le résultat de la soustraction. La génération des bits de débordement a été vue dans le chapitre sur les circuits d'addition et de soustraction, aussi pas besoin de revenir dessus. Par contre, il est intéressant de voir comment les 4 bits intermédiaires sont utilisés pour générer les conditions voulues.

Déjà, les deux opérandes sont égales si le résultat de leur soustraction est nul, et elles sont différentes sinon. Le bit qui indique si le résultat est nul ou non indique si les deux opérandes ont égales ou différentes. Cela fait déjà deux conditions de faites. De plus, l'égalité et la différence se calculent de la même manière en complément à deux et avec des entiers non-signés.

Pour ce qui est de savoir quelle opérande est supérieure ou inférieure à l'autre, cela dépend de si on analyse deux entiers non-signés ou deux entiers en complément à deux.

  • Avec des entiers non-signés, il faut regarder si la soustraction génère une retenue sortante, un débordement entier non-signé. Si c'est le cas, alors la première opérande est inférieure à la seconde. Si ce n'est pas le cas, alors la première opérande est supérieure ou égale à la seconde.
  • Pour la comparaison en complément à deux on se dit intuitivement qu'il faut regarder le signe du résultat. L'intuition nous dit que la première opérande est inférieure à l'autre si le résultat est négatif, qu'elle est supérieure ou nulle s'il est positif. Sauf que cela ne marche pas exactement comme cela : il faut aussi regarder si le calcul entraine un débordement d'entier en complément à deux. La règle est que le résultat est négatif si on a un débordement d'entier en complément à deux OU que le bit de signe est à 1. Une seule des deux conditions doit être respectée : le résultat est positif sinon. On doit donc faire un XOR entre le bit de débordement et le bit de signe. Le bit sortant de la porte XOR indique que le résultat de la soustraction est négatif, et donc que la première opérande est inférieure à la seconde.
Circuit de comparaison entière en complément à 2.

Avec ces deux informations, égalité et supériorité, on peut déterminer toutes les autres. Dans ce qui va suivre, nous allons partir du principe que le soustracteur est suivi par des circuits qui calculent les bits suivants :

  • un bit S qui n'est autre que le bit de signe en complément à deux ;
  • un bit Z qui indique si le résultat est nul ou non (1 pour un résultat nul, 0 sinon) ;
  • un bit C qui n'est autre que la retenue sortante (1 pour un débordement d'entier non-signé, 0 sinon) ;
  • un bit D qui indique un débordement d'entier signé (1 pour un débordement d'entier signé, 0 sinon).
Condition testée Calcul
A == B Z = 1
A != B Z = 0
Opérandes non-signées
A < B C = 1
A >= B C = 0
A <= B C OU Z = 1
A > B C ET Z = 0
Opérandes en complément à deux
A < B S XOR D = 1
A >= B S XOR D = 0
A <= B (S XOR D) OU Z = 1
A > B (S XOR D) ET Z = 0

Le comparateur-soustracteur total modifier

Il est possible de raffiner ce circuit, de manière à pouvoir choisir la condition en sortie du circuit. L'idée est que le circuit possède une seule sortie, sur laquelle on a le résultat de la condition choisie. Le circuit possède une entrée sur laquelle on envoie un nombre, qui permet de choisir la condition à tester. Par exemple, on peut vouloir vérifier si deux nombres sont égaux. Dans ce cas, on configure le circuit en envoyant sur l'entrée de sélection le nombre qui correspond au test d'égalité. Le circuit fait alors le test, et fournit le résultat sur la sortie de 1 bit. Ce circuit se construit simplement à partir du circuit précédent, en ajoutant un multiplexeur. Concrètement, on prend un soustracteur, on ajoute des circuits pour calculer les bits intermédiaires, puis on ajoute des portes pour calculer les différentes conditions, et enfin, on ajoute un multiplexeur.

Calcul d'une condition pour un branchement

Nous réutiliserons ce circuit bien plus tard dans ce cours, dans les chapitres sur les branchements. Mais ne vous inquiétez pas, dans ces chapitres, nous ferons des rappels sur les bits intermédiaires, la manière dont sont calculées les conditions et globalement sur tout ce qui a été dit dans cette section. Pas besoin de mémoriser par coeur les équations de la section précédente, tout ce qui compte est que vous ayez retenu le principe général.


Les autres circuits modifier

On a vu précédemment que divers composants doivent communiquer entre eux. Pour cela, ils sont reliés entre eux par des fils, qui permettent de transmettre des bits. Ces ensembles de bits sont appelés des bus. La transmission de données sur ces bus est rarement parfaite : des interférences électromagnétiques peuvent à tout moment causer des modifications des bits transmis. Par exemple, les transmissions de données via un réseau local ou internet ne sont pas parfaites, même si les câbles réseaux sont conçus pour limiter les erreurs. De même, les registres et autres formes de mémoire ne sont pas parfaites et des bits peuvent s'inverser à tout moment. Pour donner un exemple, on peut citer l'incident du 18 mai 2003 dans la petite ville belge de Schaerbeek. Lors d'une élection, la machine à voter électronique enregistra un écart de 4096 voix entre le dépouillement traditionnel et le dépouillement électronique. La faute à un rayon cosmique, qui avait modifié l'état d'un bit de la mémoire de la machine à voter.

Mais qu'on se rassure : il existe des techniques pour détecter et corriger les erreurs de transmission, ou les modifications non-prévues de données. Pour cela, divers codes de détection et de correction d'erreur ont vu le jour. Tous les codes correcteurs et détecteurs d'erreur ajoutent tous des bits aux données de base, ces bits étant appelés des bits de correction/détection d'erreur. Ils sont calculés à partir des données à transmettre/stocker. Ces bits servent à détecter et éventuellement corriger toute erreur de transmission/stockage. Plus le nombre de bits ajoutés est important, plus la fiabilité des données sera importante. Voyons plus en détail comment ceux-ci font.

Il existe plusieurs types de codes correcteurs d'erreurs, mais on peut les classer en deux types : les codes par blocs et les codes continus. Dans les premiers, les bits de correction/détection d'erreur sont ajoutés à la fin des données à transmettre. Pour les seconds, les bits de correction/détection d'erreur sont insérés directement dans les données à transmettre, à des emplacements bien précis. Les premiers sont conceptuellement plus simples, ce qui les rend plus simples à comprendre. Aussi, nous allons nous limiter à aux codes par blocs classiques dans ce chapitre.

Le bit de parité modifier

Nous allons commercer par aborder le bit de parité/imparité, une technique utilisée dans un grand nombre de circuits électroniques, comme certaines mémoires RAM, ou certains bus (RS232, notamment). Le bit de parité est ajouté aux bits à stocker. Avec ce bit de parité, le nombre stocké (bit de parité inclus) contient toujours un nombre pair de bits à 1. Ainsi, le bit de parité vaut 0 si le nombre contient déjà un nombre pair de 1, et 1 si le nombre de 1 est impair.

Si un bit s'inverse, quelle qu'en soit la raison, la parité du nombre total de 1 est modifié : ce nombre deviendra impair si un bit est modifié. Et ce qui est valable avec un bit l'est aussi pour 3, 5, 7, et pour tout nombre impair de bits modifiés. Mais tout change si un nombre pair de bit est modifié : la parité ne changera pas. Il permet de détecter des corruptions qui touchent un nombre impair de bits. Si un nombre pair de bit est modifié, il est impossible de détecter l'erreur avec un bit de parité. Ainsi, on peut vérifier si un bit (ou un nombre impair) a été modifié : il suffit de vérifier si le nombre de 1 est impair. Il faut noter que le bit de parité, utilisé seul, ne permet pas de localiser le bit corrompu.

Le bit d'imparité est similaire a bit de parité, si ce n'est que le nombre total de bits doit être impair, et non pair comme avec un bit de parité. Sa valeur est l'inverse du bit de parité du nombre : quand le premier vaut 1, le second vaut 0, et réciproquement. Mais celui-ci n'est pas meilleur que le bit de parité : on retrouve l'impossibilité de détecter une erreur qui corrompt un nombre pair de bits.

Reste que ce bit de parité doit être calculé, avec un circuit dédié qui prend un nombre et renvoie sur sa sortie le bit de parité.

Le calcul via le poids de Hamming modifier

Une première méthode est tout simplement de commencer par calculer le nombre de 1 dans le nombre, avant de calculer sa parité et d'en déduire le bit de parité. Le calcul du nombre de 1 dans un nombre est une opération tellement courante qu'elle porte un nom : on l'appelle la population count, ou encore poids de Hamming. Son calcul est assez simple : si on découpe un nombre en deux parties, la population count du nombre est la somme des population count de chaque partie. Il est possible d'appliquer ce raisonnement de manière récursive sur chaque morceau, jusqu'à réduire chaque morceau 1 bit. Or, la population count d'un bit est égale au bit lui-même, par définition. On en déduit donc comment construire le circuit : il suffit d'utiliser une série d'additionneurs enchainés en arbre.

Un fois la population count du nombre obtenue, il faut en déduire la parité. Ce qui est très simple : la parité d'un nombre est tout simplement égal au bit des unités, le bit de poids faible ! Le nombre est impair si ce bit est à 1, tandis que le nombre est pair si ce bit vaut 0. Cette parité étant égale au bit de parité, on sait calculer le bit de parité : il faut utiliser le circuit de population count, et prendre le bit le plus à droite : ce bit est le bit de parité.

Les générateurs de parité modifier

Il est cependant possible d'obtenir un circuit plus simple, qui utilise nettement moins de portes logiques et qui est plus rapide. Pour comprendre comment créer ce circuit, nous allons commencer avec un cas simple : le calcul à partir d'un nombre de 2 bits. Le circuit étant simple, il suffit d'utiliser les techniques vues précédemment, avec le tables de vérité. En écrivant la table de vérité du circuit, on remarque rapidement que la table de vérité donne la table de vérité d'une porte XOR.

Bit 1 Bit 2 Bit de parité
0 0 0
0 1 1
1 0 1
1 1 0

Pour la suite, nous allons partir d'un nombre de trois bits. On pourrait tenter de créer ce circuit à partir d'une table de vérité, mais nous allons utiliser une autre méthode, qui nous donnera un indice important. Ce nombre de 3 bits est composé d'un nombre de 2 bits auquel on a jouté un troisième bit. L'ajout de ce troisième bit modifie naturellement le bit de parité du nombre précédent. Dans ce qui va suivre, nous allons créer un circuit qui calcule le bit de parité final, à partir : du bit de parité du nombre de 2 bits, et du bit ajouté. On voit alors que la table de vérité est celle d'une porte XOR.

Bit de parité précédent Bit ajouté Bit de parité final
0 0 0
0 1 1
1 0 1
1 1 0

Chose assez intéressante, ce mécanisme fonctionne quel que soit le nombre de bits du nombre auquel on ajoute un bit. Ajouter un bit à un nombre modifie sa parité, celle-ci état alors égale à : bit ajouté XOR bit-parité du nombre. L’explication est relativement simple : ajouter n 0 ne modifie pas le nombre de 1, et donc le bit de parité, tandis qu'ajouter un 1 inverse le bit de parité.

Avec cette logique, on peut créer un générateur de parité sériel, un circuit qui calcule le bit de parité bit par bit. Celui-ci est composé d'un registre à décalage, d'une bascule et d'une porte XOR. Le registre à décalage est initialisé avec le nombre dont on veut calculer la parité. La bascule est initialisée à zéro et son but est de conserver le bit de parité calculé à chaque étape. À chaque cycle, un bit de ce nombre sort du registre à décalage et est envoyé en entrée de la porte XOR. La porte XOR fait un XOR entre ce bit et le bit de parité stocké dans la bascule, ce qui donne un bit de parité temporaire. Ce dernier est mémorisé dans la bascule pour être utilisé au prochain cycle.

Générateur de parité sériel

Une autre manière de faire, qui ne se base pas sur un registre à décalage, consiste simplement à faire un XOR entre tous les bits du nombre en enchaînant des portes XOR. Effectué naïvement, il suffit d’enchaîner des portes XOR les unes à la suite des autres. Mais en réfléchissant, on peut faire autrement. Prenons deux nombres et concaténons-les. On peut déduire facilement le bit de parité de la concaténation à partir des bits de parité de chaque nombre : il suffit de faire un XOR entre ces deux bits de parité. On peut donc utiliser un circuit organisé en arbre, comme pour les multiplieurs. Le résultat est appelé un générateur de parité parallèle.

Circuit de parité

Les codes de Hamming modifier

Le code de Hamming se base sur l'usage de plusieurs bits de parité pour un seul nombre. Chaque bit de parité n'est cependant pas calculé en prenant en compte la totalité des bits du nombre : seul un sous-ensemble de ces bits est utilisé pour calculer chaque bit de parité. Chaque bit de parité a son propre sous-ensemble, tous étant différents, mais pouvant avoir des bits en commun. Le but étant que deux sous-ensembles partagent un bit : si ce bit est modifié, cela modifiera les deux bits de parité associés. Et la modification de ce bits est la seule possibilité pour que ces deux bits soient modifiés en même temps : si ces deux bits de parité sont modifiés en même temps, on sait que le bit partagé a été modifié. Pour résumer, un code de Hamming utilise plusieurs bits de parité, calculés chacun à partir de bits différents, souvent partagés entre bits de parité.

Le code 7-4-3 modifier

Hamming(7,4)

Le code de Hamming le plus connu est certainement le code 7-4-3, un code de Hamming parmi les plus simple à comprendre. Celui-ci prend des données sur 4 bits, et leur ajoute 3 bits de parité, ce qui fait en tout 7 bits : c'est de là que vient le nom de 7-4-3 du code. Chaque bit de parité se calcule à partir de 3 bits du nombre. Pour poursuivre, nous allons noter les bits de parité p1, p2 et p3, tandis que les bits de données seront notés d1, d2, d3 et d4. Voici à partir de quels bits de données sont calculés chaque bit de parité :

Bits de parité incorrects Bit modifié
Les trois bits de parité : p1, p2 et p3 Bit d4
p1 et p2 d1
p2 et p3 d3
p1 et p3 d2

Il faut préciser que toute modification d'un bit de donnée entraîne la modification de plusieurs bits de parité. Si un seul bit de parité est incorrect, il est possible que ce bit de parité a été corrompu et que les données sont correctes. Ou alors, il se peut que deux bits de données ont été modifiés, sans qu'on sache lesquels.

Le code 8-4-4 modifier

Le code 8-4-4 est un code 7-4-3 auquel on a ajouté un bit de parité supplémentaire. Celui-ci est calculé à partir de tous les bits, bits de parités ajoutés par le code 7-4-3 inclus. Ainsi, on permet de se prémunir contre une corruption de plusieurs bits de parité.

Hamming(8,4)

Autres modifier

Évidemment, il est possible de créer des codes de Hamming sur un nombre plus grand que bits. Le cas le plus classique est le code 11-7-4.

Hamming(11,7)

Les sommes de contrôle modifier

Les sommes de contrôle sont des techniques de correction d'erreur, où les bits de correction d'erreur sont ajoutés à la suite des données. Les bits de correction d'erreur, ajoutés à la fin du nombre à coder, sont appelés la somme de contrôle. La vérification d'une erreur de transmission est assez simple : on calcule la somme de contrôle à partir des données transmises et on vérifie qu'elle est identique à celle envoyée avec les données. Si ce n'est pas le cas, il y a eu une erreur de transmission.

Techniquement, les techniques précédentes font partie des sommes de contrôle au sens large, mais il existe un sens plus restreint pour le terme de somme de contrôle. Il est souvent utilisé pour regrouper des techniques telle l'addition modulaire, le CRC, et quelques autres. Toutes ont en commun de traiter les données à coder comme un gros nombre entier, sur lequel on effectue des opérations arithmétiques pour calculer les bits de correction d'erreur. La seule différence est que l'arithmétique utilisée est quelque peu différente de l'arithmétique binaire usuelle. Dans les calculs de CRC, on utilise une arithmétique où les retenues ne sont pas propagées, ce qui fait que les additions et soustractions se résument à des XOR.

Le mot de parité modifier

La technique de l'addition modulaire est de loin la plus simple. On peut la voir comme une extension du bit de parité pour plusieurs nombres, ce qui explique que la somme de contrôle est appelée un mot de parité. Elle découpe les données à coder en plusieurs blocs de taille fixe, la somme XOR de tous les blocs donnant la somme de contrôle. Le calcul du mot de parité se calcule en disposant chaque nombre/bloc l'un au-dessus des autres, le tout donnant un tableau dont les lignes sont des nombres Le mot de parité se calcule en calculant le bit de parité de chaque colonne du tableau, et en le plaçant en bas de la colonne. Le résultat obtenu sur la dernière ligne est un octet de parité. Pour comprendre le principe, supposons que nous disposions de 8 entiers de 8 bits. Voici comment effectuer le calcul du mot de parité :

  • 11000010 : nombre ;
  • 10001000 : nombre ;
  • 01001010 : nombre ;
  • 10010000 : nombre ;
  • 10001001 : nombre ;
  • 10010001 : nombre ;
  • 01000001 : nombre ;
  • 01100101 : nombre ;
  • ------------------------------------
  • 10101100 : mot de parité.
Le bit de parité peut être vu comme une spécialisation de la technique du mot de parité, où les blocs font tous 1 bit.

L'avantage de cette technique est qu'elle permet de reconstituer une donnée manquante. Par exemple, dans l'exemple précédent, si une ligne du calcul disparaissait, on pourrait la retrouver à partir du mot de parité. Pour cela, il faut faire faire XOR entre les données non-manquantes et le mot de parité. Pour comprendre pourquoi cela fonctionne, il faut se souvenir que faire un XOR entre un nombre et lui-même donne 0. De plus, le mot de parité est égal au XOR de tous les nombres. Si on XOR un nombre avec le mot de parité, cela va annuler la présence de ce nombre (son XOR) dans le mot de parité : le résultat correspondra au mot de parité des nombres, nombre xoré exclu. Ce faisant, en faisant un XOR avec tous les nombres connus, ceux-ci disparaîtront du mot de parité, ne laissant que le nombre manquant.

On a vu que le bit de parité ne permet pas de savoir quel bit a été modifié. Mais il existe des solutions dans certains cas particuliers. Si on suppose qu'un seul bit a été modifié lors de la transmission des données, on peut localiser le bit modifié. Pour cela, il suffit de coupler un mot de parité avec plusieurs bits de parité, un par nombre. Détecter le bit modifié est simple. Pour comprendre comment, il faut se souvenir que les nombres sont organisés en tableau, avec un nombre par ligne. Le bit modifié est situé à l'intersection d'une ligne et d'une colonne. Sa modification entraînera la modification du bit de parité de la ligne et de la colonne qui correspondent, respectivement un bit de parité sur la verticale, et un bit de parité dans le mot de parité. Les deux bits fautifs indiquent alors respectivement la ligne et la colonne fautive, le bit erroné est situé à l'intersection. Cette technique peut s'adapter non pas avec une disposition en lignes et colonnes, mais aussi avec des dimensions en plus où les nombres sont disposés en cube, en hyper-cube, etc.

Le mot de parité est utilisé sur les bus de communication entre composants, sur lesquels on peut envoyer plusieurs nombres à la suite. Il est aussi utilisé dans certaines mémoires qui stockent plusieurs nombres les uns à côté des autres. Mais surtout, c'est cette technique qui est utilisée sur les disques durs montés en RAID 3, 5 6, et autres. Grâce à elle, si un disque dur ne fonctionne plus, on peut quand même reconstituer les données du disque dur manquant.

Le contrôle de Redondance Cyclique modifier

Une autre méthode consiste diviser les données à envoyer par un nombre entier arbitraire et à utiliser le reste de la division euclidienne comme somme de contrôle. Cette méthode, qui n'a pas de nom, est similaire à celle utilisée dans les Codes de Redondance Cyclique. Avec cette méthode, on remplace la division par une opération légèrement différente. L'idée est de faire comme une division, mais dont on aurait remplacé les soustractions par des opérations XOR. Nous appellerons cette opération une pseudo-division dans ce qui suit. Une pseudo-division donne un quotient et un reste, comme le ferait une division normale. Le calcul d'un CRC pseudo-divise les données par un diviseur et on utilise le reste de la pseudo-division comme somme de contrôle. Il existe plusieurs CRC différents et ils se distinguent surtout par le diviseur utilisé, qui est standardisé pour chaque CRC.

La technique peut sembler bizarre, mais cela marche. Cependant, expliquer pourquoi demanderait d'utiliser des concepts mathématiques de haute volée qui n'ont pas leur place dans ce cours, comme la division polynomiale, les codes linéaire ou encore les codes polynomiaux cycliques. Si nous devons omettre ces développements, nous pouvons cependant dire que les CRC sont faciles à calculer en matériel. Les circuits de calcul de CRC sont ainsi très simples à concevoir : ce sont souvent de simples registres à décalage à rétroaction linéaire améliorés. Le registre en question a la même taille que le mot dont on veut vérifier l'intégrité. Il suffit d'insérer le mot à contrôler bit par bit dans ce registre, et le CRC est calculé au fil de l'eau, le résultat étant obtenu une fois que le mot est totalement inséré dans le registre.

Circuit de calcul d'un CRC-8, en fonctionnement. Le diviseur choisi est égal à 100000111.

Le registre dépend du CRC à calculer, chaque CRC ayant son propre registre.

Circuit de vérification du CRC-8 précédent, en fonctionnement.


De nombreuses situations demandent de générer des nombres totalement aléatoires. C'est très utile dans des applications cryptographiques, statistiques, mathématiques, dans les jeux vidéos, et j'en passe. Ces applications sont le plus souvent logicielles, et cette génération de nombres aléatoire s'effectue avec divers algorithmes plus ou moins efficaces. L'aléatoire dans les jeux vidéos en est un bon exemple : pas besoin d'un aléatoire de qualité, un simple algorithme logiciels suffit. Mais dans certaines situations, il arrive que l'on veuille créer ces nombres aléatoires de manière matérielle. Cela peut servir pour sélectionner une ligne de cache à remplacer lors d'un défaut de cache, pour implémenter des circuits cryptographiques, pour calculer la durée d'émission sur un bus Ethernet à la suite d'une collision, et j'en passe. Mais comment créer une suite de nombres aléatoires avec des circuits ? C'est le but de ce tutoriel de vous expliquer comment !

Les registres à décalage à rétroaction modifier

La première solution consiste à utiliser des registres à décalages à rétroaction, aussi appelés Feedback Shift Registers, abréviés LSFR. Ce genre de circuit donne un résultat assez proche de l'aléatoire, mais on peut cependant remarquer qu'il ne s'agit pas de vrai aléatoire. En effet, un tel circuit est déterministe : pour le même résultat en entrée, il donnera toujours le même résultat en sortie. De plus, ce registre ne peut contenir qu'un nombre fini de valeurs, ce qui fait qu'il finira donc par repasser par une valeur qu'il aura déjà parcourue. Lors de son fonctionnement, le compteur finira donc par repasser par une valeur qu'il aura déjà parcourue, vu que le nombre de valeurs possibles est fini. Une fois qu'il repassera par cette valeur, son fonctionnement se reproduira à l'identique comparé à son passage antérieur. Un LSFR ne produit donc pas de « vrai » aléatoire, vu que la sortie d'un tel registre finit par faire des cycles. Ceci dit, si la période d'un cycle est assez grande, son contenu semblera varier de manière totalement aléatoire, tant qu'on ne regarde pas durant longtemps. Il s'agit d'une approximation de l'aléatoire particulièrement bonne.

Nonlinear-combo-generator

Si les LSFR sont très intéressants, diverses techniques permettent d'améliorer le fonctionnement de ces registres à décalages à rétroaction. Par exemple, on peut décider d'utiliser des LSFR plus compliqués, non linéaires. La fonction appliquée au bit sur l'entrée est alors plus complexe, mais le jeu en vaut la chandelle. Une variante de cette technique consiste à prendre la totalité des bits d'un registre à décalage à rétroaction linéaire (ou affine), et à envoyer ces bits dans un circuit non-linéaire. La différence, c'est que dans ce cas, tous les bits du registre sont pris en compte. Cependant, les techniques les plus efficaces consistent à combiner plusieurs LSFR pour obtenir une meilleure approximation de l'aléatoire. Avec cette technique, plusieurs registres à décalages à rétroaction sont reliés à un circuit combinatoire non-linéaire. Ce circuit prendra en entrée un (ou plusieurs) bit de chaque registre à décalage à rétroaction, et combinera ces bits pour fournir un bit de sortie. Un circuit conçu avec ce genre de méthode va fournir un bit à la fois. Les bits en sortie de ce circuit seront alors accumulés dans un registre à décalage normal, pour former un nombre aléatoire.

Problème : ces circuits ne sont pas totalement fiables : ils peuvent produire plus de bits à 0 que de bits à 1, et des corrections sont nécessaires pour éviter cela. Pour cela, ces circuits de production de nombres aléatoires sont souvent couplés à des circuits qui corrigent le flux de bits accumulé dans le registre pour l'aléatoiriser. Une solution consiste à simplement prendre plusieurs de ces circuits, et d'appliquer un XOR sur les bits fournis par ces circuits : on obtient alors un bit un peu moins biaisé, qu'on peut envoyer dans notre registre à décalage. Pratiquement, des circuits avec trop de bits en entrées sont difficilement concevables.

Pour rendre le tout encore plus aléatoire, il est possible de cadencer nos registres à décalage à rétroaction linéaire à des fréquences différentes. Ainsi, le résultat fourni par notre circuit combinatoire est encore plus aléatoire. Cette technique est utilisée dans les générateurs stop-and-go, alternative step, et à shrinking. Dans le premier, on utilise trois registres à décalages à rétroaction linéaire. Le bit fourni par le premier va servir à choisir lequel de deux restants sera utilisé. Dans le générateur stop-and-go, on utilise deux registres à décalage à rétroaction. Le premier est relié à l'entrée d'horloge du second. Le bit de sortie du second est utilisé comme résultat. Une technique similaire était utilisée dans les processeurs VIA C3, pour l'implémentation de leurs instructions cryptographiques. Dans le shrinking generator, deux registres à décalage à rétroaction sont cadencés à des vitesses différentes. Si le bit de sortie du premier vaut 1, alors le bit de sortie du second est utilisé comme résultat. Par contre, si le bit de sortie du premier vaut 0, aucun bit n'est fourni en sortie, et le bit de sortie du second registre est oublié.

L'aléatoire généré par l'horloge modifier

On vient de voir que les registres à décalage à rétroaction ne permettent pas d'obtenir du vrai aléatoire, compte tenu de leur comportement totalement déterministe. Pour obtenir un aléatoire un peu plus crédible, il est possible d'utiliser d'autres moyens qui ne sont pas aussi déterministes et surtout qui ne sont pas cycliques. Parmi ces moyens, certains d'entre eux utilisent le signal d'horloge. Par exemple, une technique très simple utilise un compteur incrémenté à chaque cycle d'horloge. Si on a besoin d'un nombre aléatoire, il suffit de lire le contenu de ce registre et de l'utiliser directement comme nombre aléatoire. Si le délai entre deux demandes est irrégulier, le résultat semblera bien aléatoire. Mais il s'agit là d'une technique assez peu fiable dans le monde réel et seules quelques applications bien spécifiques se satisfont de cette méthode.

Une solution un peu plus fiable utilise ce qu'on appelle la dérive de l'horloge. Il faut savoir qu'un signal d'horloge n'est jamais vraiment très précis. Une horloge censée tourner à 1 Ghz ne tournera pas en permanence à 1Ghz exactement, mais verra sa fréquence varier de quelques Hz ou Khz de manière irrégulière. Ces variations peuvent venir de variations aléatoires de température, des variations de tension, des perturbations électromagnétiques, ou à des phénomènes assez compliqués qui peuvent se produire dans tout circuit électrique (comme le shot noise). L'idée consiste à prendre au moins deux horloges et d'utiliser la dérive des horloges pour les désynchroniser. On peut par exemple prendre deux horloges : une horloge lente et une horloge rapide, dont la fréquence est un multiple de l'autre. Par exemple, on peut choisir une fréquence de 1 Mhz et une autre de 100 Hz : la fréquence la plus grande est égale à 10000 fois l'autre. La dérive d'horloge fera alors son œuvre : les deux horloges se désynchroniseront en permanence, et cette désynchronisation peut être utilisée pour produire des nombres aléatoires. Par exemple, on peut compter le nombre de cycles d'horloge produit par l'horloge rapide durant une période de l'horloge lente. Si ce nombre est pair, on produit un bit aléatoire qui vaut 1 sur la sortie du circuit. Pour information, c'est exactement cette technique qui était utilisée dans l'Intel 82802 Firmware Hub.

L'aléatoire généré par la tension d'alimentation modifier

Il existe d'autres solutions matérielles. Dans les solutions électroniques, il arrive souvent qu'on utilise le bruit thermique présent dans tous les circuits électroniques de l'univers. Tous nos circuits sont soumis à de microscopiques variations de température, dues à l'agitation thermique des atomes. Plus la température est élevée, plus les atomes qui composent les fils de nos circuits s'agitent. Vu que les particules d'un métal contiennent des charges électriques, ces vibrations font naître des variations de tensions assez infimes. Il suffit d'amplifier ces variations pour obtenir un résultat capable de représenter un zéro ou un 1. C'est sur ce principe que fonctionne le circuit présent dans les processeurs Intel modernes. Comme vous le savez peut-être déjà, les processeurs Intel Haswell contiennent un circuit capable de générer des nombres aléatoires. Ces processeurs incorporent des instructions capables de fournir des nombres aléatoires, instructions utilisant le fameux circuit que je viens de mentionner.


Dans ce chapitre, nous allons voir un dernier type de circuits, qui font les conversion entre de l'analogique et du numérique. Il en existe deux types . Le circuit qui convertit un signal analogique en signal numérique cela est un CAN (convertisseur analogique-numérique). Le circuit qui fait la conversion inverse est un CNA (convertisseur numérique-analogique). On verra que ces circuits sont utilisés dans les cartes son et dans les anciennes cartes graphiques. Par exemple, il y a un CAN intégré à la carte son, qui sert à convertir le signal provenant d'un microphone en un signal numérique utilisable par la carte son. Il existe aussi un CNA, qui cette fois convertit le signal provenant de la carte son en signal analogique à destination des haut-parleurs. Mais nous reverrons cela dans quelques chapitres.

CAN & CNA

Le convertisseur numérique-analogique modifier

Les CNA sont plus simples à étudier que les CAN, ce qui fait que nous allons les voir en premier. Les CNA convertissent un nombre en binaire codé sur bits en tension analogique. la tension de sortie est comprise dans un intervalle, qui va du 0 volts à une tension maximale . Un 0 binaire sera convertie en une tension de 0 volts, tandis que la valeur binaire est codée avec la tension maximale. Tout nombre entre les deux est compris entre la tension maximale et minimale. Le lien entre nombre binaire et tension de sortie varie pas mal selon le CNA, mais la plupart sont des convertisseurs dits linéaires.

CNA de 8 bits.
Exemple avec un CNA de 2 bits : chaque nombre binaire de 2 bits correspond à un intervalle de tension précis, tous identiques.

On peut expliquer leur fonctionnement de deux manières différentes. Une première manière de voir un CAN linéaire est de regarder l'association entre tension de sortie et nombre binaire. L'intervalle de la tension de sortie est découpé en sous-intervalles de même taille, chacun d'entre eux se voyant attribuer un nombre binaire. Des sous-intervalles consécutifs codent des intervalles consécutifs, le premier codant un 0 et le dernier la valeur maximale .

La taille de chaque sous-intervalle est appelé le quantum de tension et vaut . Il s'agit de la différence de tension minimale que l'on obtient en changeant l'entrée. En clair, la différence de tension en sortie entre deux nombres binaires consécutifs, est toujours la même, égale au quantum de tension. Par exemple, supposons qu'un 5 et un 6 en binaire donneront des tensions différentes de 1 volt. Alors ce sera la même différence de tension entre un 10 binaire et un 11, entre un 1000 et 1001, etc. La seconde manière de les voir est de considérer que la tension de sortie est proportionnelle au nombre à convertir, le coefficient de proportionnalité n'étant autre que le quantum de tension.

CNA linéaire.

Les CNA uniformes (non-pondérés) modifier

Le CNA peut être construit de diverses manières, qui utilisent toutes des composants analogiques nommés résistances et amplificateurs analogiques, que vous avez certainement vu en cours de collège ou de lycée.

Le premier type utilise autant de générateurs de tension qu'il y a de valeurs possibles en sortie. En clair, ce CNA possède générateurs de tension (en comptant la masse et la tension d'alimentation). L'idée est de connecter le générateur qui fournit la tension de sortie et de déconnecter les autres. Chaque connexion/déconnexion se fait par l'intermédiaire d'un interrupteur commandable, à savoir un transistor. Pour faire le lien entre chaque transistor et la valeur binaire, on utilise un décodeur. Il suffit de relier chaque sortie du décodeur (qui correspond à une entrée unique) au transistor (la tension) qui correspond.

CNA uniforme (non-pondéré).

Les CNA pondérés en binaire modifier

Il est maintenant temps de passer aux CNA pondérés. L'idée qui se cache derrière les circuits que nous allons voir est très simple. Partons d'un nombre binaire de bits . Si le bit correspond à un quantum de tension , alors la tension correspondant au bit est de , celle de est de , etc. Une fois chaque bit convertit en tension, il suffit d'additionner les tension obtenues pour obtenir la tension finale. Toute la difficulté est de convertir chaque bit en tension, puis d'additionner le tout. C’est surtout l'addition des tensions qui pose problème, ce qui fait que la plupart des circuits convertit les bits en courants, plus faciles à additionner, avant de convertir le résultat final en tension. Dans ce qui va suivre, nous allons voir deux circuits : les CNA pondérés à résistances équilibrées et non-équilibrées.

CNA à résistances non-équilibrées. CNA à résistances équilibrées.

Le CNA à résistances non-équilibrées modifier

Le circuit suivant utilise des résistances pour convertir un bit en un courant proportionnel à sa valeur. Rappelons que chaque bit est codé par une tension égale à la tension d'alimentation (pour un 1) ou un 0 volt (pour un 0). Cette tension est convertie en courant par un interrupteur, une tension et une résistance. Le courant est obtenu en faisant passer une tension à travers une résistance, l'interrupteur ouvrant ou fermant le circuit selon le bit à coder. Quand le bit est de zéro, l'interrupteur s'ouvre, et le courant ne passe pas : il vaut 0. Quand le bit est à 1, l'interrupteur se ferme et le courant est alors mis à sa valeur de conversion. La valeur de la résistance permet de multiplier chaque bit par son poids (par 1, 2, 4, , 16, ...) : c'est pour cela qu'il y a des résistances de valeur R, 2R, 4R, 8R, etc. Les courants en sortie de chaque résistance sont ensuite additionnés par le reste du circuit, avant d'être transformé en une tension proportionnelle.

Convertisseur numérique-analogique

Le CNA à résistances équilibrées modifier

Le circuit précédent a pour défaut d'utiliser des résistances de valeurs fort différentes : R, 2R, 4R, etc. Mais la valeur d'une résistance est rarement très fiable, surtout quand on commence à utiliser des résistances assez fortes. Chaque résistance a une petit marge d'erreur, qui fait que sa résistance véritable n'est pas tout à fait égale à sa valeur idéale. Avec des résistances fort variées, les marges d'erreurs s'accumulent et influencent le fonctionnement du circuit. Si on veut un circuit réellement fiable, il vaut mieux utiliser des résistances qui ont des marges d'erreur similaires. Et qui dit marges d'erreur similaire dit résistances de valeur similaires. Pas question d'utiliser une résistance de valeur R avec une autre de valeur 16R ou 32R. Pour éviter cela, on doit modifier le circuit précédent de manière à utiliser des résistances de même valeur ou presque. Cela donne le circuit suivant.

Convertisseur numérique analogique R-2R

Le convertisseur analogique-numérique modifier

Les convertisseurs analogique-numérique convertissent une tension en un nombre binaire codé sur bits. Comme pour les CNA, la tension d'entrée peut prendre toutes les valeurs dans un intervalle de tension allant de 0 à une tension maximale. L'intervalle de tension est découpé en sous-intervalles de même taille, chacun se voyant attribuer un nombre binaire. Si la tension d'entrée tombe dans un de ces intervalle, le nombre binaire en sortie est celui qui correspond à cet intervalle. Des intervalles consécutifs correspondent à des nombres binaires consécutifs, le premier intervalle codant un 0 et le dernier le nombre . En clair, le nombre binaire est plus ou moins proportionnel à la tension d'entrée. La taille de chaque intervalle est appelé le quantum de tension, comme pour les CNA.

La conversion d'un signal analogique se fait en plusieurs étapes. La toute première consiste à mesurer régulièrement le signal analogique, pour déterminer sa valeur. Il est en effet impossible de faire la conversion au fil de l'eau, en temps réel. À la place, on doit échantillonner à intervalle réguliers la tension, pour ensuite la convertir. La seconde étape consiste à convertir celle-ci en un signal numérique, un signal discret. Enfin, ce dernier est convertit en binaire. Ces trois étapes portent le nom d’échantillonnage, la quantification et le codage.

signal échantillonné.
Signal discrétisé.

L'échantillonnage modifier

L’échantillonnage mesure régulièrement le signal analogique, afin de fournir un flux de valeurs à convertir en numérique. Il a lieu régulièrement, ce qui signifie que le temps entre deux mesures est le même. Ce temps entre deux mesures est appelée la période d'échantillonnage, notée . Le nombre de fois que la tension est mesurée par seconde s'appelle la fréquence d'échantillonnage. Elle n'est autre que l'inverse de la période d’échantillonnage : . Plus celle-ci est élevée, plus la conversion sera de bonne qualité et fidèle au signal original. Les deux schémas ci-dessous montrent ce qui se passe quand on augmente la fréquence d’échantillonnage : le signal à gauche est échantillonné à faible fréquence, alors que le second l'est à une fréquence plus haute.

Signal échantillonné à basse fréquence.
Signal échantillonné à haute fréquence.

L’échantillonnage est réalisé par un circuit appelé l’échantillonneur-bloqueur. L'échantillonneur-bloqueur le plus simple ressemble au circuit du schéma ci-dessous. Les triangles de ce schéma sont ce qu'on appelle des amplificateurs opérationnels, mais on n'a pas vraiment à s'en préoccuper. Dans ce montage, ils servent juste à isoler le condensateur du reste du circuit, en ne laissant passer les tensions que dans un sens. L'entrée C est reliée à un signal d'horloge qui ouvre ou ferme l'interrupteur à fréquence régulière. La tension va remplir le condensateur quand l'interrupteur se ferme. Une fois le condensateur remplit, l'interrupteur est déconnecté isolant le condensateur de la tension d'entrée. Celui-ci mémorisera alors la tension d'entrée jusqu'au prochain échantillonnage.

Echantillonneur-bloqueur.

La quantification et le codage modifier

Le signal échantillonné est ensuite convertit en un signal numérique, codé sur plusieurs bits. Le nombre de bits du résultat est ce qu'on appelle la résolution du CAN. Plus celle-ci est important,e plus le signal codé sera fidèle au signal d'origine. La précision du CAN sera plus importante avec une résolution importante. Malgré tout, un signal analogique ne peut pas être traduit en numérique sans pertes, l'infinité de valeurs d'un intervalle de tension ne pouvant être codé sur un nombre fini de bits. La tension envoyée va ainsi être arrondie à une tension qui peut être traduite en un entier sans problème. Cette perte de précision va donner lieu à de petites imprécisions qui forment ce qu'on appelle le bruit de quantification. Plus le nombre de bits utilisé pour encoder la valeur numérique est élevée, plus ce bruit est faible.

Résolution d'un CAN.

Le CAN Flash modifier

Un CAN peut être construit de diverses manières, à partir de composants nommés résistances et amplificateurs analogiques. Par exemple, voici à quoi ressemble un CAN Flash, le type de CAN le plus performant. C'est aussi le plus simple à comprendre, bizarrement. Pour comprendre comment celui-ci fonctionne, précisons que le CAN code la tension analogique sur bits, soit des valeurs comprises entre 0 et . Chaque nombre binaire est associée à la tension d'entrée qui correspond. L'idée est de comparer la tension avec toutes les valeurs de tension correspondantes. On utilise pour cela un comparateur pour chaque tension, qui fournit un résultat codé sur un bit : ce dernier vaut 1 si la tension d'entrée est supérieure à la valeur, 0 sinon. Les résultats de chaque comparateur sont combinés entre eux pour déterminer la tension la plus grande qui est proche du résultat. La combinaison des résultats est réalisée avec un encodeur à priorité. Les résultats des comparateurs sont envoyés sur l'entrée adéquate de l'encodeur, qui convertit aussi cette tension en nombre binaire.

Comparateur flash

Ce circuit, bien que très simple, a cependant de nombreux défauts. Le principal est qu'il prend beaucoup de place : les comparateurs de tension sont des dispositifs encombrants, sans compter l'encodeur. Mais le défaut principal est le nombre de comparateurs à utiliser. Sachant qu'il en faut un par valeur, on doit utiliser comparateurs pour un CAN de bits. En clair, le nombre de comparateurs à utiliser croît exponentiellement avec le nombre de bits. En conséquence, les CAN Flash ne sont utilisables que pour de petits convertisseurs, limités à quelques bits. Mais il existe des CAN construits autrement qui n'ont pas ce genre de problèmes.

Le CAN simple rampe modifier

Le CAN simple rampe est un CAN construit avec un compteur, un générateur de tension, un comparateur de tension et un signal d'horloge. L'idée derrière ce circuit est assez simple : au lieu de faire toutes les comparaisons en parallèle, comme avec un CAN Flash, celles-ci sont faites une par une, une tension après l'autre. Ce faisant, on n'a besoin que d'un seul comparateur de tension. Les tensions sont générées successivement par un générateur de rampe, à savoir un circuit qui crée une tension qui croit linéairement. La tension en sortie du générateur de rampe commence à 0, puis monte régulièrement jusqu’à une valeur maximale. Celle-ci est alors comparée à la tension d'entrée. Tant que la tension générée est plus faible, la sortie du comparateur est à 0. Quand la tension en sortie du générateur de rampe dépasse à la tension d'entrée, le comparateur renvoie un 1.

Tout ce système permet de faire les comparaisons de tension, mais il n'est alors plus possible d'utiliser un encodeur pour faire la traduction (tension -> nombre binaire). L'encodeur est remplacé par un autre circuit, qui n'est autre que le compteur. Le compteur est initialisé à 0, mais est incrémenté régulièrement, ce qui fait qu'il balaye toutes les valeurs que peut prendre la sortie numérique. L'idée est que le compteur et la tension du générateur de rampe se suivent : quand l'un augmente, l'autre augmente dans la même proportion. Ainsi, la valeur dans le compteur correspondra systématiquement à la tension de sortie du générateur. Pour cela, on synchronise les deux circuits avec un signal d'horloge. À chaque cycle, le compteur est incrémenté, tandis que le générateur augmente d'un quantum de tension. Ce faisant, quand le comparateur renverra un 0, on saura que la tension d'entrée est égale à celle du générateur. Au même cycle d'horloge, le compteur contient la valeur binaire qui lui correspond. Il suffit alors d’arrêter le compteur et de recopier son contenu sur la sortie.

Comparateur simple rampe.

Ce CAN a l'avantage de prendre bien moins de place que son prédécesseur, sans compter qu'il utilise très peu de circuits. Pas besoin de beaucoup de comparateurs de tension, ni d'un encodeur très compliqué : quelques circuits très simples et peu encombrants suffisent. Ce qui est un avantage certain pour les CAN avec beaucoup de bits. Mais ce CAN a cependant des défauts assez importants. Le défaut principal de ce CAN est qu'il est très lent. Déjà, la conversion est plus rapide pour les tensions faibles, mais très lente pour les grosses tensions, vu qu'il faut balayer les tensions unes par unes. On gagne en place ce qu'on perd en vitesse.

Le CAN delta modifier

Le CAN delta peut être vu comme une amélioration du circuit précédent. Il est lui aussi organisé autour d'un compteur, initialisé à 0, qui est incrémenté jusqu'à tomber sur la valeur de sortie. Encore une fois, ce compteur contient un nombre binaire et celui-ci est associé à une tension équivalente. Sauf que cette fois-ci, la tension équivalente n'est pas générée par un générateur synchronisé avec le compteur, mais directement à partir du compteur lui-même. Le compteur relié à un CNA, qui génère la tension équivalente. La tension équivalente est alors comparée avec la tension d'entrée, et le comparateur commande l'incrémentation du compteur, comme dans le circuit précédent.

Convertisseur CAN de type Delta.

Le CAN par approximations successives modifier

Le CAN par approximations successives effectue une comparaison par étapes, en suivant une procédure dite de dichotomie. Chaque étape correspond à un cycle d'horloge du CAN, qui met donc plusieurs cycles d'horloges pour faire une conversion. Le CAN essaye d'encadrer la tension dans un intervalle, est divisé en deux à chaque étape. L'intervalle à la première étape est de [0 , Tension maximale en entrée ], puis il se réduit progressivement, jusqu'à atteindre un encadrement suffisant, compatible avec la résolution du CAN. À chaque étape, le CAN découpe l'intervalle en deux parties égales, séparées au niveau d'une tension médiane. Il compare l'entrée à la tension médiane et en déduit un bit du résultat, qui est ajouté dans un registre à décalage.

Pour comprendre le concept, prenons l’exemple d'un CAN qui prend en entrée une tension comprise entre 0 et 5 Volts.

  • Lors de la première étape, le CAN vérifie si la tension d'entrée est supérieure/inférieure à 2,5 V.
  • Lors de la seconde étape, il vérifie si la tension d'entrée est supérieure/inférieure 3,75 V ou de 1,25 Volts, selon le résultat de l'étape précédente : 1,25 V si l'entrée est inférieure à 2,5 V, 3,75 V si elle est supérieure.
  • Et on procède sur le même schéma, jusqu’à la dernière étape.

Pour faire son travail, ce CAN comprend un comparateur, un registre et un CNA. Le comparateur est utilisé pour comparer la tension d'entrée avec la tension médiane. Le registre à décalage sert à accumuler les bits calculés à chaque étape, dans le bon ordre. En réfléchissant un petit peu, on devine que les bits sont calculés en partant du bit de poids fort vers le bit de poids faible : le bit de poids fort est calculé dans la première étape, le bit de poids faible lors de la dernière, .... Le CNA sert à générer la tension médiane de chaque étape, à partir de la valeur du registre. L'ensemble est organisé comme illustré dans le schéma ci-dessous.

CAN à approximations successives.


L'architecture d'un ordinateur modifier

Dans les chapitres précédents, nous avons vu comment représenter de l'information, la traiter et la mémoriser avec des circuits. Mais un ordinateur n'est pas qu'un amoncellement de circuits et est organisé d'une manière bien précise. Il est structuré autour de trois circuits principaux :

  • les entrées/sorties, qui permettent à l'ordinateur de communiquer avec l'extérieur ;
  • une mémoire qui mémorise les données à manipuler ;
  • un processeur, qui manipule l'information et donne un résultat.
Architecture d'un système à mémoire.

Pour faire simple, le processeur est un circuit qui s'occupe de faire des calculs et de traiter des informations. La mémoire s'occupe purement de la mémorisation des informations. Les entrées-sorties permettent au processeur et à la mémoire de communiquer avec l'extérieur et d'échanger des informations avec des périphériques. Tout ce qui n'appartient pas à la liste du dessus est obligatoirement connecté sur les ports d'entrée-sortie et est appelé périphérique. Ces composants communiquent via un bus, un ensemble de fils électriques qui relie les différents éléments d'un ordinateur.

Architecture minimale d'un ordinateur.

Parfois, on décide de regrouper la mémoire, les bus, le CPU et les ports d'entrée-sortie dans un seul composant électronique nommé microcontrôleur. Dans certains cas, qui sont plus la règle que l'exception, certains périphériques sont carrément inclus dans le microcontrôleur ! On peut ainsi trouver dans ces microcontrôleurs, des compteurs, des générateurs de signaux, des convertisseurs numériques-analogiques... On trouve des microcontrôleurs dans les disques durs, les baladeurs mp3, dans les automobiles, et tous les systèmes embarqués en général. Nombreux sont les périphériques ou les composants internes à un ordinateur qui contiennent des microcontrôleurs.

Les entrées-sorties modifier

Tous les circuits vus précédemment sont des circuits qui se chargent de traiter des données codées en binaire. Ceci dit, les données ne sortent pas de n'importe où : l'ordinateur contient des composants électroniques qui se chargent de traduire des informations venant de l’extérieur en nombres. Ces composants sont ce qu'on appelle des entrées. Par exemple, le clavier est une entrée : l'électronique du clavier attribue un nombre entier (scancode) à une touche, nombre qui sera communiqué à l’ordinateur lors de l'appui d'une touche. Pareil pour la souris : quand vous bougez la souris, celle-ci envoie des informations sur la position ou le mouvement du curseur, informations qui sont codées sous la forme de nombres. La carte son évoquée il y a quelques chapitres est bien sûr une entrée : elle est capable d'enregistrer un son, et de le restituer sous la forme de nombres.

S’il y a des entrées, on trouve aussi des sorties, des composants électroniques qui transforment des nombres présents dans l'ordinateur en quelque chose d'utile. Ces sorties effectuent la traduction inverse de celle faite par les entrées : si les entrées convertissent une information en nombre, les sorties font l'inverse : là où les entrées encodent, les sorties décodent. Par exemple, un écran LCD est un circuit de sortie : il reçoit des informations, et les transforme en image affichée à l'écran. Même chose pour une imprimante : elle reçoit des documents texte encodés sous forme de nombres, et permet de les imprimer sur du papier. Et la carte son est aussi une sortie, vu qu'elle transforme les sons d'un fichier audio en tensions destinées à un haut-parleur : c'est à la fois une entrée, et une sortie.

La mémoire modifier

La mémoire est le composant qui mémorise des informations, des données. Dans la majorité des cas, la mémoire est découpée en plusieurs bytes, des blocs de mémoire qui contiennent chacun un nombre fini et constant de bits. Le plus souvent, ces bytes sont composés de plusieurs groupes de 8 bits, appelés des octets. Bien évidemment, une mémoire ne peut stocker qu'une quantité finie de données. Et à ce petit jeu, certaines mémoires s'en sortent mieux que d'autres et peuvent stocker beaucoup plus de données que les autres. La capacité d'une mémoire correspond à la quantité d'informations que celle-ci peut mémoriser. Plus précisément, il s'agit du nombre de bits que celle-ci peut contenir.

Lecture et écriture : mémoires ROM et RWM modifier

Pour simplifier grandement, on peut grossièrement classer les mémoires en deux types : les Read Only Memory et les Read Write Memory, aussi appelées mémoires ROM et mémoires RWM. Pour les mémoires ROM, on ne peut pas modifier leur contenu. On peut y récupérer une donnée ou une instruction : on dit qu'on y accède en lecture. Mais on ne peut pas modifier les données qu'elles contiennent. Quant aux mémoires RWM, on peut y accéder en lecture (récupérer une donnée stockée en mémoire), mais aussi en écriture : on peut stocker une donnée dans la mémoire, ou modifier une donnée existante. Tout ordinateur contient au moins une mémoire ROM et une mémoire RWM (souvent une RAM). La mémoire ROM stocke un programme, alors que la mémoire RWM sert essentiellement pour maintenir des résultats de calculs.

Architecture avec une ROM et une RAM.

Si tout ordinateur contient au minimum une ROM et une RWM (souvent une mémoire RAM), les deux n'ont pas exactement le même rôle. Idéalement, les mémoires ROM stockent des programmes à exécuter et sont lues directement par le processeur. La mémoire ROM stocke aussi les constantes, à savoir des données qui peuvent être lues mais ne sont jamais accédées en écriture durant l'exécution du programme. Elles ne sont donc jamais modifiées et gardent la même valeur quoi qu'il se passe lors de l'exécution du programme. Quant à la mémoire RWM, elle est censée mémoriser des données temporaires, nécessaires pour que le programme en mémoire ROM fonctionne. Elle mémorise alors les variables du programme à exécuter, qui sont des données que le programme va manipuler. Vu que les variables du programme sont des données qui sont fréquemment mises à jour et modifiées, elles sont naturellement stockées dans une mémoire RWM. Pour les systèmes les plus simples, la mémoire RWM ne sert à rien de plus.

L'adressage modifier

Sur une mémoire RAM ou ROM, on ne peut lire ou écrire qu'un byte, qu'un registre à la fois : une lecture ou écriture ne peut lire ou modifier qu'un seul byte. Techniquement, le processeur doit préciser à quel byte il veut accéder à chaque lecture/écriture. Pour cela, chaque byte se voit attribuer un nombre binaire unique, l'adresse, qui va permettre de le sélectionner et de l'identifier celle-ci parmi toutes les autres. En fait, on peut comparer une adresse à un numéro de téléphone (ou à une adresse d'appartement) : chacun de vos correspondants a un numéro de téléphone et vous savez que pour appeler telle personne, vous devez composer tel numéro. Les adresses mémoires en sont l'équivalent pour les bytes. Il existe des mémoires qui ne fonctionnent pas sur ce principe, mais passons : ce sera pour la suite. Une autre explication est que l'adresse donne la position dans la mémoire d'une donnée.

Exemple : on demande à la mémoire de sélectionner le byte d'adresse 1002 et on récupère son contenu (ici, 17).

Le processeur modifier

L'unité de traitement est un circuit qui s'occupe de faire des calculs et de manipuler l'information provenant des entrées-sorties ou récupérée dans la mémoire. Dans les ordinateurs, l'unité de traitement porte le nom de processeur, ou encore de Central Processing Unit, abrévié en CPU. Tout processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Elles se classent en quelques grands types très simples :

  • Les instructions arithmétiques font des calculs. Un ordinateur peut ainsi additionner deux nombres, les soustraire, les multiplier, les diviser, etc.
  • Les instructions de test comparent deux nombres entre eux et agissent en fonction.
  • Les instructions d'accès mémoire échangent des données entre la mémoire et le processeur.
  • Les instructions d'entrée-sortie communiquent avec les périphériques.
  • Etc.

Tout processeur est conçu pour exécuter une suite d'instructions dans l'ordre demandé, cette suite s'appelant un programme. Ce que fait le processeur est défini par la suite d'instructions qu'il exécute, par le programme qu'on lui demande de faire. La totalité des logiciels présents sur un ordinateur sont des programmes comme les autres. Ce programme est stocké dans la mémoire de l'ordinateur, comme les données : sous la forme de suites de bits. C'est ainsi que notre ordinateur est rendu programmable : on peut parfaitement modifier le contenu de la mémoire (ou la changer, au pire), et donc changer le programme exécuté par notre ordinateur. Mine de rien, cette idée d'automate stockant son programme en mémoire est ce qui a fait que l’informatique est ce qu'elle est aujourd’hui. C'est la définition même d'ordinateur : appareil programmable qui stocke son programme dans une mémoire modifiable.

Pour exécuter une suite d'instructions dans le bon ordre, le processeur détermine à chaque cycle savoir quelle est la prochaine instruction à exécuter. Il faut donc que notre processeur se souvienne de cette information quelque part, dans une petite mémoire. C'est le rôle du registre d'adresse d'instruction, aussi appelé Program Counter. Ce registre stocke l'adresse de la prochaine instruction à exécuter, adresse qui permet de localiser la prochaine instruction en mémoire. Cette adresse ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples qu'on verra dans la suite de ce tutoriel. Généralement, on profite du fait que ces instructions sont exécutées dans un ordre bien précis, les unes après les autres. Sur la grosse majorité des ordinateurs, celles-ci sont placées les unes à la suite des autres dans l'ordre où elles doivent être exécutées. L'ordre en question est décidé par le programmeur. Un programme informatique n'est donc qu'une vulgaire suite d'instructions stockée quelque part dans la mémoire de notre ordinateur. En faisant ainsi, on peut calculer facilement l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction juste chargée (le nombre de case mémoire qu'elle occupe) au contenu du registre d'adresse d'instruction. Dans ce cas, l'adresse de la prochaine instruction est calculée par un petit circuit combinatoire couplé à notre registre d'adresse d'instruction, qu'on appelle le compteur ordinal.

Mais sur d'autres processeurs, chaque instruction précise l'adresse de la suivante. Ces processeurs n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau d'argent. Sur de tels processeurs, chaque instruction précise quelle est la prochaine instruction, directement dans la suite de bit représentant l'instruction en mémoire. Sur des processeurs aussi bizarres, pas besoin de stocker les instructions en mémoire dans l'ordre dans lesquelles elles sont censées être exécutées. Mais ces processeurs sont très très rares et peuvent être considérés comme des exceptions à la règle.

Le bus de communication modifier

Le processeur est relié à la mémoire ainsi qu'aux entrées-sorties par un ou plusieurs bus de communication. Ce bus n'est rien d'autre qu'un ensemble de fils électriques sur lesquels on envoie des zéros ou des uns. Tout ordinateur contient au moins un bus, qui relie le processeur, la mémoire, les entrées et les sorties ; et leur permet d’échanger des données ou des instructions. Pour permettre au processeur (ou aux périphériques) de communiquer avec la mémoire, il y a trois prérequis que ce bus doit respecter : pouvoir sélectionner la case mémoire (ou l'entrée-sortie) dont on a besoin, préciser à la mémoire s'il s'agit d'une lecture ou d'une écriture, et enfin pouvoir transférer la donnée. Pour cela, on doit donc avoir trois bus spécialisés, bien distincts, qu'on nommera le bus de commande, le bus d'adresse, et le bus de donnée.

  • Le bus de données est un ensemble de fils par lequel s'échangent les données entre les composants.
  • Le bus de commande permet au processeur de configurer la mémoire et les entrées-sorties.
  • Le bus d'adresse, facultatif, permet au processeur de sélectionner l'entrée, la sortie ou la portion de mémoire avec qui il veut échanger des données.

Chaque composant possède des entrées séparées pour le bus d'adresse, le bus de commande et le bus de données. Par exemple, une mémoire RAM possédera des entrées sur lesquelles brancher le bus d'adresse, d'autres sur lesquelles brancher le bus de commande, et des broches d'entrée-sortie pour le bus de données.

Architecture Von Neumann avec les bus.

Les architectures Harvard et Von Neumann modifier

Un point important d'un ordinateur est la séparation entre données et instructions. Dans ce qui va suivre, nous allons faire la distinction entre la mémoire programme, qui stocke les programmes à exécuter, et la mémoire travail qui mémorise des variables nécessaires au fonctionnement des programmes. Nous avons vu plus haut que les données sont censées être placées en mémoire RAM, alors que les instructions sont placées en mémoire ROM. En fait, les choses sont plus compliquées. Il y a des architectures où cette séparation est nette et sans bavures. Mais d'autres ne respectent pas cette séparation à dessin. Cela permet de faire la différence entre les architectures Harvard où la séparation entre données et instructions est stricte, des architectures Von Neumann où données et instructions sont traitées de la même façon par le processeur.

Sur les architectures Harvard, la mémoire ROM est une mémoire programme, alors que la mémoire RWM est une mémoire travail. À l’opposé, les architectures Von Neumann permettent de copier des programmes et de les exécuter dans la RAM. La mémoire RWM sert alors en partie de mémoire programme, en partie de mémoire travail. Par exemple, on pourrait imaginer le cas où le programme est stocké sous forme compressée dans la mémoire ROM, et est décompressé pour être exécuté en mémoire RWM. Le programme de décompression est lui aussi stocké en mémoire ROM et est exécuté au lancement de l’ordinateur. Cette méthode permet d'utiliser une mémoire ROM très petite et très lente, tout en ayant un programme rapide (si la mémoire RWM est rapide). Mais un cas d'utilisation bien plus familier est celui de votre ordinateur personnel, comme nous le verrons plus bas.

Répartition des données et du programme entre la ROM et les RWM.

L'architecture Harvard modifier

Avec l'architecture Harvard, la mémoire ROM et la mémoire RAM sont reliées au processeur par deux bus séparés. L'avantage de cette architecture est qu'elle permet de charger une instruction et une donnée simultanément : une instruction chargée sur le bus relié à la mémoire programme, et une donnée chargée sur le bus relié à la mémoire de données.

Architecture Harvard, avec une ROM et une RAM séparées.

Sur ces architectures, le processeur voit bien deux mémoires séparées avec leur lot d'adresses distinctes.

Vision de la mémoire par un processeur sur une architecture Harvard.

Sur ces architectures, le processeur sait faire la distinction entre programme et données. Les données sont stockées dans la mémoire RAM, le programme est stocké dans la mémoire ROM. Les deux sont séparés, accédés par le processeur sur des bus séparés, et c'est ce qui permet de faire la différence entre les deux. Il est impossible que le processeur exécute des données ou modifie le programme. Du moins, tant que la mémoire qui stocke le programme est bien une ROM.

L'architecture Von Neumann modifier

Avec l'architecture Von Neumann, mémoire ROM et mémoire RAM sont reliées au processeur par un bus unique. Quand une adresse est envoyée sur le bus, les deux mémoires vont la recevoir mais une seule va répondre.

Architecture Von Neumann, avec deux bus séparés.

Avec l'architecture Von Neumann, tout se passe comme si les deux mémoires étaient fusionnées en une seule mémoire. Une adresse correspond soit à la mémoire RAM, soit à la mémoire ROM, mais pas aux deux.

Vision de la mémoire par un processeur sur une architecture Von Neumann.

Une particularité de ces architectures est qu'il est impossible de distinguer programme et données, sauf en ajoutant des techniques de protection mémoire avancées. La raison est qu'il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. Et c'est à l'origine d'un des avantages majeur de l'architecture Von Neumann : il est possible que des programmes soient recopiés dans la mémoire RWM et exécutés dans celle-ci. Un cas d'utilisation familier est celui de votre ordinateur personnel. Le système d'exploitation et les autres logiciels sont copiés en mémoire RAM à chaque fois que vous les lancez.

L'impossibilité de séparer données et instructions a beau être l'avantage majeur des architectures Von Neumann, elle est aussi à l'origine de problèmes assez fâcheux. Il est parfaitement possible que le processeur charge et exécute des données, qu'il prend par erreur pour des instructions. C'est le cas quand le programme exécuté est bugué, le cas le plus courant étant l'exploitation de ces bugs par les pirates informatiques. Il arrive que des pirates informatiques vous fournissent des données corrompues, destinées à être accédées par un programme bugué. Les données corrompues contiennent en fait un virus ou un programme malveillant, caché dans les données. Le bug en question permet justement à ces données d'être exécutées, ce qui exécute le virus. En clair, exécuter des données demande que le processeur ne fasse pas ce qui est demandé ou que le programme exécuté soit bugué. Pour éviter cela, le système d'exploitation fournit des mécanismes de protection pour éviter cela. Par exemple, il peut marquer certaines zones de la mémoire comme non-exécutable, c'est à dire que le système d'exploitation interdit d’exécution de quoi que ce soit qui est dans cette zone.

Il existe cependant des cas très rares où un programme informatique est volontairement codé pour exécuter des données. Par exemple, cela permet de créer des programmes qui modifient leurs propres instructions : cela s'appelle du code auto-modifiant. Ce genre de choses servait autrefois à écrire certains programmes sur des ordinateurs rudimentaires, pour gérer des tableaux et autres fonctionnalités de base utilisées par les programmeurs. Au tout début de l'informatique, où les adresses à lire/écrire devaient être écrites en dur dans le programme, dans les instructions exécutées. Pour gérer certaines fonctionnalités des langages de programmation qui ont besoin d'adresses modifiables, comme les tableaux, on devait recopier le programme dans la mémoire RWM et corriger les adresses au besoin. De nos jours, ces techniques peuvent être utilisées occasionnellement pour compresser un programme, le cacher et le rendre indétectable dans la mémoire (les virus informatiques utilisent beaucoup ce genre de procédés). Mais passons !

L'architecture Harvard modifiée modifier

Les architectures Von Neumann et Harvard sont des cas purs, qui sont encore très utilisés dans des microcontrôleurs ou des DSP (processeurs de traitement de signal). Mais quelques architectures ne suivent pas à la lettre les critères des architectures Harvard et Von Neumann et mélangent les deux, et sont des sortes d'intermédiaires entre les deux. De telles architectures sont appelées des architectures Harvard modifiée. Pour rappel, les architectures Harvard et Von neumman se distinguent sur deux points :

  • Les adresses pour la mémoire ROM (le programme) et la mémoire RAM (les données) sont séparées sur les architectures Harvard, partagées sur l’architecture Von Neumann.
  • L'accès aux données et instructions se font par des voies séparées sur l'architecture Harvard, sur le même bus avec l'architecture Von Neumann.

Les deux points sont certes reliés, mais on peut cependant les décorréler. On peut par exemple imaginer une architecture où les adresses sont partagées, mais où les voies d'accès aux instructions et aux données sont séparées. On peut aussi imaginer le cas où les voies d'accès aux données et instructions sont les mêmes, mais les adresses différentes.

Prenons le premier cas, où les adresses sont partagées, mais où les voies d'accès aux instructions et aux données sont séparées. C'est le cas sur les ordinateurs personnels modernes, où programmes et données sont stockés dans la même mémoire comme dans l'architecture Von Neumann. Cependant, les voies d'accès aux instructions et aux données ne sont pas les mêmes au-delà d'un certain point. La séparation se fait au niveau de la mémoire intégrée dans le processeur, la fameuse mémoire cache dont nous parlerons dans le prochain chapitre. Aussi, nous repartons les explications sur ces architectures dans le chapitre suivant, nous n’avons pas le choix que de faire ainsi.

Le deuxième type d'architecture Harvard modifiée est celle où les voies d'accès aux données et instructions sont les mêmes, mais les adresses différentes. Concrètement, cela ne signifie pas qu'il n'y a qu'un seul bus, mais que des mécanismes sont prévus pour que les deux bus d’instruction et de données interagissent et échangent des informations. Et là, on en trouve deux types.

  • Le cas le plus simple d'architecture Harvard modifiée est une architecture Harvard, où le processeur peut lire des données constantes depuis la mémoire ROM. Vu que les adresses des données et des instructions sont séparées, le processeur doit disposer d'une instruction pour lire les données en mémoire RWM, et d'une instruction pour lire des données en mémoire ROM. Ce n'est pas le cas sur les architectures Harvard, où la lecture des données en ROM est interdite, ni sur les architectures Von Neumann, où la lecture des données se fait avec une unique instruction qui peut lire n'importe quelle adresse aussi bien en ROM qu'en RAM. Une autre possibilité est que le processeur copie ces données constantes depuis la mémoire ROM dans la mémoire RAM, au lancement du programme, avec des instructions adaptées.
  • D'autres architectures font l’inverse. Là où les architectures précédentes pouvaient lire des données en ROM et en RWM, mais chargent leurs instructions depuis la ROM seulement, d'autres architectures font l'inverse. Il leur est possible d’exécuter des instructions peut importe qu'elles viennent de la ROM ou de la RAM. Par contre, quand les instructions sont exécutées depuis la mémoire RAM, les performances s'en ressentent, car on ne peut plus accéder à une donnée en même temps qu'on charge une instruction.


Sur la plupart des systèmes embarqués ou des tous premiers ordinateurs, on n'a que deux mémoires : une mémoire RAM et une mémoire ROM, comme indiqué dans le chapitre précédent. Mais ces systèmes sont très simples et peuvent se permettre d'implémenter l'architecture de base sans devoir y ajouter quoi que ce soit. Ce n'est pas le cas sur les ordinateurs plus puissants.

Un ordinateur moderne ne contient pas qu'une seule mémoire, mais plusieurs. Entre le disque dur, la mémoire RAM, les différentes mémoires cache, et autres, il y a de quoi se perdre. Et de plus, toutes ces mémoires ont des caractéristiques, voire des fonctionnements totalement différents. Certaines mémoires seront très rapides, d'autres auront une grande capacité mémoire (elles pourront conserver beaucoup de données), certaines s'effacent quand on coupe le courant et d'autres non.

Finalement, l'architecture d'un ordinateur moderne diffère de l'architecture de base par la présence d'une grande quantité de mémoires, organisées sous la forme d'une hiérarchie qui va des mémoires très rapides mais très petites à des mémoires de forte capacité très lentes. Le reste de l’architecture ne change pas trop par rapport à l'architecture de base : on a toujours un processeur, des entrées-sorties, un bus de communication, et tout ce qui s'en suit.

La distinction entre mémoire primaire et secondaire modifier

La première amélioration de l'architecture de base consiste à rajouter un niveau de mémoire. Il n'y a alors que deux niveaux de mémoire : la mémoires primaires directement accessible par le processeur, et la mémoire secondaire accessible comme les autres périphériques. La mémoire primaire, correspond actuellement à la mémoire RAM de l'ordinateur, dans laquelle sont chargés les programmes en cours d’exécution et les données qu'ils manipulent. Les mémoires secondaires correspondent aux disques durs, disques SSD, clés USB et autres. Ce sont des périphériques connectés sur la carte mère ou via un connecteur externe.

Distinction entre mémoire primaire et mémoire secondaire.

Le démarrage de l'ordinateur à partir d'une mémoire secondaire modifier

L'ajout de deux niveaux de mémoire pose quelques problèmes pour le démarrage de l'ordinateur : comment charger les programmes depuis un périphérique ?

Les tout premiers ordinateurs pouvaient démarrer directement depuis un périphérique. Ils étaient conçus pour cela, directement au niveau de leurs circuits. Ils pouvaient automatiquement lire un programme depuis une carte perforée ou une mémoire magnétique, et le copier en mémoire RAM. Par exemple, l'IBM 1401 lisait les 80 premiers caractères d'une carte perforée et les copiait en mémoire, avant de démarrer le programme copié. Si un programme faisait plus de 80 caractères, les 80 premiers caractères contenaient un programme spécialisé, appelé le chargeur d’amorçage, qui s'occupait de charger le reste. Sur l'ordinateur Burroughs B1700, le démarrage exécutait automatiquement le programme stocké sur une cassette audio, instruction par instruction.

Les processeurs "récents" ne savent pas démarrer directement depuis un périphérique. À la place, ils contiennent une mémoire ROM utilisée pour le démarrage, qui contient un programme qui charge les programmes depuis le disque dur. Rappelons que la mémoire ROM est accessible directement par le processeur.

Sur les premiers ordinateurs avec une mémoire secondaire, le programme à exécuter était en mémoire ROM et la mémoire secondaire ne servait que de stockage pour les données. Le système d'exploitation était dans la mémoire ROM, ce qui fait que l'ordinateur pouvait démarrer même sans mémoire secondaire. La mémoire secondaire était utilisée pour stocker données comme programmes à exécuter. Les programmes à utiliser étaient placés sur des disquettes, des cassettes audio, ou tout autre support de stockage. Les premiers ordinateurs personnels, comme les Amiga, Atari et Commodore, étaient de ce type.

Par la suite, le système d'exploitation aussi a été déporté sur la mémoire secondaire, à savoir qu'il est installé sur le disque dur, voire un SSD. Un cas d'utilisation familier est celui de votre ordinateur personnel. Le système d'exploitation et les logiciels que vous utilisez au quotidien sont mémorisés sur le disque dur. Mais vu qu'aucun ordinateur ne démarre directement depuis le disque dur ou une clé USB, il y a forcément une mémoire ROM dans un ordinateur moderne, qui n'est autre que le BIOS sur les ordinateurs anciens, l'UEFI sur les ordinateurs récents. Elle est utilisée lors du démarrage de l'ordinateur pour le configurer à l'allumage et démarrer son système d'exploitation. La ROM en question ne sert donc qu'au démarrage de l'ordinateur, avant que le système d'exploitation prenne la relève. L'avantage, c'est qu'on peut modifier le contenu du disque dû assez facilement, tandis que ce n'est pas vraiment facile de modifier le contenu d'une ROM (et encore, quand c'est possible). On peut ainsi facilement installer ou supprimer des programmes, en rajouter, en modifier, les mettre à jour sans que cela ne pose problème.

Le fait de mettre les programmes et le système d'exploitation sur des mémoires secondaire a quelques conséquences. La principale est que le système d'exploitation et les autres logiciels sont copiés en mémoire RAM à chaque fois que vous les lancez. Impossible de faire autrement pour les exécuter. Les systèmes de ce genre sont donc des architectures de type Von Neumann ou de type Harvard modifiée, qui permettent au processeur d’exécuter du code depuis la RAM. Vu que le programme s’exécute en mémoire RAM, l'ordinateur n'a aucun moyen de séparer données et instructions, ce qui amène son lot de problèmes, comme nous l'avons dit au chapitre précédent.

La hiérarchie mémoire d'un ordinateur moderne modifier

De nos jours, un ordinateur contient plusieurs mémoires, organisées en hiérarchie mémoire. Pour simplifier, il existe quatre grands niveaux de hiérarchie mémoire, indiqués dans le tableau ci-dessous : les registres du processeur, la ou les mémoires cache, la mémoire principale (la RAM) et les mémoires de masse. Notons que même si les ordinateurs actuels ont plus de deux niveaux de mémoire, la distinction entre mémoire primaire et secondaire est quand même présente. La mémoire primaire n'est autre que la mémoire RAM, alors que la ou les mémoires secondaires correspondent approximativement aux mémoires de masse. Si je dis approximativement, c'est que le terme mémoire de masse regroupe aussi bien les mémoires secondaires que les mémoires tertiaires et quaternaires dont nous reparlerons plus bas.

Les registres et caches sont des mémoires incorporées au processeur, alors que mémoire primaire et secondaire restent séparées du processeur. La hiérarchie mémoire d'un ordinateur moderne est donc une variante de la hiérarchie à deux niveaux de la section précédente (primaire et secondaire) à laquelle on a rajouté des mémoires internes au processeurs. Le rajout de ces niveaux supplémentaires est une question de performance.

Les processeurs anciens pouvaient se passer de mémoires caches et même de registres, ce qui fait que les ordinateurs avaient alors une hiérarchie mémoire à deux niveaux (primaire et secondaire). Mais au fil du temps, les processeurs ont gagné en performances plus rapidement que la mémoire primaire. Il a donc fallu rajouter des niveaux de hiérarchie mémoire au-dessus de la mémoire primaire, ce qui a complexifié la hiérarchie mémoire des ordinateurs.

Les nouveaux niveaux devant être très rapides, de l'ordre de la nanoseconde, il fallait réduire drastiquement la distance entre le processeur et ces mémoires. Cela n'a l'air de rien, mais l'électricité met quelques dizaines ou centaines de nanosecondes pour parcourir les connexions entre le processeur et la mémoire, temps qui fait partie du temps d'accès à la mémoire. En intégrant registres et caches dans le processeur, on s'assure que le temps d'accès est minimal, la mémoire étant la plus proche possible des circuits de calcul.

Type de mémoire Temps d'accès Capacité Relation avec la mémoire primaire/secondaire
Registres 1 nanosecondes Entre 1 et 512 bits Mémoire incorporée dans le processeur
Caches 10 - 100 nanosecondes Milliers ou millions de bits Mémoire incorporée dans le processeur, sauf pour quelques exceptions anciennes
Mémoire RAM 1 microsecondes Milliards de bits Mémoire primaire
Mémoires de masse (Disque dur, disque SSD, autres) 1 millisecondes Centaines de milliards de bits Mémoire secondaire
Hiérarchie mémoire

Les mémoires de masse modifier

Les mémoires de masse sont des mémoires de grande capacité, qui servent à stocker de grosses quantités de données. De plus, elles conservent des données qui ne doivent pas être effacés et sont donc des mémoire de stockage permanent (on dit qu'il s'agit de mémoires non-volatiles). Concrètement, elles conservent leurs données mêmes quand l'ordinateur est éteint et ce pendant plusieurs années, voir décennies. Les disques durs, mais aussi les CD/DVD et autres clés USB sont des mémoires de masse. Du fait de leur grande capacité, elles sont très lentes. Leur lenteur pachydermique fait qu'elles n'ont pas besoin de communiquer directement avec le processeur, ce qui fait qu'il est plus pratique d'en faire de véritables périphériques, plutôt que de les souder/connecter sur la carte mère.

Parmi les mémoires de masse, on trouve notamment :

  • les mémoires magnétiques, comme disques durs ou les fameuses disquettes (totalement obsolètes de nos jours) ;
  • les mémoires électroniques, comme les mémoires Flash utilisées dans les clés USB et disques durs SSD ;
  • les disques optiques, comme les CD-ROM, DVD-ROM, et autres CD du genre ;
  • mais aussi quelques mémoires très anciennes et rarement utilisées de nos jours, comme les rubans perforés et quelques autres.

Les mémoires de masse se classent en plusieurs types : les mémoires secondaires proprement dit, les mémoires tertiaires et les mémoires quaternaires. Toutes sont traitées comme des périphériques par le processeur, la différence étant dans l’accessibilité.

  • Une mémoire secondaire a beau être un périphérique, elle est située dans l'ordinateur, connectée à la carte mère. Elle s'allume et s'éteint en même temps que l'ordinateur et est accessible tant que l'ordinateur est allumé. Les disques durs et disques SSD sont dans ce cas.
  • Une mémoire tertiaire est un véritable périphérique, dans le sens où on peut l'enlever ou l'insérer dans un connecteur externe à loisir. Par exemple, les clés USB, les CD/DVD ou les disquettes sont dans ce cas. Une mémoire tertiaire est donc rendue accessible par une manipulation humaine, qui connecte la mémoire à l'ordinateur. Le système d'exploitation doit alors effectuer une opération de montage (connexion du périphérique à l’ordinateur) ou de démontage (retrait du périphérique).
  • Quant aux mémoires quaternaires, elles sont accessibles via le réseau, comme les disques durs montés en cloud.

La mémoire principale modifier

La mémoire principale est appelée, par abus de langage, la mémoire RAM. Il s'agit d'une mémoire qui stocke temporairement des données que le processeur doit manipuler (on dit qu'elle est volatile). Elle sert donc essentiellement pour maintenir des résultats de calculs, contenir temporairement des programmes à exécuter, etc. Il s'agit d'une mémoire adressable.

La mémoire principale peut être vue comme un rassemblement de "registres" de même taille (qui ont tous une même quantité de bits). Cette description simpliste décrit parfaitement certaines mémoires RWM électroniques, dans le sens où chaque byte correspond bien à un registre dans la mémoire. Cette description est à nuancer quelque peu pour d'autres mémoires RWM et pour les mémoires ROM, qui implémentent leur contenu avec d'autres composants que des bascules.

Les caches et local stores modifier

Illustration des mémoires caches et des local stores. Le cache est une mémoire spécialisée, de type SRAM, intercalée entre la RAM et le processeur. Les local stores sont dans le même cas, mais ils sont composées du même type de mémoire que la mémoire principale (ce qui fait qu'ils sont abusivement mis au même niveau sur ce schéma).

Le troisième niveau est intermédiaire entre les registres et la mémoire principale. Il regroupe deux types distincts de mémoires : les mémoires caches (du moins, certains caches) et les local stores.

Dans la majorité des cas, la mémoire intercalée entre les registres et la mémoire RAM/ROM est ce qu'on appelle une mémoire cache. De nos jours, ce cache est intégré dans le processeur, mais il a existé des caches qui s'installaient sur un port dédié de la carte mère, du temps du Pentium 1 et 2. Aussi bizarre que cela puisse paraître, elle n'est jamais adressable ! Le contenu du cache est géré par un circuit spécialisé et le programmeur ne peut pas gérer directement ce cache.

Le cache contient une copie de certaines données présentes en RAM et cette copie est accessible bien plus rapidement, le cache étant beaucoup plus rapide que la RAM. Tout accès mémoire provenant du processeur est intercepté par le cache, qui vérifie si une copie de la donnée demandée est présente ou non dans le cache. Si c'est le cas, on accède à la copie le cache : on a un succès de cache (cache hit). Sinon, c'est un défaut de cache (cache miss) : on est obligé d’accéder à la RAM et/ou de charger la donnée de la RAM dans le cache. Tout s'éclairera dans le chapitre dédié aux mémoires caches.

Sur certains processeurs, les mémoires caches sont remplacées par des mémoires RAM appelées des local stores. Ce sont des mémoires RAM, identiques à la mémoire RAM principale, mais qui sont plus petites et plus rapides. Contrairement aux mémoires caches, il s'agit de mémoires adressables, ce qui fait qu'elles ne sont plus gérées automatiquement par le processeur : c'est le programme en cours d'exécution qui prend en charge les transferts de données entre local store et mémoire RAM. Ces local stores consomment moins d'énergie que les caches à taille équivalente : en effet, ceux-ci n'ont pas besoin de circuits compliqués pour les gérer automatiquement, contrairement aux caches. Côté inconvénients, ces local stores peuvent entraîner des problèmes de compatibilité : un programme conçu pour fonctionner avec des local stores ne fonctionnera pas sur un ordinateur qui en est dépourvu.

Les registres du processeur modifier

Enfin, le dernier niveau de hiérarchie mémoire est celui des registres, de petites mémoires très rapides et de faible capacité. Celles-ci sont intégrées à l'intérieur du processeur. La capacité des registres dépend fortement du processeur. Au tout début de l'informatique, il n'était pas rare de voir des registres de 3, 4, voire 8 bits. Par la suite, la taille de ces registres a augmenté, passant rapidement de 16 à 32 bits, voire 48 bits sur certaines processeurs spécialisés. De nos jours, les processeurs de nos PC utilisent des registres de 64 bits. Il existe toujours des processeurs de faible performance qui utilisent des registres relativement petits, de 8 à 16 bits.

Certains processeurs disposent de registres spécialisés, dont la fonction est prédéterminée une bonne fois pour toutes : un registre est conçu pour stocker, uniquement des nombres entiers, ou seulement des flottants, quand d'autres sont spécialement dédiés aux adresses mémoires. Par exemple, les processeurs présents dans nos PC séparent les registres entiers des registres flottants. Pour plus de flexibilité, certains processeurs remplacent les registres spécialisés par des registres généraux, utilisables pour tout et n'importe quoi. Pour reprendre notre exemple du dessus, un processeur peut fournir 12 registres généraux, qui peuvent stocker 12 entiers, ou 10 entiers et 2 flottants, ou 7 adresses et 5 entiers, etc. Dans la réalité, les processeurs utilisent à la fois des registres généraux et quelques registres spécialisés.

L'origine de la hiérarchie mémoire et son fonctionnement modifier

La raison à la présence de toutes ces mémoires est une question de performance. Toutes les mémoires ne sont pas aussi rapides. La rapidité d'une mémoire se mesure grâce à deux paramètres : le temps de latence et son débit binaire.

  • Le temps de latence correspond au temps qu'il faut pour effectuer une lecture ou une écriture : plus il est bas, plus la mémoire est rapide.
  • Le débit mémoire correspond à la quantité d'informations qui peut être récupéré ou enregistré en une seconde dans la mémoire : plus il est élevé, plus la mémoire est rapide

Temps d'accès et débit dépendent de la capacité mémoire mémoire : plus une mémoire peut contenir de données, plus elle est lente. Le fait est que si l'on souhaitait utiliser une seule grosse mémoire dans notre ordinateur, celle-ci serait trop lente et l'ordinateur serait inutilisable. Pour résoudre ce problème, il suffit d'utiliser plusieurs mémoires de taille et de vitesse différentes, qu'on utilise suivant les besoins. Des mémoires très rapides de faible capacité seconderont des mémoires lentes de capacité importante. On peut regrouper ces mémoires en niveaux : toutes les mémoires appartenant à un même niveau ont grosso modo la même vitesse.

Les principes de localité spatiale et temporelle modifier

Le but de cette organisation est de placer les données accédées souvent, ou qui ont de bonnes chances d'être accédées dans le futur, dans une mémoire qui soit la plus rapide possible. Le tout est de faire en sorte de placer les données intelligemment, et les répartir correctement dans cette hiérarchie des mémoires. Ce placement se base sur deux principes qu'on appelle les principes de localité spatiale et temporelle :

  • un programme a tendance à réutiliser les instructions et données accédées dans le passé : c'est la localité temporelle ;
  • et un programme qui s'exécute sur un processeur a tendance à utiliser des instructions et des données consécutives, qui sont proches, c'est la localité spatiale.

Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécutés : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). La localité spatiale est donc respectée tant qu'on a pas de branchements qui renvoient assez loin dans la mémoire (appels de sous-programmes). De même, les boucles (des fonctionnalité des langages de programmation qui permettent d’exécuter en boucle un morceau de code tant qu'une condition est remplie) sont un bon exemple de localité temporelle. Les instructions de la boucle sont exécutées plusieurs fois de suite et doivent être lues depuis la mémoire à chaque fois.

On peut exploiter ces deux principes pour placer les données dans la bonne mémoire. Par exemple, si on a accédé à une donnée récemment, il vaut mieux la copier dans une mémoire plus rapide, histoire d'y accéder rapidement les prochaines fois : on profite de la localité temporelle. On peut aussi profiter de la localité spatiale : si on accède à une donnée, autant précharger aussi les données juste à côté, au cas où elles seraient accédées. Ce placement des données dans la bonne mémoire peut être géré par le matériel de notre ordinateur, mais aussi par le programmeur.

Une bonne utilisation des principes de localité par les programmeurs modifier

De nos jours, le temps que passe le processeur à attendre la mémoire principale devient de plus en plus un problème au fil du temps, et gérer correctement la hiérarchie mémoire est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est très importante : alors qu'une simple addition ou multiplication va prendre entre 1 et 5 cycles d'horloge, une lecture en mémoire RAM fera plus dans les 400-1000 cycles d'horloge. les processeurs modernes utilisent des techniques avancées pour masquer ce temps de latence, qui reviennent à exécuter des instructions pendant ce temps d'attente, mais elles ont leurs limites.

Bien évidement, optimiser au maximum la conception de la mémoire et de ses circuits dédiés améliorera légèrement la situation, mais n'en attendez pas des miracles. Il faut dire qu'il n'y a pas vraiment de solutions facile à implémenter. Par exemple, changer la taille d'une mémoire pour contenir plus de données aura un effet désastreux sur son temps d'accès qui peut se traduire par une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans les jeux vidéos baisser de 2 à 3 % malgré de nombreuses améliorations architecturales très évoluées : la latence du cache L1 avait augmentée de 2 cycles d'horloge, réduisant à néant de nombreux efforts d'optimisations architecturales.

Une bonne utilisation de la hiérarchie mémoire repose en réalité sur le programmeur qui doit prendre en compte les principes de localités vus plus haut dès la conception de ses programmes. La façon dont est conçue un programme joue énormément sur sa localité spatiale et temporelle. Un programmeur peut parfaitement tenir compte du cache lorsqu'il programme, et ce aussi bien au niveau :

  • de son algorithme : on peut citer l'existence des algorithmes cache oblivious ;
  • du choix de ses structures de données : un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse);
  • ou de son code source : par exemple, le sens de parcourt d'un tableau multidimensionnel peut faire une grosse différence.

Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Quoiqu'il en soit, il est quasiment impossible de prétendre concevoir des programmes optimisés sans tenir compte de la hiérarchie mémoire. Et cette contrainte va se faire de plus en plus forte quand on devra passer aux architectures multicœurs.


Dans ce qui précède, nous avons abordé divers paramètres, qui sont souvent indiqués lorsque vous achetez un processeur, de la mémoire, ou tout autre composant électronique :

  • la finesse de gravure ;
  • le nombre de transistors ;
  • la fréquence ;
  • et la tension d'alimentation.

Ces paramètres ont une influence indirecte sur les performances d'un ordinateur, ou sur sa consommation énergétique. Dans ce qui va suivre, nous allons voir comment déduire la performance d'un processeur ou d'une mémoire suivant ces paramètres. Nous allons aussi étudier quel est l'impact de la finesse de gravure sur la consommation d'énergie d'un composant électronique. Cela nous amènera à étudier les tendances de l'industrie.

Pour commencer, il faut d'abord définir ce qu'est la performance d'un ordinateur. C'est loin d'être une chose triviale : de nombreux paramètres font qu'un ordinateur sera plus rapide qu'un autre. De plus, la performance ne signifie pas la même chose selon le composant dont on parle. La performance d'un processeur n'est ainsi pas comparable à la performance d'une mémoire ou d'un périphérique. La finesse de gravure n'a pas d'impact en elle-même sur la performance ou la consommation d'énergie : elle permet juste d'augmenter le nombre de transistors et la fréquence, tout en diminuant la tension d'alimentation. Et ce sont ces trois paramètres qui vont nous intéresser.

La performance du processeur modifier

Concevoir un processeur n'est pas une chose facile et en concevoir un qui soit rapide l'est encore moins, surtout de nos jours. Pour comprendre ce qui fait la rapidité d'un processeur, nous allons devoir déterminer ce qui fait qu'un programme lancé sur notre processeur va prendre plus ou moins de temps pour s’exécuter.

Le temps d’exécution d'une instruction : CPI et fréquence modifier

Le temps que met un programme pour s’exécuter est le produit :

  • du nombre moyen d'instructions exécutées par le programme ;
  • de la durée moyenne d'une instruction, en seconde.
, avec N le nombre moyen d'instruction du programme et la durée moyenne d'une instruction.

Le nombre moyen d'instructions exécuté par un programme s'appelle l'Instruction path length, ou encore longueur du chemin d'instruction en français. Si on utilise le nombre moyen d’instructions, c'est car il n'est pas forcément le même d'une exécution à l'autre. Les processeurs modernes disposent de fonctionnalités appelées branchements qui leur permettent de passer outre certaines sections de code quand elles ne sont pas nécessaires. Par exemple, certaines sections de code ne sont exécutées que si une condition bien spécifique est remplie, grâce à ces branchements. Tout cela deviendra plus clair quand nous aborderons les instructions et les structures de contrôle, dans un chapitre dédié.

Le temps d’exécution d'une instruction peut s'exprimer en secondes, mais on peut aussi l'exprimer en nombre de cycles d'horloge. Par exemple, sur les processeurs modernes, une addition va prendre un cycle d'horloge, une multiplication entre 1 et 2 cycles, etc. Cela dépend du processeur, de l'opération, et d'autres paramètres assez compliqués. Mais on peut calculer un nombre moyen de cycle d'horloge par opération : le CPI (Cycle Per Instruction). Le temps d’exécution moyen, en seconde, d'une instruction dépend alors :

  • du nombre moyen de cycles d'horloge nécessaires pour exécuter une instruction, qu'on notera CPI (ce qui est l'abréviation de Cycle Per Instruction) ;
  • et de la durée d'un cycle d'horloge, notée P (P pour période).

Quand on sait que la durée d'un cycle d'horloge n'est autre que l'inverse de la fréquence on peut reformuler en :

, avec f la fréquence.

La puissance de calcul : IPC et fréquence modifier

On peut rendre compte de la puissance du processeur par une seconde approche. Au lieu de faire intervenir le temps mis pour exécuter une instruction, on peut utiliser la puissance de calcul, à savoir le nombre de calculs que l'ordinateur peut faire par seconde. En toute rigueur, cette puissance de calcul se mesure en nombre d'instructions par secondes, une unité qui porte le nom de IPS. En pratique, la puissance de calcul se mesure en MIPS : Million Instructions Per Second, (million de calculs par seconde en français). Plus un processeur a un MIPS élevé, plus il sera rapide : un processeur avec un faible MIPS mettra plus de temps à faire une même quantité de calcul qu'un processeur avec un fort MIPS. Le MIPS est surtout utilisé comme estimation de la puissance de calcul sur des nombres entiers. Mais il existe cependant une mesure annexe, utilisée pour la puissance de calcul sur les nombres flottants : le FLOPS, à savoir le nombre d'opérations flottantes par seconde.

Par définition, le nombre d'instruction par secondes se calcule en prenant le nombre d'instruction exécutée, et en divisant par le temps d’exécution, ce qui donne :

, avec le temps moyen d’exécution d'une instruction.

Sachant que l'on a vu plus haut que , on peut faire le remplacement :

L'équation nous dit quelque chose d'assez intuitif : plus la fréquence du processeur est élevée, plus il est puissant. Par contre, elle ne dépend pas du CPI, mais de son inverse : le nombre de calculs qui sont effectués par cycle d'horloge. Celui-ci porte le doux nom d'IPC (Instruction Per Cycle). Celui-ci a plus de sens sur les processeurs actuels, qui peuvent effectuer plusieurs calculs en même temps, dans des circuits différents (des unités de calcul différentes, pour être précis). Sur ces ordinateurs, l'IPC est supérieur à 1. En remplaçant l'inverse du CPI par l'IPC, on a alors :

La puissance de calcul est égale à la fréquence multipliée par l'IPC. En clair, plus la fréquence ou l'IPC sont élevés, plus le processeur sera rapide. Cependant, des processeurs de même fréquence ont souvent des IPC différents, ce qui fait que la relation entre fréquence et puissance de calcul dépend fortement du processeur. On ne peut donc pas comparer deux processeurs sur la seule base de leur fréquence. Et si la fréquence est généralement une information qui est mentionnée lors de l'achat d'un processeur, l'IPC ne l'est pas. La raison vient du fait que la mesure de l'IPC n'est pas normalisée et qu'il existe de très nombreuses façons de le mesurer. Il faut dire que l'IPC varie énormément suivant les opérations, le programme, diverses optimisations matérielles, etc.

Comment Augmenter les performances d'un processeur ? modifier

On vient de voir que le temps d’exécution d'un programme est décrit par la formule suivante :

, avec f la fréquence.

Les équations précédentes nous disent qu'il existe trois moyens pour accélérer un programme :

  • diminuer le nombre d'instructions à exécuter ;
  • diminuer le CPI (nombre de cycles par instruction) ou augmenter l'IPC ;
  • augmenter la fréquence.

Voyons en détail ces trois solutions.

Diminuer l'instruction path length modifier

L'instruction path length peut varier suivant les données manipulées par le programme, leur taille, leur quantité, etc. Il est possible de le réduire, mais cela demande généralement au programmeur d'optimiser son programme ou un meilleur compilateur. La seule option au niveau matériel est d'améliorer le jeu d'instruction. Cette dernière solution peut s'implémenter facilement. Il suffit de créer des instructions plus complexes, capables de remplacer des suites d'instructions simples : il n'est pas rare qu'une grosse instruction complexe fasse exactement la même chose qu'une suite d'instructions plus élémentaires. Par exemple, on peut implémenter des instructions pour le calcul de la racine carrée, de l'exponentielle, des puissances, des fonctions trigonométriques : cela évite d'avoir à simuler ces calculs à partir d'additions ou de multiplications. Les programmes qui font alors usage de ces opérations compliquées seront naturellement plus rapides.

Cependant, cette solution a des défauts. Par exemple, cela demande d'ajouter des circuits électroniques, qui consomment et chauffent. Mais le principal problème est que les occasions d'utiliser les instructions complexes sont assez rares, ce qui fait que les compilateurs ou programmeurs ne les utilisent pas, même quand elles peuvent être utiles. Cela explique que la mode actuelle est plutôt d'avoir des processeurs avec un nombre limité d'instruction machine très rapides (les fameux processeurs RISC). Mieux vaut utiliser des circuits électroniques pour de la mémoire cache, pour des unités de calcul ou ajouter des cœurs que d'implémenter des instructions inutiles. La seule exception notable tient dans les instructions dites vectorielles, que nous verrons dans les derniers chapitres.

De plus, ajouter des instructions complexes est quelque chose qui doit se faire sur le long terme, avec le poids de la compatibilité, ce qui n'est pas sans poser quelques problèmes. Par exemple, un programme qui utiliserait des instructions complexes récentes ne peut pas fonctionner sur les anciens processeurs ne possédant pas ces instructions. Ainsi, on a beau rajouter des tas d'instructions dans nos processeurs, il faut attendre que tout le monde ait un processeur avec ces nouvelles instructions pour les utiliser. Pour donner un exemple très simple : à votre avis, sur les nouvelles instructions rajoutées dans chaque nouveau processeur Intel, AMD, ou ARM, combien sont réellement utilisées dans les programmes que vous utilisez ? Combien utilisent les instructions SSE, AVX ou autres extensions récentes ? Très peu.

Diminuer le CPI modifier

Il existe différentes solutions pour diminuer CPI. Tout d'abord, on peut concevoir notre processeur de façon à diminuer le temps mis par le processeur pour exécuter une instruction. C'est particulièrement difficile et nécessite de refaire ses circuits, trouver de nouveaux algorithmes matériels pour effectuer une instruction, améliorer son fonctionnement et sa conception, etc.

Une autre solution consiste à mieux choisir les instructions utilisées. Comme je l'ai dit plus haut, le CPI est une moyenne : certaines instructions sont plus rapides que d'autres. En utilisant de préférence des instructions rapides au lieu d'instructions plus lentes pour faire la même chose, on peut facilement diminuer le CPI. De nos jours, les programmeurs n'ont que très peu d'influence sur le choix des instructions à utiliser : les langages de haut niveau comme le C++ ou le Java se sont démocratisés et ont délégués cette tache aux compilateurs (qui se débrouillent particulièrement bien, en passant).

La dernière solution consiste à exécuter plusieurs instructions à la fois, à augmenter le débit d’instructions. L'idée est d'augmenter l'IPC non pas en jouant sur le temps d'une instruction, mais en exécutant plusieurs instructions durant un même cycle. Ce faisant, on peut augmenter l'IPC, même sans changer le temps que met une instruction à s’exécuter. Les processeurs récents utilisent beaucoup ce genre d'astuce et sont capables d’exécuter plusieurs dizaines, voire centaines d'instructions en parallèle. Une bonne partie de la fin du cours est d'ailleurs dédiée aux techniques qui permettent ce genre de prouesses.

Augmenter la fréquence modifier

Augmenter la fréquence demande d'utiliser des composants électroniques plus rapides. Généralement, cela nécessite de miniaturiser les transistors de notre processeur : plus un transistor est petit, plus il est rapide. Diminuer la taille des transistors permet de les rendre plus rapides. C'est un scientifique du nom de Robert Dennard qui a découvert un moyen de rendre un transistor plus rapide en diminuant certains de ses paramètres physiques. De plus, la vitesse de transmission des bits dans les fils d'interconnexions est proportionnelle à leur longueur : plus ces fils seront courts, plus la transmission sera rapide. Ce qui est clairement un second effet positif de la miniaturisation sur la fréquence.

Sachant que la loi de Moore nous dit que le nombre de transistors d'un processeur est multiplié par 2 tous les deux ans, on peut s'attendre à ce qu'il en soit de même pour la fréquence. En réalité, la fréquence ne dépend pas du nombre de transistor, mais de la longueur de leur côté, la finesse de gravure. La loi de Moore dit que les transistors prennent 2 fois moins de place tous les deux ans. Or, les transistors sont réunis sur une surface : qui dit diminution par deux de la surface signifie division par de la longueur d'un transistor, de sa finesse de gravure. Ainsi, la finesse de gravure est divisée par tous les deux ans. La fréquence ces processeurs est, en conséquence, multipliée par tous les deux ans, ce qui donne environ 40% de performances en plus tous les deux ans. Du moins, c'est la théorie… En réalité, cette augmentation de 40% n'est qu'une approximation : la fréquence effective d'un processeur dépend fortement de sa conception (de la longueur du pipeline, notamment) et des limites imposées par la consommation thermique.

L'évolution de la performance dans le temps modifier

On vient de voir comment quantifier la performance d'un processeur : avec sa fréquence, et avec son IPC/CPI. Il va de soi que les nouveaux processeurs sont plus puissants que les anciens. La raison à cela vient des optimisations apportées par les concepteurs de processeurs. La plupart de ces optimisations ne sont cependant possibles qu'avec la miniaturisation des transistors, qui leur permet d'aller plus vite. On a vu plus haut que la vitesse des transistors est proportionnelle à la finesse de gravure. En conséquence, tous deux augmentent de 40 % tous les deux ans (elle est multipliée par la racine carrée de deux). Pour cette raison, la fréquence devrait augmenter au même rythme. Cependant, la fréquence dépend aussi de la rapidité du courant dans les interconnexions (les fils) qui relient les transistors, celles-ci devenant de plus en plus un facteur limitant pour la fréquence.

De plus, si la miniaturisation permet d'augmenter la fréquence, elle permet aussi d'améliorer l'IPC et le CPI. Cependant, si l'effet est direct sur la fréquence, il est assez indirect en ce qui concerne le CPI. La loi de Pollack dit que l'augmentation de l'IPC d'un processeur est approximativement proportionnelle à la racine carrée du nombre de transistors ajoutés : si on double le nombre de transistors, la performance est multipliée par la racine carrée de 2. En utilisant la loi de Moore, on en déduit qu'on gagne approximativement 40% d'IPC tous les deux ans, à ajouter aux 40 % d'augmentation de fréquence.

On peut expliquer cette loi de Pollack assez simplement. Il faut savoir que les processeurs modernes peuvent exécuter plusieurs instructions en même temps (on parle d’exécution superscalaire), et peuvent même changer l'ordre des instructions pour gagner en performances (on parle d’exécution dans le désordre). Pour cela, les instructions sont préchargées dans une mémoire tampon de taille fixe, interne au processeur, avant d'être exécutée en parallèle dans divers circuits de calcul. Cependant, le processeur doit gérer les situations où une instruction a besoin du résultat d'une autre pour s'exécuter : si cela arrive, on ne peut exécuter les instructions en parallèle. Pour détecter une telle dépendance, chaque instruction doit être comparée à toutes les autres, pour savoir quelle instruction a besoin des résultats d'une autre. Avec N instructions, vu que chacune d'entre elles doit être comparée à toutes les autres, ce qui demande N^2 comparaisons. En doublant le nombre de transistors, on peut donc doubler le nombre de comparateurs, ce qui signifie que l'on peut multiplier le nombre d'instructions exécutables en parallèle par la racine carrée de deux.

On peut cependant contourner la loi de Pollack, qui ne vaut que pour un seul processeur. Mais en utilisant plusieurs processeurs, la performance est la somme des performances individuelles de chacun d'entre eux. C'est pour cela que les processeurs actuels sont doubles, voire quadruple cœurs : ce sont simplement des circuits imprimés qui contiennent deux, quatre, voire 8 processeurs différents, placés sur la même puce. Chaque cœur correspond à un processeur. En faisant ainsi, doubler le nombre de transistors permet de doubler le nombre de cœurs et donc de doubler la performance, ce qui est mieux qu'une amélioration de 40%.

La performance d'une mémoire modifier

Toutes les mémoires ne sont pas faites de la même façon et les différences entre mémoires sont nombreuses. Dans cette partie, on va passer en revue les différences les plus importantes.

La capacité mémoire modifier

Pour commencer, une mémoire ne peut pas stocker une quantité infinie de données : qui n'a jamais eu un disque dur ou une clé USB plein ? Et à ce petit jeu là, toutes les mémoires ne sont pas égales : elles n'ont pas la même capacité. Cette capacité mémoire n'est autre que le nombre maximal de bits qu'une mémoire peut contenir. Dans la majorité des mémoires, les bits sont regroupés en paquets de taille fixe : des cases mémoires, aussi appelées bytes. De nos jours, le nombre de bits par byte est généralement un multiple de 8 bits : ces groupes de 8 bits s'appellent des octets. Mais toutes les mémoires n'ont pas des bytes d'un octet ou plusieurs octets : certaines mémoires assez anciennes utilisaient des cases mémoires contenant 1, 2, 3, 4, 7, 18, 36 bits.

Les unités de capacité mémoire : kilo, méga et giga modifier

Le fait que les mémoires aient presque toutes des bytes faisant un octet nous arrange pour compter la capacité d'une mémoire. Au lieu de compter cette capacité en bits, on préfère mesurer la capacité d'une mémoire en donnant le nombre d'octets que celle-ci peut contenir. Mais les mémoires des PC font plusieurs millions ou milliards d'octets. Pour se faciliter la tâche, on utilise des préfixes pour désigner les différentes capacités mémoires. Vous connaissez sûrement ces préfixes : kibioctets, mébioctets et gibioctets, notés respectivement Kio, Mio et Gio.

Préfixe Capacité mémoire en octets Puissance de deux
Kio 1024 210 octets
Mio 1 048 576 220 octets
Gio 1 073 741 824 230 octets

On peut se demander pourquoi utiliser des puissances de 1024, et ne pas utiliser des puissances un peu plus communes ? Dans la majorité des situations, les électroniciens préfèrent manipuler des puissances de deux pour se faciliter la vie. Par convention, on utilise souvent des puissances de 1024, qui est la puissance de deux la plus proche de 1000. Or, dans le langage courant, kilo, méga et giga sont des multiples de 1000. Quand vous vous pesez sur votre balance et que celle-ci vous indique 58 kilogrammes, cela veut dire que vous pesez 58 000 grammes. De même, un kilomètre est égal à 1000 mètres, et non 1024 mètres.

Autrefois, on utilisait les termes kilo, méga et giga à la place de nos kibi, mebi et gibi, par abus de langage. Mais peu de personnes sont au courant de l'existence de ces nouvelles unités, et celles-ci sont rarement utilisées. Et cette confusion permet aux fabricants de disques durs de nous « arnaquer » : Ceux-ci donnent la capacité des disques durs qu'ils vendent en kilo, mega ou giga octets : l’acheteur croit implicitement avoir une capacité exprimée en kibi, mebi ou gibi octets, et se retrouve avec un disque dur qui contient moins de mémoire que prévu.

L'évolution de la capacité suivant le type de mémoire modifier

De manière générale, les mémoires électroniques sont plus rapides que les mémoires magnétiques ou optiques, mais ont une capacité inférieure. La loi de Moore a une influence certaine sur la capacité des mémoires électroniques. En effet, une mémoire électronique est composée de bascules de 1 bit, elles-mêmes composées de transistors : plus la finesse de gravure est petite, plus la taille d'une bascule sera faible. Quand le nombre de transistors d'une mémoire double, on peut considérer que le nombre de bascules, et donc la capacité double. D'après la loi de Moore, cela arrive tous les deux ans, ce qui est bien ce qu'on observe pour les mémoires RAM.

Évolution du nombre de transistors d'une mémoire électronique au cours du temps. On voit que celle-ci suit de près la loi de Moore.

Par contre, les mémoires magnétiques, comme les disques durs, augmentent à un rythme différent, celles-ci n'étant pas composées de transistors.

Évolution de la capacité des disques durs (mémoires magnétiques) dans le temps en échelle logarithmique.

Le temps d’accès d'une mémoire modifier

La vitesse d'une mémoire correspond au temps qu'il faut pour récupérer une information dans la mémoire, ou pour y effectuer un enregistrement. Lors d'une lecture/écriture, il faut attendre un certain temps que la mémoire finisse de lire ou d'écrire la donnée : ce délai est appelé le temps d'accès, ou aussi temps de latence. Plus celui-ci est bas, plus la mémoire est rapide. Il se mesure en secondes, millisecondes, microsecondes pour les mémoires les plus rapides. Généralement, le temps de latence dépend de temps de latences plus élémentaires, qui sont appelés les timings mémoires.

Cependant, tous les accès à la mémoire ne sont pas égaux en termes de temps d'accès. Généralement, lire une donnée ne prend pas le même temps que l'écrire. Dit autrement, le temps d'accès en lecture est souvent inférieur au temps d'accès en écriture. Il faut dire qu'il est beaucoup plus fréquent de lire dans une mémoire qu'y écrire, et les fabricants préfèrent donc réduire le temps d'accès en lecture.

Voici les temps d'accès moyens en lecture de chaque type de mémoire :

  • Registres : 1 nanoseconde (10-9)
  • Caches : 10 - 100 nanosecondes (10-9)
  • Mémoire RAM : 1 microseconde (10-6)
  • Mémoires de masse : 1 milliseconde (10-3)

Le débit d'une mémoire modifier

Enfin, toutes les mémoires n'ont pas le même débit binaire. Par débit, on veut dire que certaines sont capables d'échanger un grand nombre de données par seconde, alors que d'autres ne peuvent échanger qu'un nombre limité de données sur le bus. Le débit binaire est la quantité de données que la mémoire peut envoyer ou recevoir par seconde. Il se mesure en octets par seconde ou en bits par seconde. Évidemment, plus ce débit est élevé, plus la mémoire sera rapide. Il ne faut pas confondre le débit et le temps d'accès. Pour faire une analogie avec les réseaux, le débit binaire peut être vu comme la bande passante, alors que le temps d'accès serait similaire au ping. Il est parfaitement possible d'avoir un ping élevé avec une connexion qui télécharge très vite, et inversement. Pour la mémoire, c'est similaire. D'ailleurs, le débit binaire est parfois improprement appelé bande passante.

Dans presque tous les cas, le débit dépend fortement de la fréquence de la mémoire. Or, l'évolution de la fréquence des mémoires suit plus ou moins celle des processeurs, elle double au même rythme. Mais malheureusement, cette fréquence reste inférieure à celle des processeurs. Cette augmentation de fréquence permet au débit des mémoires d'augmenter avec le temps. En effet, à chaque cycle d'horloge, la mémoire peut envoyer ou recevoir une quantité fixe de données. En multipliant cette largeur du bus par la fréquence, on obtient le débit. Par contre, la fréquence n'a aucun impact sur le temps de latence.

Le temps de balayage modifier

Le temps de balayage d'une mémoire est le temps mis pour parcourir/accéder à toute la mémoire. Concrètement, il est défini en divisant la capacité de la mémoire par son débit binaire. Le résultat s'exprime en secondes. Le temps de balayage est en soi une mesure peu utilisée, sauf dans quelques applications spécifiques. C'est le temps nécessaire pour lire ou réécrire tout le contenu de la mémoire. On peut le voir comme une mesure du compromis réalisé entre la capacité de la mémoire et sa rapidité : une mémoire aura un temps de balayage d'autant plus important qu'elle est lente à capacité identique, ou qu'elle a une grande capacité à débit identique. Généralement un temps de balayage faible signifie que la mémoire est rapide par rapport à sa capacité.

Comme dit plus haut, le temps d'accès est différent pour les lectures et les écritures, et il en est de même pour le débit binaire. En conséquence, le temps de balayage n'est pas le même si le balayage se fait en lecture ou en écriture. On doit donc distinguer le temps de balayage en lecture qui est le temps mis pour lire la totalité de la mémoire, et le temps de balayage en écriture qui est le temps mis pour écrire une donnée dans toute la mémoire. La distinction est d'autant plus importante que les cas où on balaye une mémoire en lecture avec les cas où on balaye la mémoire en écriture. Généralement, on balaye une mémoire en lecture quand on veut recherche une donnée bien précise dedans. Par contre, le balayage en écriture correspond surtout aux cas où on veut réinitialiser la mémoire, la remplir tout son contenu avec des zéros afin de la remettre au même état qu'à son démarrage.

Un exemple de balayage en écriture est celui d'une réinitialisation de la mémoire, à savoir remplacer le contenu de chaque case mémoire par un 0. Le temps nécessaire pour réinitialiser la mémoire n'est autre que le temps de balayage en écriture. En soi, les opérations de réinitialisation de la mémoire sont plutôt rares. Certains vieux ordinateurs effaçaient la mémoire à l'allumage, et encore pas systématiquement, mais ce n'est plus le cas de nos jours. Un cas plus familier est celui du formatage complet du disque dur. Si vous voulez formater un disque dur ou une clé USB ou tout autre support de stockage, le système d'exploitation va vous donner deux choix : le formatage rapide et le formatage complet. Le formatage rapide n'efface pas les fichiers sur le disque dur, mais utilise des stratagèmes pour que le système d'exploitation ne puisse plus savoir où ils sont sur le support de stockage. Les fichiers peuvent d'ailleurs être récupérés avec des logiciels spécialisés trouvables assez facilement. Par contre, le formatage complet efface la totalité du disque dur et effectue bel et bien une réinitialisation. Le temps mis pour formater le disque dur n'est autre que le temps de balayage en écriture.

Un autre cas de réinitialisation de la mémoire est celui de l'effacement du framebuffer sur les très vielles cartes graphiques. Sur les vielles cartes graphiques, la mémoire vidéo ne servait qu'à stocker des images calculées par le processeur. Le processeur calculait l'image à afficher et l'écrivait dans la mémoire vidéo, appelée framebuffer. Puis, l'image était envoyée à l'écran quand celui-ci était libre, la carte graphique gérant l'affichage. L'écran affichait généralement 60 images par secondes, et le processeur devait calculer une image en moins de 1/60ème de seconde. Mais si le processeur mettait plus de temps, l'image dans le framebuffer était un mélange de l'ancienne image et des parties de la nouvelle image déjà calculées par le processeur. L'écran affichait donc une image bizarre durant 1/60ème de seconde, ce qui donnait des légers bugs graphiques très brefs, mais visibles. Pour éviter cela, le framebuffer était effacé entre chaque image calculée par le processeur. Au lieu d'afficher un bug graphique, l'écran affichait alors une image blanche en cas de lenteur du processeur. Cette solution était possible, car les mémoires de l'époque avaient un temps de balayage en écriture assez faible. De nos jours, cette solution n'est plus utilisée, car la mémoire vidéo stocke d'autres données que l'image à afficher à l'écran, et ces données ne doivent pas être effacées.

Le temps de balayage en lecture est surtout pertinent dans les cas où on recherche une donnée précise dans la mémoire. L'exemple le plus frappant est celui des antivirus, qui recherchent si une certaine suite de donnée est présente en mémoire RAM. Les antivirus scannent régulièrement la RAM à la recherche du code binaire de virus, et doivent donc balayer la RAM et appliquer des algorithmes assez complexes sur les données lues. Bref, le temps de balayage donne le temps nécessaire pour scanner la RAM, si on oublie le temps de calcul. Tous les exemples précédents demandent de scanner la RAM à la recherche d'une donnée précise, et le temps de balayage donne une borne inférieure à ce temps de recherche. Cet exemple n'est peut-être pas très réaliste, mais il deviendra plus clair dans le chapitre sur les mémoires associatives, un type de mémoire particulier conçu justement pour réduire le temps de balayage en lecture au strict minimum.

Enfin, on peut aussi citer le cas où l'on souhaite vérifier le contenu de la mémoire, pour vérifier si tous les bytes fonctionnent bien. Il arrive que les mémoires RAM aient des pannes : certains bytes tombent en panne après quelques années d'utilisation, et deviennent inaccessibles. Lorsque cela arrive, tout se passe bien tant que les bytes défectueux ne sont pas lus ou écrits. Mais quand cela arrive, les lectures renvoient des données incorrectes. Les conséquences peuvent être très variables, mais cela cause généralement des bugs assez importants, voire des écrans ou de beaux plantages. De nombreux cas d'instabilité système sont liés à ces bytes défectueux. Il est possible de vérifier l'intégrité de la mémoire avec des logiciels spécialisés, qui vérifient chaque byte de la mémoire un par un. Les systèmes d'exploitation modernes incorporent un logiciel de ce genre, comme Windows qui en a un d'intégré. Le BIOS ou l'UEFI de votre ordinateur a de bonnes chances d'intégrer un logiciel de ce genre. Ces logiciels de diagnostic mémoire balayent la mémoire byte par byte, case mémoire par case mémoire, et effectuent divers traitements dessus. Dans le cas le plus simple, ils écrivent une donnée dans chaque byte, avant de le lire : si la donnée lue et écrite ne sont pas la même, le byte est défectueux. Mais d'autres traitements sont possibles. Toujours est-il que ces utilitaires balayent la mémoire, généralement plusieurs fois. Le temps de balayage donne alors une idée du temps que mettront ces logiciels de diagnostic pour s’exécuter.


Dans ce chapitre, nous allons évoquer la consommation énergétique. Nos ordinateurs consomment une certaine quantité de courant pour fonctionner, et donc une certaine quantité d'énergie. Il se trouve que cette énergie finit par être dissipée sous forme de chaleur : plus un composant consomme d'énergie, plus il chauffe. La chaleur dissipée est mesurée par ce qu'on appelle l'enveloppe thermique, ou TDP (Thermal Design Power), mesurée en Watts. Généralement, le TDP d'un processeur tourne autour des 50 watts, parfois plus sur les modèles plus anciens.

De telles valeurs signifient que les processeurs actuels chauffent beaucoup. Pourtant, ce n'est pas la faute des fabricants de processeurs qui essaient de diminuer la consommation d'énergie de nos CPU au maximum. Malgré cela, nos processeurs voient leur consommation énergétique augmenter toujours plus : l'époque où l'on refroidissait les processeurs avec un simple radiateur est révolue. Place aux gros ventilateurs super puissants, placés sur un radiateur.

Et pourtant, l'efficacité énergétique des processeurs n'a pas cessé d'augmenter au cours du temps : pour le même travail, les processeurs chauffent moins. Avant l'année 2010 (environ, la date exacte varie suivant les estimations), la quantité de calcul que peut effectuer le processeur en dépensant un watt double tous les 1,57 ans. Cette observation porte le nom de loi de Kommey. Mais depuis l'année 2010, on est passé à une puissance de calcul par watt qui double tous les 2,6 ans. Il y a donc eu un ralentissement dans l'efficacité énergétique des processeurs. Globalement, le nombre de Watts nécessaires pour effectuer une instruction a diminué de manière exponentielle avec les progrès de la miniaturisation.

Watts par millions d'instructions, au cours du temps.

La consommation d'un processeur modifier

Pour comprendre pourquoi, on doit parler de ce qui fait qu'un processeur consomme de l'énergie. Précisons que ce que nous allons dire se généralise à tous les circuits électroniques, sans exceptions. Une partie de la consommation d'énergie d'un processeur vient du fait que le circuit consomme de l'énergie en changeant d'état. On appelle cette perte la consommation dynamique. Une autre partie vient du fait que les circuits ne sont pas des dispositifs parfaits et qu'ils laissent fuiter un peu de courant. Cette consommation est appelée la consommation statique.

L'importance relative de la consommation statique ou dynamique dépend de la technologie utilisée pour fabriquer les portes logiques. Les anciennes technologies, comme les technologies bipolaire, NMOS ou PMOS, ont une forte consommation statique, mais une faible consommation dynamique. A l'inverse, la technologie CMOS a une grande consommation dynamique, mais une faible consommation statique. La raison est que les technologie pré-CMOS utilisaient des résistances avec les transistors. Et des résistances parcourues par un courant consomment de l'énergie, qu'elles convertissent en chaleur. La consommation d'énergie était donc très élevée. Et vu qu'il n'est pas possible d'éteindre une résistance, qui est parcourue par un courant en permanence, il s'agissait surtout de consommation statique. A l'opposé, le MOS n'utilise que des transistors, qui ont une consommation statique très faible. Aussi, les puce électroniques actuelles sont dominées par la consommation dynamique.

Du moins, c'est la théorie, car la miniaturisation a beaucoup changé les choses. La réduction de la finesse de gravure diminue la consommation dynamique, mais augmente la consommation statique. La miniaturisation a permis de radicalement réduire la consommation électrique des puces, en réduisant leur consommation dynamique. Vu que les puces anciennes avaient une consommation statique ridiculement basse et une consommation dynamique normale, doubler la première et diviser par deux la seconde donnait un sacré gain au total. Lors de cette période, la réduction de la finesse de gravure permettait de diminuer la consommation d'énergie, tout en augmentant la puissance de calcul. La loi de Kommey était valide, car la consommation dynamique était la plus importante des deux. Mais depuis, la consommation statique a finit par rattraper la consommation dynamique. L'évolution de la finesse de gravure ne permettant plus de réduire la consommation statique, la consommation d'énergie des processeurs s'est mise à stagner, ce qui explique le ralentissement de la loi de Kommey.

La consommation statique d'un processeur modifier

Transistor CMOS - 1

Avant toute chose, nous devons faire quelques rappels sur les transistors MOS, sans lesquels les explications qui vont suivre seront compliquées. Un transistor MOS est composé de deux morceaux de conducteurs (l'armature de la grille et la liaison drain-source) séparés par un isolant. L'isolant empêche la fuite des charges de la grille vers la liaison source-drain. Mais avec la miniaturisation, la couche d'isolant ne fait guère plus de quelques dizaines atomes d'épaisseur et laisse passer un peu de courant : on parle de courants de fuite. Plus cette couche d'isolant est petite, plus le courant de fuite sera fort. En clair, une diminution de la finesse de gravure a tendance à augmenter les courants de fuite. Les lois de la physique nous disent alors que la consommation d'énergie qui résulte de ce courant de fuite est une consommation statique. Mieux, on sait prouver qu'elle est égale au produit entre la tension d'alimentation et le courant de fuite.

La consommation dynamique d'un processeur modifier

Nous avons dit plus haut qu'un transistor MOS est composé de deux transistors séparés par un isolant. Tout cela ressemble beaucoup à un autre composant électronique appelé le condensateur, qui sert de réservoir à électricité. On peut le charger en électricité, ou le vider pour fournir un courant durant une petite durée de temps. Et l'intérieur d'un condensateur est formé de deux couches de métal conducteur, séparées par un isolant électrique, tout comme les transistors MOS. Tout cela fait qu'un transistor MOS incorpore un pseudo-condensateur caché entre la grille et la liaison source-drain, qui porte le nom de capacité parasite du transistor. Et tout cela permet d'expliquer d'où provient la consommation d'énergie d'un transistor.

La quantité d'énergie que peut stocker un condensateur est égale au produit ci-dessous, avec C un coefficient de proportionnalité appelé la capacité du condensateur et U la tension d'alimentation. Cette équation nous dit que la consommation d'énergie d'un transistor dépend du carré de la tension d'alimentation et de sa capacité. Les transistors étant un condensateur,

On peut alors multiplier par le nombre de transistors d'une puce électronique, ce qui donne :

L'énergie est dissipée quand les transistors changent d'état, il dissipe une quantité de chaleur proportionnelle au carré de la tension d'alimentation. Or, la fréquence définit le nombre maximal de changements d'état qu'un transistor peut subir en une seconde : pour une fréquence de 50 hertz, un transistor peut changer d'état 50 fois par seconde maximum. Après, il s'agit d'une borne maximale : il se peut qu'un transistor ne change pas d'état à chaque cycle. Sur les architectures modernes, la probabilité de transition 0 ⇔ 1 étant d'environ 10%-30%. Et si les bits gardent la même valeur, alors il n'y a pas de dissipation de puissance. Mais on peut faire l'approximation suivante : le nombre de changement d'état de tous les transistors d'une puce dépend de la fréquence. L'énergie dissipée en une seconde (la puissance P) est approximée par l'équation suivante :

Dans la section qui suit, la capacité des transistors ne nous intéressera pas beaucoup, pas plus que leur nombre. On pourra se contenter d'une relation qui ne contient que la fréquence et la tension.

L'équation précédente nous dit que la consommation d'énergie d'une puce dépend de la tension d'alimentation et de la fréquence. Plus la tension d'alimentation et la fréquence sont élevées, plus le composant consomme d'énergie. La tension a un impact bien plus grand que la fréquence, car elle est au carré alors que la fréquence ne l'est pas.

Il s'agit là d'une approximation. En réalité, la consommation dynamique dépend des changements d'état des transistors. Et cela aura des conséquences dans la suite du chapitre.

Les technologies de réduction de la puissance dissipée par un processeur modifier

L'équation précédente dit que diminuer la tension ou la fréquence permettent de diminuer la consommation énergétique. De plus, la diminution de tension a un effet plus marqué que la diminution de la fréquence. Pour réduire la tension et la fréquence, les ingénieurs ont inventé diverses techniques assez intéressantes, qui permettent d'adapter la tension et la fréquence en fonction des besoins.

La distribution de tensions d'alimentations multiples modifier

Une première solution prend en compte le fait que certaines portions du processeur sont naturellement plus rapides que d'autres. Il est en effet possible de faire fonctionner certaines portions du processeur à une fréquence plus basse que le reste. Autant les circuits de calculs doivent fonctionner à la fréquence maximale, autant un processeur intègre des circuits annexes assez divers, sans rapport avec ses capacités de calcul et qui peuvent fonctionner au ralenti. Par exemple, les circuits de gestion de l'énergie n'ont pas à fonctionner à la fréquence maximale, tout comme les timers (des circuits qui permettent de compter les secondes, intégrés dans les processeurs et utilisés pour des décomptes logiciels). Pour cela, les concepteurs de CPU font fonctionner ces circuits à une fréquence plus basse que la normale. Ils ont juste à ajouter des circuits diviseur de fréquence dans l'arbre d'horloge.

Vu que les circuits en question fonctionnent à une fréquence inférieure à ce qu'ils peuvent, on peut baisser leur tension d'alimentation juste ce qu'il faut pour les faire aller à la bonne vitesse. Pour ce faire, on doit utiliser plusieurs tensions d'alimentation pour un seul processeur. Ainsi, certaines portions du processeur seront alimentées par une tension plus faible, tandis que d'autres seront alimentées par des tensions plus élevées. La distribution de la tension d'alimentation dans le processeur est alors un peu plus complexe, mais rien d'insurmontable. Pour obtenir une tension quelconque, il suffit de partir de la tension d'alimentation et de la faire passer dans un régulateur de tension, qui fournit la tension voulue en sortie. Les concepteurs de CPU ont juste besoin d'ajouter plusieurs régulateurs de tension, qui fournissent les diverses tensions nécessaires, et de relier chaque circuit avec le bon régulateur.

Le Dynamic Voltage Scaling et le Frequency Scaling modifier

Les fabricants de CPU ont eu l'idée de faire varier la tension et la fréquence en fonction de ce que l'on demande au processeur. Rien ne sert d'avoir un processeur qui tourne à 200 gigahertz pendant que l'on regarde ses mails. Par contre, avoir un processeur à cette fréquence peut être utile lorsque l'on joue à un jeu vidéo dernier cri. Dans ce cas, pourquoi ne pas adapter la fréquence suivant l'utilisation qui est faite du processeur ? C'est l'idée qui est derrière le Dynamic Frequency Scaling, aussi appelé DFS.

De plus, diminuer la fréquence permet de diminuer la tension d'alimentation, pour diverses raisons techniques. La technologie consistant à diminuer la tension d'alimentation suivant les besoins s'appelle le Dynamic Voltage Scaling, de son petit nom : DVS. Ces techniques sont gérées par un circuit intégré dans le processeur, qui estime en permanence l'utilisation du processeur et la fréquence/tension adaptée.

Le Power Gating et le Clock Gating modifier

Si on prend un exemple pour une maison, ne pas éclairer et/ou chauffer une pièce inutilisée évite des gaspillages. Eh bien des économies du même genre sont possibles dans un circuit imprimé. Un bon moyen de réduire la consommation électrique est simplement de couper les circuits inutilisés. Par couper, on veut dire soit ne plus les alimenter en énergie, soit les déconnecter de l'horloge. Par chance, un circuit intégré complexe est constitué de plusieurs sous-circuits distincts, aux fonctions bien délimitées. Et il est rare que tous soient utilisés en même temps. Pour économiser de l'énergie, on peut tout simplement déconnecter les sous-circuits inutilisés, temporairement.

Power Gating.

Une première solution consiste à ne pas dépenser d'énergie inutilement, ne pas alimenter ce qui ne fonctionne pas, ce qui est en pause ou inutilisé, afin qu'ils ne consommeront donc plus de courant : on parle de power gating. Elle s'implémente en utilisant des Power Gates qui déconnectent les circuits de la tension d'alimentation quand ceux-ci sont inutilisés. Cette technique est très efficace, surtout pour couper l'alimentation du cache du processeur. Cette technique réduit la consommation statique des circuits, mais pas leur consommation dynamique, par définition.

Une autre solution consiste à jouer sur la manière dont l'horloge est distribuée dans le processeur. On estime qu'une grande partie des pertes ont lieu dans l'arbre d'horloge (l'ensemble de fils qui distribuent l'horloge aux bascules), approximativement 20 à 30% (mais tout dépend du processeur). La raison est que l'horloge change d'état à chaque cycle, même si les circuits cadencés par l'horloge sont inutilisés, et que la dissipation thermique a lieu quand un bit change de valeur. S'il est possible de limiter la casse en utilisant des bascules spécialement conçues pour consommer peu, il est aussi possible de déconnecter les circuits inutilisés de l'horloge : on appelle cela le Clock Gating.

Clock gating.

Pour implémenter cette technique, on est obligé de découper le processeur en plusieurs morceaux, reliés à l'horloge. Un morceau forme un tout du point de vue de l'horloge : on pourra tous le déconnecter de l'horloge d'un coup, entièrement. Pour implémenter le Clock Gating, on dispose entre l'arbre d'horloge et le circuit, une une Clock Gate, un circuit qui inhibe l'horloge au besoin. Comme on le voit sur le schéma du dessus, ces Clock Gates sont commandées par un bit, qui ouvre ou ferme la Clock Gate Ce dernier est relié à la fameuse unité de gestion de l'énergie intégrée dans le processeur qui se charge de le commander.

Une clock gate est, dans le cas le plus simple, une vulgaire porte logique tout ce qu'il y a de plus banale. Ce peut être une porte OU ou encore une porte ET. La seule différence entre les deux est la valeur du signal d'horloge quand celle-ci est figée : soit elle est figée à 1 avec une porte OU, soit elle est figée à 0 avec une porte ET. Le bit à envoyer sur l'entrée de contrôle n'est pas le même : il faut envoyer un 1 avec une porte OU pour figer l'horloge, un 0 avec une porte ET.

Clock gate fabriquée avec une porte OU.
Clock gate fabriquée avec une porte ET.

Il est aussi possible de complexifier le circuit en ajoutant une bascule pour mémoriser le signal de contrôle avant la porte logique.

Clock gate fabriquée avec une porte ET et une bascule.

L'influence de la loi de Moore modifier

La loi de Moore a des conséquences assez intéressantes. En effet, l'évolution de la finesse de gravure permet d'augmenter le nombre de transistors et la fréquence, mais aussi de diminuer la tension d'alimentation et la capacité des transistors. Pour comprendre pourquoi, il faut savoir que le condensateur formé par la grille, l'isolant et le morceau de semi-conducteur est ce que l'on appelle un condensateur plan. La capacité de ce type de condensateur dépend de la surface de la plaque de métal (la grille), du matériau utilisé comme isolant (en fait, de sa permittivité), et de la distance entre la grille et le semi-conducteur. On peut calculer cette capacité comme suit, avec S la surface de la grille, e un paramètre qui dépend de l'isolant utilisé et d la distance entre le semi-conducteur et la grille (l'épaisseur de l'isolant, quoi).

Généralement, le coefficient e (la permittivité électrique) reste le même d'une génération de processeur à l'autre. Les fabricants ont bien tenté de diminuer celui-ci, et trouver des matériaux ayant un coefficient faible n'a pas été une mince affaire. Le dioxyde de silicium pur a longtemps été utilisé, celui-ci ayant une permittivité de 4,2, mais il ne suffit plus de nos jours. De nombreux matériaux sont maintenant utilisés, notamment des terres rares. Leur raréfaction laisse planer quelques jours sombres pour l'industrie des processeurs et de la microélectronique en général. Dans les faits, seuls les coefficients S et d vont nous intéresser.

Les équations de Dennard modifier

Si la finesse de gravure diminue de , la distance d va diminuer du même ordre. Quant à la surface S, elle va diminuer du carré de , c’est-à-dire qu'elle sera divisée par 2. La capacité totale sera donc divisée par tous les deux ans. La fréquence suit le même motif. Cela vient du fait que la période de l'horloge correspond grosso modo au temps qu'il faut pour remplir ou vider l'armature de la grille. Dans ces conditions, diminuer la capacité diminue le temps de remplissage/vidange, ce qui diminue la période et fait augmenter la fréquence. Les conséquences d'une diminution par de la finesse de gravure sont résumées dans le tableau ci-dessous. L'ensemble porte le nom de lois de Dennard, du nom de l'ingénieur qui a réussi à démontrer ces équations à partir des lois de la physique des semi-conducteurs.

Paramètre Coefficient multiplicateur (tous les deux ans)
Finesse de gravure
Nombre de transistors par unité de surface
Tension d'alimentation
Capacité d'un transistor
Fréquence

Plus la finesse de gravure est faible, plus le composant électronique peut fonctionner avec une tension d'alimentation plus faible. La tension d'alimentation est proportionnelle à la finesse de gravure : diviser par deux la finesse de gravure divisera la tension d'alimentation par deux. La finesse de gravure étant divisée par la racine carrée de deux tous les deux ans, la tension d'alimentation fait donc de même. Cela a pour conséquence de diviser la consommation énergétique par deux, toutes choses étant égales par ailleurs. Mais dans la réalité, les choses ne sont pas égales par ailleurs : la hausse du nombre de transistors va compenser exactement l'effet de la baisse de tension. À tension d'alimentation égale, le doublement du nombre de transistors tous les deux ans va faire doubler la consommation énergétique, ce qui compensera exactement la baisse de tension.

La finesse de gravure permet aussi de faire diminuer la capacité d'un transistor. Comme pour la tension d'alimentation, une division par deux de la finesse de gravure divise par deux la capacité d'un transistor. Dit autrement, la capacité d'un transistor est divisée par la racine carrée de deux tous les deux ans. On peut remarquer que cela compense exactement l'effet de la fréquence sur la consommation énergétique. La fréquence étant multipliée par la racine carrée de deux tous les deux ans, la consommation énergétique ferait de même toutes choses étant égales par ailleurs.

Si on fait le bilan, la consommation énergétique des processeurs ne change pas avec le temps, du moins si ceux-ci gardent la même surface. Cette conservation de la consommation énergétique se fait cependant avec une augmentation de performance. Ainsi, la performance par watt d'un processeur augmente avec le temps. L'augmentation de performance étant de 80 % par an pour un processeur, on déduit rapidement que la performance par watt augmente de 80 % tous les deux ans. Du moins, c'est la théorie. Théorie qui fonctionnait bien il y a encore quelques années, mais qui ne se traduit plus dans les faits depuis la commercialisation des premiers processeurs Intel Core. Depuis, on observe que le nombre de transistors et la finesse de gravure suivent la tendance indiquée par la loi de Moore, mais cela ne permet plus de faire baisser la tension ou la fréquence. L'ère des équations de Dennard est aujourd'hui révolue, reste à vous expliquer pourquoi.

La fin des équations de Dennard modifier

La raison principale de la fin des équations de Dennard tient dans la consommation statique et plus précisément dans l'existence des courants de fuite. Les courants de fuite traversent l'isolant du transistor, qui a une certaine résistance électrique. Or, quand on fait passer un courant dans une résistance, il se forme une tension à ses bornes, appelée tension de seuil. Cette tension de seuil est proportionnelle au courant et à la résistance (c'est la fameuse loi d'Ohm vue au collège). On ne peut pas faire fonctionner un transistor si la tension d'alimentation (entre source et drain) est inférieure à la tension de seuil. C'est pour cela que ces dernières années, la tension d'alimentation des processeurs est restée plus ou moins stable, à une valeur proche de la tension de seuil (1 volt, environ). Il faut alors trouver autre chose pour limiter la consommation à des niveaux soutenables.

Les concepteurs de processeurs ne pouvaient pas diminuer la fréquence pour garder une consommation soutenable, et ont donc préféré augmenter le nombre de cœurs. L'augmentation de consommation énergétique ne découle que de l'augmentation du nombre de transistors et des diminutions de capacité. Et la diminution de 30 % tous les deux ans de la capacité ne compense plus le doublement du nombre de transistors : la consommation énergétique augmente ainsi de 40 % tous les deux ans. Bilan : la performance par watt stagne. Et ce n'est pas près de s'arranger tant que les tensions de seuil restent ce qu'elles sont.


Les bus et liaisons point à point modifier

Tout ordinateur contient au minimum un bus, qui sert à connecter processeur, mémoire et entrées-sorties. Mais c'est là le cas le plus simple : rien n’empêche un ordinateur d'avoir plusieurs bus. c'est le cas sur les architectures Harvard, qui ont un bus séparé pour la mémoire RAM et la mémoire ROM. Comme autre possibilité, on pourrait avoir un bus entre processeur et mémoires, et un autre bus qui connecte le processeur aux entrées-sorties. Bref, les possibiliés sont multiples. Dans un ordinateur de type PC, les composants sont placés sur un circuit imprimé (la carte mère), sur lequel on vient connecter les différents composants d'un ordinateur et qui les relie via divers bus. Si je dis "par divers bus", c'est parce qu'il n'y a pas qu'un seul bus dans un ordinateur, mais plusieurs : un bus pour communiquer avec le disque dur, un bus pour la carte graphique, un pour le processeur, un pour la mémoire, etc. De ce fait, un PC moderne contient un nombre impressionnant de bus, jugez plutôt :

  • les bus USB ;
  • le bus PCI Express, utilisé pour communiquer avec des cartes graphiques ou des cartes son ;
  • le bus S-ATA et ses variantes eSATA, eSATAp, ATAoE, utilisés pour communiquer avec le disque dur ;
  • le bus Low Pin Count, qui permet d'accéder au clavier, aux souris, au lecteur de disquette, et aux ports parallèle et série ;
  • le SMBUS, qui est utilisé pour communiquer avec les ventilateurs, les sondes de température et les sondes de tension présentes un peu partout dans notre ordinateur ;
  • l'Intel QuickPath Interconnect et l'HyperTransport, qui relient les processeurs récents au reste de l'ordinateur ;

Et c'est oublier tous les bus aujourd'hui défunts, mais qui étaient utilisés sur les anciens PC. Comme exemples, on pourrait citer :

  • le bus ISA et le bus PCI (l'ancêtre du PCI Express), autrefois utilisés pour les cartes d'extension ;
  • le bus AGP, autrefois utilisé pour les cartes graphiques ;
  • les bus P-ATA et SCSI, pour les disque durs ;
  • le bus MIDI, qui servait pour les cartes son ;
  • le fameux RS-232 utilisé dans les ports série ;
  • enfin, le bus IEEE-1284 utilisé pour le port parallèle.

Et à ces bus reliés aux périphériques, il faudrait rajouter le bus mémoire qui connecte processeur et mémoire, le bus système et bien d'autres. La longue liste précédente sous-entend qu'il existe de nombreuses différences entre les bus. Et c'est le cas : ces différents bus sont très différents les uns des autres.

Les bus dédiés et multiplexés modifier

Commençons par parler de la distinction entre les bus (et plus précisément les bus dits multiplexés) et les liaisons point à point (aussi appelées bus dédiés).

Petite précision de vocabulaire : Le composant qui envoie une donnée sur un bus est appelé un émetteur, alors que ceux reçoivent les données sont appelés récepteurs.

Les liaisons point à point (bus dédiés) modifier

Les bus dédiés se contentent de connecter deux composants entre eux. Un autre terme, beaucoup utilisé dans le domaine des réseaux informatiques, est celui de liaisons point-à-point. Pour en donner un exemple, le câble réseau qui relie votre ordinateur à votre box internet est une liaison point à point. Mais le terme est plus large que cela et regroupe tout ce qui connecte deux équipements informatiques/électroniques entre eux, et qui permet l'échange de données. Par exemple, le câble qui relie votre ordinateur à votre imprimante est lui aussi une liaison point à point, au même titre que les liaisons USB sur votre ordinateur. De même, certaines liaisons point à point relient des composants à l'intérieur d'un ordinateur, comme le processeur et certains capteurs de températures.

Les liaisons point à point sont classés en trois types, suivant les récepteurs et les émetteurs.

Type de bus Description
Simplex Les informations ne vont que dans un sens : un composant est l'émetteur et l'autre reste à tout jamais récepteur.
Half-duplex Il est possible d'être émetteur ou récepteur, suivant la situation. Par contre, impossible d'être en même temps émetteur et récepteur.
Full-duplex Il est possible d'être à la fois récepteur et émetteur.
Liaison simplex Liaison half-duplex Liaison full-duplex

Les bus full duplex sont créés en regroupant deux bus simplex ensemble : un pour l'émission et un pour la réception. Mais certains bus full-duplex, assez rares au demeurant, n'utilisent pas cette technique et se contentent d'un seul bus bidirectionnel.

Les bus multiplexés modifier

Les liaisons point à point, ou bus dédiés, sont à opposer aux bus proprement dit, aussi appelés bus multiplexés. Ces derniers ne sont pas limités à deux composants et peuvent interconnecter un grand nombre de circuits électroniques. Par exemple, un bus peut interconnecter la mémoire RAM, le processeur et quelques entrées-sorties entre eux. Et cela fait qu'il existe quelques différences entre un bus et une liaison point à point.

Avec un bus, l'émetteur envoie ses données à tous les autres composants reliés aux bus, à tous les récepteurs. Sur tous ces récepteurs, il se peut que seul l'un d'entre eux soit le destinataire de la donnée : les autres vont alors l'ignorer, seul le destinataire la traite. Cependant, il se peut qu'il y ait plusieurs récepteurs comme destinataires : dans ce cas, les destinataires vont tous recevoir la donnée et la traiter. Les bus permettent donc de faire des envois de données à plusieurs composants en une seule fois.

Bus multiplexés.

La fréquence du bus et son caractère synchrone/asynchrone modifier

On peut faire la différence entre bus synchrone et asynchrone, la différence se faisant selon l'usage ou non d'une horloge. La méthode de synchronisation des composants et des communications sur le bus peut ainsi utiliser une horloge, ou la remplacer par des mécanismes autres.

Les bus synchrones modifier

Certains bus sont synchronisés sur un signal d'horloge : ce sont les bus synchrones. Avec ces bus, le temps de transmission d'une donnée est fixé une fois pour toute. Le composant sait combien de cycles d'horloge durent une lecture ou une écriture. Sur certains bus, le contenu du bus n'est pas mis à jour à chaque front montant, ou à chaque front descendant, mais aux deux : fronts montant et descendant. De tels bus sont appelés des bus double data rate. Cela permet de transférer deux données sur le bus (une à chaque front) en un seul cycle d'horloge : le débit binaire est doublé sans toucher à la fréquence du bus.

Exemple de lecture sur un bus synchrone.

Les bus asynchrones modifier

À haute fréquence, le signal d'horloge met un certain temps pour se propager à travers le fil d'horloge, ce qui induit un léger décalage entre les composants. Plus on augmente la longueur des fils, plus ces décalages deviendront ennuyeux. Plus on augmente la fréquence, plus la période diminue comparée au temps de propagation de l'horloge dans le fil. Ces phénomènes font qu'il est difficile d'atteindre des fréquences de plusieurs gigahertz sur les bus actuels. Pour ces raisons, certains bus se passent complètement de signal d'horloge, et ont un protocole conçu pour : ce sont les bus asynchrones. Ces bus sont donc très adaptés pour transmettre des informations sur de longues distances (plusieurs centimètres ou plus).

Exemple d'écriture sur un bus asynchrone

La largeur du bus modifier

Comparaison entre bus série et parallèle.

La plupart des bus peuvent échanger plusieurs bits en même temps et sont appelés bus parallèles. Mais il existe des bus qui ne peuvent échanger qu'un bit à la fois : ce sont des bus série.

Les bus série modifier

On pourrait croire qu'un bus série ne contient qu'un seul fil pour transmettre les données, mais il existe des contrexemples. Généralement, c'est le signe que le bus n'utilise pas un codage NRZ, mais une autre forme de codage un peu plus complexe. Par exemple, le bus USB utilise deux fils D+ et D- pour transmettre un bit. Pour faire simple, lorsque le fil D+ est à sa tension maximale, l'autre est à zéro (et réciproquement).

La transmission et la réception sur un bus série demande de faire une conversion entre les données, qui sont codées sur plusieurs bits, et le flux série à envoyer sur le bus. Cela s'effectue généralement en utilisant des registres à décalage, commandés par des circuits de contrôle.

Interface de conversion série-parallèle (UART).

Les bus parallèles modifier

Passons maintenant aux bus parallèles. Pour information, si le contenu d'un bus de largeur de bits est mis à jour fois par secondes, alors son débit binaire est de . Mais contrairement à ce qu'on pourrait croire, les bus parallèles ne sont pas plus rapides que les bus série. Sur les bus synchrones, la fréquence est bien meilleure pour les bus série que pour les bus parallèles. La fréquence plus élevée l'emporte sur la largeur plus faible, ce qui surcompense le fait qu’un bus série ne peut envoyer qu'un bit à la fois. Le même problème se pose pour les bus asynchrones : le temps entre deux transmissions est plus grand sur les bus parallèles, alors qu'un bus série n'a pas ce genre de problèmes.

Il existe plusieurs raisons à cela, qui proviennent de phénomènes électriques assez subtils. Premièrement, les fils d'un bus ne sont pas identiques électriquement : leur longueur et leur résistance changent très légèrement d'un fil à l'autre. En conséquence, un bit va se propager à des vitesses différentes suivant le fil. On est obligé de se caler sur le fil le plus lent pour éviter des problèmes à la réception. En second lieu, il y a le phénomène de crosstalk. Lorsque la tension à l'intérieur du fil varie (quand le fil passe de 0 à 1 ou inversement), le fil va émettre des ondes électromagnétiques qui perturbent les fils d'à côté. Il faut attendre que la perturbation électromagnétique se soit atténuée pour lire les bits, ce qui limite le nombre de changements d'état du bus par seconde.


On a vu dans le chapitre précédent qu'il faut distinguer les liaisons point à point des bus de communication. Dans ce chapitre, nous allons voir tout ce qui a trait aux liaisons point à point, à savoir comment les données sont transmises sur de telles liaisons, comment l'émetteur et le récepteur s'interfacent, etc. Gardez cependant à l'esprit que tout ce qui sera dit dans ce chapitre vaut aussi bien pour les liaisons point à point que pour les bus de communication. En effet, les liaisons point à point font face aux même problèmes que les bus de communication, si ce n'est la gestion de l'arbitrage.

Les codes en ligne : le codage des bits sur une ligne de transmission modifier

Chaque fil d'un bus transmet un signal, qui peut être codé de diverses manières. Il existe des méthodes relativement nombreuses pour coder un bit de données pour le transmettre sur un bus : ces méthodes sont appelées des codages en ligne. Toutes codent celui-ci avec une tension, qui peut prendre un état haut (tension forte) ou un état bas (tension faible, le plus souvent proche de 0 volts). Outre le codage des données, il faut prendre aussi en compte le codage des commandes. En effet, certains bus série utilisent des fils dédiés pour la transmission des bits de données et de commande. Cela permet d'éviter d'utiliser trop de fils pour un même procédé.

Les codages non-différentiels modifier

Pour commencer, nous allons voir les codages qui permettent de transférer un bit sur un seul fil (nous verrons que d'autres font autrement, mais laissons cela à plus tard). Il en existe de toutes sortes, qui se distinguent par des caractéristiques électriques qui sont à l’avantage ou au désavantage de l'un ou l'autre suivant la situation : meilleur spectre de bande passante, composante continue nulle/non-nulle, etc. Les plus courants sont les suivants :

  • Le codage NRZ-L utilise l'état haut pour coder un 1 et l'état bas pour le zéro (ou l'inverse).
  • Le codage RZ est similaire au codage NRZ, si ce n'est que la tension retourne systématiquement à l'état bas après la moitié d'un cycle d'horloge. Celui-ci permet une meilleure synchronisation avec le signal d'horloge, notamment dans les environnements bruités.
  • Le codage NRZ-M fonctionne différemment : un état haut signifie que le bit envoyé est l'inverse du précédent, tandis que l'état bas indique que le bit envoyé est identique au précédent.
  • Le codage NRZ-S est identique au codage NRZ-M si ce n'est que l'état haut et bas sont inversés.
  • Avec le codage Manchester, aussi appelé codage biphasé, un 1 est codé par un front descendant, alors qu'un 0 est codé par un front montant (ou l'inverse, dans certaines variantes).
Illustration des différents codes en ligne.

Faisons une petite remarque sur le codage de Manchester : il s'obtient en faisant un XOR entre l'horloge et le flux de bits à envoyer (codé en NRZ-L). Les bits y sont donc codés par des fronts montants ou descendants, et l'absence de front est censé être une valeur invalide. Si je dis censé, c'est que de telles valeurs invalides peuvent avoir leur utilité, comme nous le verrons dans le chapitre sur la couche liaison. Elles peuvent en effet servir pour coder autre chose que des bits, comme des bits de synchronisation entre émetteur et récepteur, qui ne doivent pas être confondus avec des bits de données. Mais laissons cela à plus tard.

Codage de Manchester.

Les codages différentiels modifier

Pour plus de fiabilité, il est possible d'utiliser deux fils pour envoyer un bit (sur un bus série). Ces deux fils ont un contenu qui est inversé électriquement : le premier utilise une tension positive pour l'état haut et le second une tension négative. Ce faisant, on utilise la différence de tension pour coder le bit. Un tel codage est appelé un codage différentiel.

Ce codage permet une meilleure résistance aux perturbations électromagnétiques, aux parasites et autres formes de bruits qui peuvent modifier les bits transmis. L'intérêt d'un tel montage est que les perturbations électromagnétiques vont modifier la tension dans les deux fils, la variation induite étant identique dans chaque fil. La différence de tension entre les deux fils ne sera donc pas influencée par la perturbation.

Évidemment, chaque codage a son propre version différentielle, à savoir avec deux fils de transmission.

Ce type de codage est, par exemple, utilisé sur le protocole USB. Sur ce protocole, deux fils sont utilisés pour transmettre un bit, via codage différentiel. Dans chaque fil, le bit est codé par un codage NRZ-I.

Signal USB : exemple.

Les codes redondants modifier

D'autres bus encodent un bit sur plusieurs fils, mais sans pour autant utiliser de codage différentiel. Il s'agit des codes redondants, dans le sens où ils dupliquent de l'information, ils dupliquent des bits. L'intérêt est là encore de rendre le bus plus fiable, d'éliminer les erreurs de transmission. Avec eux, chaque bit est encodé en utilisant plusieurs bits. La méthode la plus simple est la suivante : le bit est envoyé à l'identique sur deux fils. Si jamais les deux bits sont différents à l'arrivée, alors il y a un problème. Une autre méthode encode un 1 avec deux bits identiques, et un 0 avec deux bits différents, comme illustré ci-dessous. Mais ce genre de redondance est rarement utilisé, vu qu'on lui préfère des systèmes de détection/correction d'erreur comme un bit de parité.

Code rendondant sur 2 bits.
Protocole 3 états

Les codes redondants sont aussi utilisés pour faire communiquer entre eux des composants asynchrones, à savoir deux composants qui ne ne sont pas synchronisés par l'intermédiaire d'une horloge. Il s'agit d'ailleurs de leur utilisation principale. Vu que les composants ne sont pas synchronisés par une horloge, il se peut que certains bits arrivent avant les autres lors de la transmission d'une donnée sur une liaison parallèle. L'usage d'un code redondant permet de savoir quels bits sont valides et ceux pas encore transmis.

Une méthode pour cela est celle illustrée ci-contre. On utilise encore deux bits pour en coder un seul, mais les deux bits doivent être différents. Si les deux bits sont identiques, on fait soit face à un état interdit, soit à un état de transition et on doit attendre que le bus se stabilise.

L'ordre d'envoi des bits sur une liaison série modifier

Sur une liaison ou un bus série, les bits sont envoyés uns par uns. L'intuition nous dit que l'on peut procéder de deux manières : soit on envoie la donnée en commençant par le bit de poids faible, soit on commence par le bit de poids fort. Les deux méthodes sont valables et tout n'est au final qu'une question de convention. Les deux méthodes sont appelées LSB0 et MSB0. Avec la convention LSB0, le bit de poids faible est envoyé en premier, puis on parcourt la donnée de gauche à droite, jusqu’à atteindre le bit de poids fort. Avec la convention MSB0, c'est l'inverse ; on commence par le bit de poids fort, on parcours la donnée de gauche à droite, jusqu'à arriver au bit de poids faible.

Exemple sur un octet (groupe de 8 bits) :

Convention de numérotation LSB0 : Le premier bit transmis (bit 0) est celui de poids faible (LSB)
  • (bit numéro 7) Le bit de poids fort (MSB), celui le plus à gauche, vaut 1 (poids 27).
  • (bit numéro 0) Le bit de poids faible (LSB), celui le plus à droite, vaut 0 (poids 20).
Convention de numérotation MSB0 : Le premier bit transmis (bit 0) est celui de poids fort (MSB)
  • (bit numéro 0) Le bit de poids fort (MSB), celui le plus à gauche, vaut 1 (poids 27).
  • (bit numéro 7) Le bit de poids faible (LSB), celui le plus à droite, vaut 0 (poids 20).

Le codage des trames : début et de la fin de transmission modifier

Deux composants électroniques communiquent entre eux en s'envoyant des trames, des paquets de bits où chaque information nécessaire à la transmission est à une place précise. Le terme trame réfère aux données transmises : toutes les informations nécessaires pour une transmission sont regroupées dans une trame, qui est envoyée telle quelle sur le bus. Le codage des trames indique comment interpréter les données transmises.Le but est que le récepteur puisse extraire des informations utiles du flux de bits transmis : quelle est l'adresse du récepteur, quand la transmission se termine-t-elle, et bien d'autres. Les transmissions sur un bus sont standardisées de manière à rendre l'interprétation du flux de bit claire et sans ambiguïté.

Le terme trame est parfois réservé au cas où le paquet est envoyé en plusieurs fois, mais ce n'est pas l'usage que nous ferons de ce terme dans ce qui suit.

Le transfert d'une trame est soumis à de nombreuses contraintes, qui rendent le codage de la trame plus ou moins simple. Le cas le plus simple sont ceux où la trame a une taille inférieur ou égale à la largeur du bus, ce qui permet de l'envoyer en une seule fois, d'un seul coup. Cela simplifie fortement le codage de la trame, vu qu'il n'y a pas besoin de coder la longueur de la trame ou de préciser le début et la fin de la transmission. Mais ce cas est rare et n'apparait que sur certains bus parallèles conçus pour. Sur les autres bus parallèles, plus courants, une trame est envoyée morceau par morceau, chaque morceau ayant la même taille que le bus. Sur les bus série, les trames sont transmises bit par bit grâce à des circuits spécialisés. La trame est mémorisée dans un registre à décalage, qui envoie celle-ci bit par bit sur sa sortie (reliée au bus).

Il arrive qu'une liaison point à point soit inutilisée durant un certain temps, sans données transmises. Émetteur et récepteur doivent donc déterminer quand la liaison est inutilisée afin de ne pas confondre l'état de repos avec une transmission de données. Une transmission est un flux de bits qui a un début et une fin : le codage des trames doit indiquer quand commence une transmission et quand elle se termine. Le récepteur ne reçoit en effet qu'un flux de bits, et doit détecter le début et la fin des trames. Ce processus de segmentation d'un flux de bits en trames n’est cependant pas simple et l'émetteur doit fatalement ajouter des bits pour coder le début et la fin de la trame.

Ajouter un bit sur le bus de commande modifier

Pour cela, on peut ajouter un bit au bus de commande, qui indique si le bus est en train de transmettre une trame ou s'il est inactif. Cette méthode est très utilisée sur les bus mémoire, à savoir le bus qui relie le processeur à une mémoire. Il faut dire que de tels bus sont généralement assez simples et ne demandent pas un codage en trame digne de ce nom. Les commandes sont envoyées à la mémoire en une fois, parfois en deux fois, guère plus. Mais il y a moyen de se passer de ce genre d'artifice avec des méthodes plus ingénieuses, qui sont utilisées sur des bus plus complexes, destinés aux entrées-sorties.

Inactiver la liaison à la fin de l'envoi d'une trame modifier

Une première solution est de laisser la liaison complètement inactive durant un certain temps, entre l'envoi de deux trames. La liaison reste à 0 Volts durant un temps fixe à la fin de l'émission d'une trame. Les composants détectent alors ce temps mort et en déduisent que l'envoi de la trame est terminée. Malheureusement, cette méthode pose quelques problèmes.

  • Premièrement, elle réduit les performances. Une bonne partie du débit binaire de la liaison passe dans les temps morts de fin de trame, lorsque la liaison est inactivée.
  • Deuxièmement, certaines trames contiennent de longues suites de 0, qui peuvent être confondues avec une liaison inactive.

Dans ce cas, le protocole de couche liaison peut résoudre le problème en ajoutant des bits à 1, dans les données de la trame, pour couper le flux de 0. Ces bits sont identifiés comme tel par l'émetteur, qui reconnait les séquences de bits problématiques.

Les octets START et STOP modifier

De nos jours, la quasi-totalité des protocoles utilisent la même technique : ils placent un octet spécial (ou une suite d'octet) au début de la trame, et un autre octet spécial pour la fin de la trame. Ces octets de synchronisation, respectivement nommés START et STOP, sont standardisés par le protocole.

Problème : il se peut qu'un octet de la trame soit identique à un octet START ou STOP. Pour éviter tout problème, ces pseudo-octets START/STOP sont précédés par un octet d'échappement, lui aussi standardisé, qui indique qu'ils ne sont pas à prendre en compte. Les vrais octets START et STOP ne sont pas précédés de cet octet d'échappement et sont pris en compte, là où les pseudo-START/STOP sont ignorés car précédés de l'octet d'échappement. Cette méthode impose au récepteur d'analyser les trames, pour détecter les octets d'échappements et interpréter correctement le flux de bits reçu. Mais cette méthode a l'avantage de gérer des trames de longueur arbitrairement grandes, sans vraiment de limites.

Trame avec des octets d'échappement.

Une autre solution consiste à remplacer l'octet/bit STOP par la longueur de la trame. Immédiatement à la suite de l'octet/bit START, l'émetteur va envoyer la longueur de la trame en octet ou en bits. Cette information permettra au récepteur de savoir quand la trame se termine. Cette technique permet de se passer totalement des octets d'échappement : on sait que les octets START dans une trame sont des données et il n'y a pas d'octet STOP à échapper. Le récepteur a juste à compter les octets qu'il reçoit et 'a pas à détecter d'octets d'échappements. Avec cette approche, la longueur des trames est bornée par le nombre de bits utilisés pour coder la longueur. Dit autrement, elle ne permet pas de trames aussi grandes que possibles.

Trame avec un champ "longueur".

Dans le cas où les trames ont une taille fixe, à savoir que leur nombre d'octet ne varie pas selon la trame, les deux techniques précédentes sont inutiles. Il suffit d'utiliser un octet/bit de START, les récepteurs ayant juste à compter les octets envoyés à sa suite. Pas besoin de STOP ou de coder la longueur de la trame.

Les bits de START/STOP modifier

Il arrive plus rarement que les octets de START/STOP soient remplacés par des bits spéciaux ou une séquence particulière de fronts montants/descendants.

Une possibilité est d'utiliser les propriétés certains codages, comme le codage de Manchester. Dans celui-ci, un bit valide est représenté par un front montant ou descendant, qui survient au beau milieu d'une période. L'absence de fronts durant une période est censé être une valeur invalide, mais les concepteurs de certains bus ont décidé de l'utiliser comme bit de START ou STOP. Cela donne du sens aux deux possibilités suivantes : la tension reste constante durant une période complète, soit à l'état haut, soit à l'état bas. Cela permet de coder deux valeurs supplémentaires : une où la tension reste à l'état haut, et une autre où la tension reste à l'état bas. La première valeur sert de bit de START, alors que l'autre sert de bit de STOP. Cette méthode est presque identique aux octets de START et de STOP, sauf qu'elle a un énorme avantage en comparaison : elle n'a pas besoin d'octet d'échappement dans la trame, pas plus que d'indiquer la longueur de la trame.

Un autre exemple est celui des bus RS-232, RS-485 et I²C, où les bits de START et STOP sont codés par des fronts sur les bus de données et de commande.

La fiabilité des transmissions sur une liaison point à point et/ou un bus modifier

Lorsqu'une trame est envoyée, il se peut qu'elle n'arrive pas à destination correctement. Des parasites peuvent déformer la trame et/ou en modifier des bits au point de la rendre inexploitable. Dans ces conditions, il faut systématiquement que l'émetteur et le récepteur détectent l'erreur : ils doivent savoir que la trame n'a pas été transmise ou qu'elle est erronée.

Les techniques de détection et de correction d'erreurs modifier

Pour cela, il existe diverses méthodes de détection et de correction d'erreur, que nous avons abordées en partie dans les premiers chapitres du cours. On en distingue deux classes : celles qui ne font que détecter l'erreur, et celles qui permettent de la corriger. Tous les codes correcteurs et détecteurs d'erreur ajoutent tous des bits aux données de base, ces bits étant appelés des bits de correction/détection d'erreur. Ces bits servent à détecter et éventuellement corriger toute erreur de transmission/stockage. Plus le nombre de bits ajoutés est important, plus la fiabilité des données sera importante.

Dans le cas le plus simple, on se contente d'un simple bit de parité. C'est par exemple ce qui est fait sur les bus ATA qui relient le disque dur à la carte mère, mais aussi sur les premières mémoires RAM des PC. Dans d'autres cas, on peut ajouter une somme de contrôle ou un code de Hamming à la trame, ce qui permet de détecter les erreurs de transmission. Mais cet usage de l'ECC est beaucoup plus rare. On trouve quelques carte mères qui gèrent l'ECC pour la communication avec la RAM, mais elles sont surtout utilisées sur les serveurs.

Les méthodes de retransmission modifier

Si l'erreur peut être corrigée par le récepteur, tout va bien. Mais il arrive souvent que ce ne soit pas le cas : l'émetteur doit alors être prévenu et agir en conséquence. Pour cela, le récepteur peut envoyer une trame à l'émetteur qui signifie : la trame précédente envoyée est invalide. Cette trame est appelée un accusé de non-réception. La trame fautive est alors renvoyée au récepteur, en espérant que ce nouvel essai soit le bon. Mais cette méthode ne fonctionne pas si la trame est tellement endommagée que le récepteur ne la détecte pas. Pour éviter ce problème, on utilise une autre solution, beaucoup plus utilisée dans le domaine du réseau. Celle-ci utilise des accusés de réception, à savoir l'inverse des accusés de non-réception. Ces accusés de réception sont envoyés à l'émetteur pour signifier que la trame est valide et a bien été reçue. Nous les noterons ACK dans ce qui suivra.

Après avoir envoyé une trame, l'émetteur va attendra un certain temps que l'ACK correspondant lui soit envoyé. Si l’émetteur ne reçoit pas d'ACK pour la trame envoyée, il considère que celle-ci n'a pas été reçue correctement et la renvoie. Pour résumer, on peut corriger et détecter les erreurs avec une technique qui mélange ACK et durée d'attente : après l'envoi d'une trame, on attend durant un temps nommé time-out que l'ACK arrive, et on renvoie la trame au bout de ce temps si non-réception. Cette technique porte un nom : on parle d'Automatic repeat request.

Le protocole Stop-and-Wait modifier

Dans le cas le plus simple, les trames sont envoyées unes par unes au rythme d'une trame après chaque ACK. En clair, l'émetteur attend d'avoir reçu l'ACK de la trame précédente avant d'en envoyer une nouvelle. Parmi les méthodes de ce genre, la plus connue est le protocole Stop-and-Wait.

Cette méthode a cependant un problème pour une raison simple : les trames mettent du temps avant d'atteindre le récepteur, de même que les ACK mettent du temps à faire le chemin inverse. Une autre conséquence des temps de transmission est que l'ACK peut arriver après que le time-out (temps d'attente avant retransmission de la trame) soit écoulé. La trame est alors renvoyée une seconde fois avant que son ACK arrive. Le récepteur va alors croire que ce second envoi est en fait l'envoi d'une nouvelle trame !

Pour éviter cela, la trame contient un bit qui est inversé à chaque nouvelle trame. Si ce bit est le même dans deux trames consécutives, c'est que l'émetteur l'a renvoyée car l'ACK était en retard. Mais les temps de transmission ont un autre défaut avec cette technique : durant le temps d'aller-retour, l'émetteur ne peut pas envoyer de nouvelle trame et doit juste attendre. Le support de transmission n'est donc pas utilisé de manière optimale et de la bande passante est gâchée lors de ces temps d'attente.

Les protocoles à fenêtre glissante modifier

Les deux problèmes précédents peuvent être résolus en utilisant ce qu'on appelle une fenêtre glissante. Avec cette méthode, les trames sont envoyées les unes après les autres, sans attendre la réception des ACKs. Chaque trame est numérotée de manière à ce que l'émetteur et le récepteur puisse l’identifier. Lorsque le récepteur envoie les ACK, il précise le numéro de la trame dont il accuse la réception. Ce faisant, l'émetteur sait quelles sont les trames qui ont été reçues et celles à renvoyer (modulo les time-out de chaque trame).

On peut remarquer qu'avec cette méthode, les trames sont parfois reçues dans le désordre, alors qu'elles ont été envoyées dans l'ordre. Ce mécanisme permet donc de conserver l'ordre des données envoyées, tout en garantissant le fait que les données sont effectivement transmises sans problèmes. Avec cette méthode, l'émetteur va accumuler les trames à envoyer/déjà envoyées dans une mémoire. L'émetteur devra gérer deux choses : où se situe la première trame pour laquelle il n'a pas d'ACK, et la dernière trame envoyée. La raison est simple : la prochaine trame à envoyer est l'une de ces deux trames. Tout dépend si la première trame pour laquelle il n'a pas d'ACK est validée ou non. Si son ACK n'est pas envoyé, elle doit être renvoyée, ce qui demande de savoir quelle est cette trame. Si elle est validée, l'émetteur pourra envoyer une nouvelle trame, ce qui demande de savoir quelle est la dernière trame envoyée (mais pas encore confirmée). Le récepteur doit juste mémoriser quelle est la dernière trame qu'il a reçue. Lui aussi va devoir accumuler les trames reçues dans une mémoire, pour les remettre dans l'ordre.


Il y a quelques chapitres, nous avons vu la différence entre bus et liaison point à point : là où ces dernières ne connectent que deux composants, les bus de communication en connectent bien plus. Ce faisant, les bus de communication font face à de nouveaux problèmes, inconnus des liaisons point à point. Et ce sont ces problèmes qui font l'objet de ce chapitre. Autant le chapitre précédent valait à la fois pour les liaisons point à point et les bus, autant ce n'est pas le cas de celui-ci. Ce chapitre va parler de ce qui n'est valable que pour les bus de communication, comme leur arbitrage, la détection des collisions, etc. Tous ces problèmes ne peuvent pas survenir, par définition, sur les liaisons point à point.

L'adressage du récepteur modifier

Schéma d'un bus.

La trame doit naturellement être envoyée à un récepteur, seul destinataire de la trame. Sur les liaisons point à point, il n'y a pas besoin de préciser quel est le récepteur. Mais sur les bus, c'est une toute autre histoire. Tous les composants reliés aux bus sont de potentiels récepteurs et l'émetteur doit préciser à qui la trame est destinée. Pour résoudre ce problème, chaque composant se voit attribuer une adresse, il est « numéroté ». Cela fonctionne aussi pour les composants qui sont des périphériques.

L'adressage sur les bus parallèles et série modifier

Sur les bus parallèles, l'adresse est généralement transmise sur des fils à part, sur un sous-bus dédié appelé le bus d'adresse. En général, les adresses sur les bus pour périphériques sont assez petites, de quelques bits dans le cas le plus fréquent, quelques octets tout au plus. Il n'y a pas besoin de plus pour adresser une centaine de composants ou plus. Les seuls bus à avoir des adresses de plusieurs octets sont les bus liés aux mémoires, ou ceux qui ont un rapport avec les réseaux informatiques.

Les bus multiplexés utilisent une astuce pour économiser des fils et des broches. Un bus multiplexé sert alternativement de bus de donnée ou d'adresse, suivant la valeur d'un bit du bus de commande. Ce dernier, le bit Adress Line Enable (ALE), précise si le contenu du bus est une adresse ou une donnée : il vaut 1 quand une adresse transite sur le bus, et 0 si le bus contient une donnée.

Un défaut de ces bus est que les transferts sont plus lents, car l'adresse et la donnée ne sont pas envoyées en même temps lors d'une écriture. Un autre problème des bus multiplexé est qu'ils ont a peu près autant de bits pour coder l'adresse que pour transporter les données. Par exemple, un bus multiplexé de 8 bits transmettra des adresses de 16 bits, mais aussi des données de 16 bits. Ils sont donc moins versatiles, mais cela pose problème sur les bus où l'on peut connecter peu de périphériques. Dans ce cas, les adresses sont très petites et l'économie de fils est donc beaucoup plus faible.

Passons maintenant aux bus série (ou certains bus parallèles particuliers). Pour arriver à destination, la trame doit indiquer l'adresse du composant de destination. Les récepteurs espionnent le bus en permanence pour détecter les trames qui leur sont destinées. Ils lisent toutes les trames envoyées sur le bus et en extraient l'adresse de destination : si celle-ci leur correspond, ils lisent le reste de la trame, ils ne la prennent pas en compte sinon.

L'adresse en question est intégrée à la trame et est placée à un endroit précis, toujours le même, pour que le récepteur puisse l'extraire. Le plus souvent, l'adresse de destination est placée au début de la trame, afin qu'elle soit envoyée au plus vite. Ainsi, les périphériques savent plus rapidement si la trame leur est destinée ou non, l'adresse étant connue le plus tôt possible.

Le décodage d'adresse modifier

Le fait d'attribuer une adresse à chaque composant est une idée simple, mais efficace. Encore faut-il la mettre en œuvre et il existe plusieurs possibilités pour cela. Implémenter l'adressage sur un bus demande à ce que chaque composant sache d'une manière ou d'une autre que c'est à lui que l'on veut parler et pas à un autre. Lorsqu'une adresse est envoyée sur le bus, seul l'émetteur et le récepteur se connectent au bus, les autres composants ne sont pas censés réagir. Et pour cela, il existe deux possibilités : soit on délègue l'adressage au composant, soit on ajoute un circuit qui active le composant adressé et désactive les autres.

Avec la première méthode, les composants branchés sur le bus monitorent en permanence ce qui est transféré sur le bus. Quand un envoi de commande a lieu, chaque composant extrait l'adresse transmise sur le bus et vérifie si c'est bien la sienne. Si c'est le cas, le composant se connecte sur le bus et les autres composants se déconnectent. En conséquence, chaque composant contient un comparateur pour cette vérification d'adresse, dont la sortie commande les circuits trois états qui relient le contrôleur au bus. Cette méthode est particulièrement pratique sur les bus où le bus d'adresse est séparé du bus de données. Si ce n'est pas le cas, le composant doit mémoriser l'adresse transmise sur le bus dans un registre, avant de faire la comparaison? Même chose sur les bus série.

La seconde solution est celle du décodage d'adresse. Elle utilise un circuit qui détermine, à partir de l'adresse, quel est le composant adressé. Seul ce composant sera activé/connecté au bus, tandis que les autres seront désactivés/déconnectés du bus. Pour implémenter la dernière solution, chaque périphérique possède une entrée CS, qui active ou désactive le composant suivant sa valeur. Le composant se déconnecte du bus si ce bit est à 0 et est connecté s'il est à 1. Pour éviter les conflits, un seul composant doit avoir son bit CS à 1. Pour cela, il faut ajouter un circuit qui prend en entrée l'adresse et qui commande les bits CS : ce circuit est un circuit de décodage partiel d'adresse.

Décodage d'adresse sur un bus

L'interfaçage avec le bus modifier

Une fois que l'on sait quel composant a accès au bus à un instant donné, il faut trouver un moyen pour que les composants non sélectionnés par l'arbitrage ne puissent pas écrire sur le bus.

Une première solution consiste à relier les entrées/sorties des composants au bus via un multiplexeur/démultiplexeur : on est alors certain que seul un composant pourra émettre sur le bus à un moment donné. L'arbitrage du bus choisit quel composant peut émettre, et configure l'entrée de commande du multiplexeur en fonction. Les multiplexeurs et démultiplexeurs sont configurés en utilisant l'adresse du composant émetteur/récepteur.

Une autre solution consiste à connecter et déconnecter les circuits du bus selon les besoins. A un instant t, seul l'émetteur et le récepteur sont connectés au bus. Mais cela demande pouvoir déconnecter du bus les entrées/sorties qui n'envoient pas de données. Plus précisément, leurs sorties peuvent être mises dans un état de haute impédance, qui n'est ni un 0 ni un 1. Quand une sortie est en haute impédance, elle n'a pas la moindre influence sur le bus et ne peut donc pas y écrire. Tout se passe comme si elle était déconnectée du bus, et dans les faits, elle l'est souvent.

Dans le chapitre sur les circuits intégrés, nous avons vu qu'il existait trois types de sorties : les sorties totem-pole, à drain/collecteur ouvert, et trois-état. Les sorties totem-pole fournissent soit un 1, soit un zéro, et ne peuvent pas être déconnectées proprement dit. Les deux autres types de sorties en sont capables. Et nous allons les voir dans ce qui suit.

L'interfaçage avec le bus avec des circuits trois-états modifier

Le cas le plus simple est celui des sorties trois-état, qui peuvent soit fournir un 1, soit fournir un 0, soit être déconnectées. Malheureusement, les circuits intégrés normaux n'ont pas naturellement des entrées-sorties trois-état. Les portes logiques fournissent soit un 0, soit un 1, pas d'état déconnecté.

Tampons 3 états.

La solution retenue sur presque tous les circuits actuels est d'utiliser des tampons trois états. Pour rappel, nous avions vu ce circuit dans le chapitre sur les circuits intégrés, mais un rappel ne fera clairement pas de mal. Un tampon trois-états peut être vu comme une porte OUI modifiée, qui peut déconnecter sa sortie de son entrée. Un tampon trois-état possède une entrée de donnée, une entrée de commande, et une sortie : suivant ce qui est mis sur l'entrée de commande, la sortie est soit en état de haute impédance (déconnectée du bus), soit égale à l'entrée.

Commande Entrée Sortie
0 0 Haute impédance/Déconnexion
0 1 Haute impédance/Déconnexion
1 0 0
1 1 1
Tampon trois-états.

On peut utiliser ces tampons trois états pour permettre à un composant d'émettre ou de recevoir des données sur un bus. Par exemple, on peut utiliser ces tampons pour autoriser les émissions sur le bus, le composant étant déconnecté (haute impédance) s'il n'a rien à émettre. Le composant a accès au bus en écriture seule. L'exemple typique est celui d'une mémoire ROM reliée à un bus de données.

Bus en écriture seule.

Une autre possibilité est de permettre à un composant de recevoir des données sur le bus. Le composant peut alors surveiller le bus et regarder si des données lui sont transmises, ou se déconnecter du bus. Le composant a alors accès au bus en lecture seule.

Bus en lecture seule.

Évidemment, on peut autoriser lectures et écritures : le composant peut alors aussi bien émettre que recevoir des données sur le bus quand il s'y connecte. On doit alors utiliser deux circuits trois états, un pour l'émission/écriture et un autre pour la réception/lecture. Comme exemple, on pourrait citer les mémoires RAM, qui sont reliées au bus mémoire par des circuits de ce genre. Dans ce cas, les circuits trois états doivent être commandés par le bit CS (Chip Select) qui connecte ou déconnecte la mémoire du bus, mais aussi par le bit R/W (Read/Write) qui décide du sens de transfert. Pour faire la traduction entre ces deux bits et les bits à placer sur l'entrée de commande des circuits trois états, on utilise un petit circuit combinatoire assez simple.

Bus en lecture et écriture.

L'interfaçage avec le bus avec des circuits à drain/collecteur ouvert modifier

Les sorties à drain/collecteur ouvert sont plus limitées et ne peuvent prendre que deux états. Dans le cas le plus fréquent, la sortie est soit déconnectée, soit mise à 0 par le circuit intégré, mais elle ne peut pas être mise à 1 sans intervention extérieure. Pour compenser cela, le bus est relié à la tension d'alimentation à travers une résistance, appelée résistance de rappel. Cela garantit que le bus est naturellement à l'état 1, du moins tant que les sorties des composants sont déconnectées. Au repos, quand les composants n’envoient rien sur le bus, les sorties des composants sont déconnectées et les résistances de rappel mettent le bus à 1. Mais quand un seul composant met sa sortie à 0, cela force le bus à passer à 0.

Exemple de bus n'utilisant que des composants à sortie en collecteur ouvert.

Pour le dire autrement, on peut voir le contenu du bus comme un ET des bits envoyés sur les sorties des composants connectés au bus. Ce détail aura son importance par la suite. Le contenu du fil peut être lu sans altérer l'état électrique du bus/fil.

Avec cette méthode, le nombre de composants que l'on peut placer sur le bus est surtout limité par les spécifications électriques du bus, notamment sa capacité. Mais cela a l'avantage que le bus est compatible avec des technologies de fabrication totalement différentes, qu'il s'agisse de composants TTL, CMOS ou autres. En effet, la tension d'alimentation des composants TTL n'est pas la même que celle des composants CMOS. Utiliser des entrées-sorties à drain ouvert fait que l'on peut choisir la tension d'alimentation que l'on veut, et donc que l'on peut choisir entre TTL et CMOS. Par contre, on ne peut pas connecter composants TTL et CMOS avec des tensions d'alimentation différentes sur un même bus.

Il est possible de mélanger sorties à drain/collecteur ouvert, avec des entrées "trois-états" (des entrées qui peuvent soit permettre une lecture du bus, soit être déconnectées). C'est par exemple le cas sur les microprocesseurs 8051.

Port d'un 8051

L'arbitrage du bus modifier

Collisions lors de l'accès à un bus.

Sur certains bus, il arrive que plusieurs composants tentent d'envoyer une donnée sur le bus en même temps : c'est un conflit d'accès au bus. Cette situation arrive sur de nombreux types de bus, qu'ils soient multiplexés ou non. Sur les bus multiplexés, qui relient plus de deux composants, cette situation est fréquente du fait du nombre de récepteurs/émetteurs potentiels. Mais cela peut aussi arriver sur certains bus dédiés, les bus half-duplex étant des exemples particuliers : il se peut que les deux composants veuillent être émetteurs en même temps, ou récepteurs.

Quoi qu’il en soit, ces conflits d'accès posent problème si un composant cherche à envoyer un 1 et l'autre un 0 : tout ce que l’on reçoit à l'autre bout du fil est une espèce de mélange incohérent des deux. Pour résoudre ce problème, il faut répartir l'accès au bus pour n'avoir qu'un émetteur à la fois. On doit choisir un émetteur parmi les candidats. Ce choix sera effectué différemment suivant le protocole du bus et son organisation, mais ce choix n’est pas gratuit. Certains composants devront attendre leur tour pour avoir accès au bus. Les concepteurs de bus ont inventé des méthodes pour gérer ces conflits d’accès, et choisir le plus efficacement possible l’émetteur : on parle d'arbitrage du bus.

Les méthodes d'arbitrage (algorithmes) modifier

Il existe plusieurs méthodes d'arbitrages, qui peuvent se classer en différents types, selon leur fonctionnement.

Pour donner un exemple d'algorithme d'arbitrage, parlons de l'arbitrage par multiplexage temporel. Celui-ci peut se résumer en une phrase : chacun son tour ! Chaque composant a accès au bus à tour de rôle, durant un temps fixe. Cette méthode fort simple convient si les différents composants ont des besoins approximativement équilibrés. Mais elle n'est pas adaptée quand certains composants effectuent beaucoup de transactions que les autres. Les composants gourmands manqueront de débit, alors que les autres monopoliseront le bus pour ne presque rien en faire. Une solution est d'autoriser à un composant de libérer le bus prématurément, s'il n'en a pas besoin. Ce faisant, les composants qui n'utilisent pas beaucoup le bus laisseront la place aux composants plus gourmands.

Une autre méthode est celle de l'arbitrage par requête, qui se résume à un simple « premier arrivé, premier servi » ! L'idée est que tout composant peut réserver le bus si celui-ci est libre, mais doit attendre si le bus est déjà réservé. Pour savoir si le bus est réservé, il existe deux méthodes :

  • soit chaque composant peut vérifier à tout moment si le bus est libre ou non (aucun composant n'écrit dessus) ;
  • soit on rajoute un bit qui indique si le bus est libre ou occupé : le bit busy.

Certains protocoles permettent de libérer le bus de force pour laisser la place à un autre composant : on parle alors de bus mastering. Sur certains bus, certains composants sont prioritaires, et les circuits chargés de l'arbitrage libèrent le bus de force si un composant plus prioritaire veut utiliser le bus. Bref, les méthodes d'arbitrage sont nombreuses.

Arbitrage centralisé ou décentralisé modifier

Une autre classification nous dit si un composant gère le bus, ou si cet arbitrage est délégué aux composants qui accèdent au bus.

  • Dans l'arbitrage centralisé, un circuit spécialisé s'occupe de l'arbitrage du bus.
  • Dans l'arbitrage distribué, chaque composant se débrouille de concert avec tous les autres pour éviter les conflits d’accès au bus : chaque composant décide seul d'émettre ou pas, suivant l'état du bus.
Notons qu'un même algorithme peut être implémenté soit de manière centralisée, soit de manière décentralisée.

Pour donner un exemple d'arbitrage centralisé, nous allons aborder l'arbitrage par daisy chain. Il s'agit d'un algorithme centralisé, dans lequel tout composant a une priorité fixe. Dans celui-ci, tous les composants sont reliés à un arbitre, qui dit si l'accès au bus est autorisé.

Les composants sont reliés à l'arbitre via deux fils : un fil nommé Request qui part des composants et arrive dans l'arbitre, et un fil Grant qui part de l'arbitre et parcours les composants un par un. Le fil Request transmet à l'arbitre une demande d'accès au bus. Le composant qui veut accéder au bus va placer un sur ce fil 1 quand il veut accéder au bus. Le fil Grant permet à l'arbitre de signaler qu'un des composants pourra avoir accès au bus. Le fil est unique Request est partagé entre tous les composants (cela remplace l'utilisation d'une porte OU). Par contre, le fil Grant relie l'arbitre au premier composant, puis le premier composant au second, le second au troisième, etc. Tous les composants sont reliés en guirlande par ce fil Grant.

Par défaut, l'arbitre envoie un 1 quand il accepte un nouvel accès au bus (et un 0 quand il veut bloquer tout nouvel accès). Quand un composant ne veut pas accéder au bus, il transmet le bit reçu sur ce fil tel quel, sans le modifier. Mais s'il veut accéder au bus, il mettra un zéro sur ce fil : les composants précédents verront ainsi un 1 sur le fil, mais les suivants verront un zéro (interdiction d'accès). Ainsi, les composants les plus près du bus, dans l'ordre de la guirlande, seront prioritaires sur les autres.

Daisy Chain.

L'arbitrage sur les bus à collecteur ouvert modifier

Les bus à collecteur ouvert ont un avantage pour ce qui est de l'arbitrage : ils permettent de détecter les collisions assez simplement. En effet, le contenu du bus est égal à un ET entre toutes les sorties reliées au bus. Si tous les composants veulent laisser le bus à 1 à un instant t, le bus sera à 1 : s'il y a collision, elle n'est pas grave car tous les composants envoient la même chose. Pareil s'ils veulent tous mettre le bus à 0 : le bus sera à 0 et la collision n'aura aucun impact. Par contre, si une sortie veut mettre le bus à 0 et un autre veut le laisser à 1, alors le bus sera mis à 0.

La détection des collisions est alors évidente. Les composants qui émettent quelque chose sur le bus vérifient si le bus a bien la valeur qu'ils envoient dessus. Si les deux concordent, on ne sait pas il y a collision et il y a de bonnes chances que ce ne soit pas le cas, alors on continue la transmission. Mais si un composant envoie un 1 et que le bus est à 0, cela signifie qu'un autre composant a mis le bus à 0 et qu'il y a une collision. Le composant qui a détecté la collision cesse immédiatement la transmission et laisse la place au composant qui a mis le bus à 0, il le laisse finir la transmission entamée.


Dans ce qui va suivre, nous allons étudier quelques bus relativement connus, autrefois très utilisés dans les ordinateurs. La plupart de ces bus sont très simples : il n'est pas question d'étudier les bus les plus en vogue à l'heure actuelle, du fait de leur complexité. Nous allons surtout étudier les bus série, les bus parallèles étant plus rares.

Un exemple de liaison point-à-point série : le port série RS-232 modifier

Le port RS-232 est une liaison point à point de type série, utilisée justement sur les ports série qu'on trouvait à l'arrière de nos PC. Celui-ci était autrefois utilisé pour les imprimantes, scanners et autres périphériques du même genre, et est encore utilisé comme interface avec certaines cartes électroniques. Il existe des cartes d'extension permettant d'avoir un port série sur un PC qui n'en a pas, se branchant sur un autre type de port (USB en général).

Le câblage de la liaison série RS-232 modifier

Le RS-232 est une liaison point à point de type full duplex, ce qui veut dire qu'elle est bidirectionnelle. Les données sont transmises dans les deux sens entre deux composants. Si la liaison est bidirectionnelle, les deux composants ont cependant des rôles asymétriques, ce qui est assez original. Un des deux composants est appelé le Data Terminal Equipment (DTE), alors que l'autre est appelé le Data Circuit-terminating Equipment (DCE). Les connecteurs pour ces deux composants sont légèrement différents. Mais mettons cela de côté pour le moment. En raison, de son caractère bidirectionnel, on devine que la liaison RS-232 est composée de deux fils de transmission de données, qui vont dans des sens opposés. À ces deux fils, il faut ajouter la masse, qui est commune entre les deux composants.

Liaison point à point RS-232.

Certains périphériques RS-232 n'avaient pas besoin d'une liaison bidirectionnelle et ne câblaient pas le second fil de données, et se contentaient d'un fil et de la masse. À l'inverse, d'autres composants ajoutaient d'autres fils, définis par le standard RS-232, pour implémenter un protocole de communication complexe. C'était notamment le cas sur les vieux modems connectés sur le ports série. Généralement, 9 fils étaient utilisés, ce qui donnait un connecteur à 9 broches de type DE-9.

Connecteur DE-9 et broches RS-232.

La trame RS-232 modifier

Le bus RS-232 est un bus série asynchrone. Une transmission sur ce bus se résume à l'échange d'un octet de donnée. La trame complète se décompose en un bit de start, l'octet de données à transmettre, un bit de parité, et un bit de stop. Le bit de start est systématiquement un bit qui vaut 0, tandis que le bit de stop vaut systématiquement 1.

Trame RS-232.

L'envoi et la réception des trames sur ce bus se fait simplement en utilisant un composant nommé UART composé de registres à décalages qui envoient ou réceptionnent les données bit par bit sur le bus. Les données envoyées sont placées dans un registre à décalage, dont le bit de sortie est connecté directement sur le bus série. La réception se fait de la même manière : le bus est connecté à l'entrée d'un registre à décalage. Quelques circuits annexes s'occupent du calcul de la parité et de la détection des bits de start et de stop.

Un exemple de bus série : le bus I²c modifier

Nous allons maintenant voir le fameux bus I²c. Il s'agit d'un bus série, qui utilise deux fils pour le transport des données et de l'horloge, nommés respectivement SDA (Serial Data Line) et SCL (Serial Clock Line). Chaque composant compatible I²c a donc deux broches, une pour le fil SDA et une autre pour le fil SCL.

La spécification électrique modifier

Les composants I²c ont des entrées et sorties qui sont dites à drain ouvert. Pour rappel, cela veut dire qu'une broche peut mettre le fil à 0 ou le laisser à son état de repos, mais ne peut pas décider de mettre le fil à 1. Pour compenser, les fils sont connectés à la tension d'alimentation à travers une résistance, ce qui garantit que l'état de repos soit à 1.

Pour le dire autrement, on peut voir le contenu du bus comme un ET des bits envoyés sur les sorties des composants connectés au bus. Ce détail aura son importance par la suite. Le contenu du fil peut être lu sans altérer l'état électrique du bus/fil.
Bus I2C.

En faisant cela, le nombre de composants que l'on peut placer sur le bus est surtout limité par les spécifications électriques du bus, notamment sa capacité. Mais cela a l'avantage que le bus est compatible avec des technologies de fabrication totalement différentes, qu'il s'agisse de composants TTL, CMOS ou autres. En effet, la tension d'alimentation des composants TTL n'est pas la même que celle des composants CMOS. Utiliser des entrées-sorties à drain ouvert fait que la spécification du bus I²c ne spécifie pas la tension d'alimentation du bus, mais la laisse au choix du concepteur. En clair, on peut connecter plusieurs composants TTL sur un même bus, ou plusieurs composants CMOS sur le même bus, mais on ne peut pas connecter composants TTL et CMOS avec des tensions d'alimentation différentes sur un même bus. La compatibilité est donc présente, même si elle n'est pas parfaite.

L'adressage sur le bus I²c modifier

Chaque composant connecté à un bus I²c a une adresse unique, qui sert à l’identifier. Les mémoires I²c ne font pas exception. Les adresses I²c sont codées sur 7 bits, ce qui donne un nombre de 128 adresses distinctes. Certaines adresses sont cependant réservées et ne peuvent pas être attribuées à un composant. C'est le cas des adresses allant de 0000 0000 à 0000 0111 et des adresses allant de 1111 1100 à 1111 1111, ce qui fait 8 + 4 = 12 adresses réservées. Les adresses impaires sont des adresses de lecture, alors que les adresses paires sont des adresses d'écriture. En tout, cela fait donc 128 - 12 = 116 adresses possibles, dont 2 par composant, ce qui fait 58 composants maximum.

Le codage des trames sur un bus I²c modifier

Le codage d'une trame I²c est assez simple. La trame de données est organisée comme suit : un bit de START, suivi de l'octet à transmettre, suivi par un bit d'ACK/NACK, et enfin d'un bit de STOP. Le bit d'ACK/NACK indique si le récepteur a bien reçu la donnée sans erreurs. Là où les bits START, STOP et de données sont émis par l'émetteur, le bit ACK/NACK est émis par le récepteur.

Vous êtes peut-être étonné par la notion de bit START et STOP et vous demandez comment ils sont codés. La réponse est assez simple quand on se rappelle que les fils SDA et SCL sont mis à 1 à l'état de repos. L'horloge n'est active que lors du transfert effectif des données, et reste à 1 sinon. Si SDA et SCL sont à 1, cela signifie qu'aucun composant ne veut utiliser le bus. Le début d'une transmission demande donc qu'au moins un des fils passe à 0. Un transfert de données commence avec un bit START, qui est codé par une mise à 0 de l'horloge avant le fil de donnée, et se termine avec un bit STOP, qui correspond aux conditions inverses.

Bit START. Bit RESTART Bit STOP.

Les données sont maintenues tant que l’horloge est à 1. Dit autrement, le signal de donnée ne montre aucun front entre deux fronts de l'horloge. Retenez bien cette remarque, car elle n'est valide que pour la transmission d'un bit de données (et les bits d'ACK/NACK). Les bits START et STOP correspondent à une violation de cette règle qui veut qu'il y ait absence de front sur le signal de données entre deux fronts d'horloge.

Encodage des données. Bit ACK/NACK.

Pour résumer, une transmission I²c est schématisée ci-dessous. Sur ce schéma, S représente le marqueur de début de transmission (start), puis chaque période en bleue est celle ou la ligne de donnée peut changer d'état pour le prochain bit de données à transmettre durant la période verte qui suit notée B1, B2... jusqu'à la période finale notée P marquant la fin de transmission (stop).

Transfert de données via le protocole I²c.

Une trame transmet soit une donnée, soit une adresse. Généralement, la trame transmet un octet, qu'il s'agisse d'un octet de données ou un octet d'adresse. Pour une adresse, l'octet transmis contient une adresse de 7 bits et un bit R/W. Une lecture/écriture est composée de au moins deux transmissions : d'abord on transmet l'adresse, puis la donnée est transmise ensuite. Si je viens de dire "au moins deux transmissions", c'est parce qu'il est possible de lire/écrire des données de 16 ou 32 bits, en plusieurs fois. Dans ce cas, on envoie l'adresse avec la première transmission, puis on envoie/réceptionne plusieurs octets les uns à la suite des autres, avec une transmission par octet. Il est aussi possible d'envoyer une adresse en plusieurs fois,c e qui est très utilisé pour les mémoires I²c : la première adresse envoyée permet de sélectionner la mémoire, l'adresse suivante identifie le byte voulu dans la mémoire.

Transmission de I²c en lecture/écriture.

La synchronisation sur le bus I²c modifier

Il arrive que des composants lents soient connectés à un bus I²c, comme des mémoires EEPROM. Ils mettent typiquement un grand nombre de cycles avant de faire ce qu'on leur demande, ce qui donne un temps d'attente particulièrement long. Dans ce cas, les transferts de ou vers ces composants doivent être synchronisés d'une manière ou d'une autre. Pour cela, le bus I²c permet de mettre en pause une transmission tant que le composant lent n'a pas répondu, en allongeant la durée du bit d'ACK.

Un périphérique normal répondrait à une transmission comme on l'a vu plus haut, avec un bit ACK. Pour cela, le récepteur met la ligne SDA à 0 pendant que l'horloge SCL est à 1. L'idée est qu'un récepteur lent peut temporairement maintenir la ligne SCL à 0 pendant toute la durée d'attente. Dans ce cas, l'émetteur attend un nouveau front sur l'horloge avant de faire quoi que ce soit. L'horloge est inhibée, le bus I²c est mis en pause. Quand le récepteur lent a terminé, il relâche la ligne d'horloge SDL, et envoie un ACK normal. Cette méthode est utilisée par beaucoup de mémoires EEPROM I²c. Évidemment, cela réduit les performances et la perte est d'autant plus grande que les temps d'attente sont longs.

L’arbitrage sur le bus I²c modifier

Le bit START est impliqué dans l'arbitrage du bus : dès que le signal SDA est mis à 0 par un émetteur, les autres composants savent qu'une transmission a commencé et qu'il faut attendre.

Il est malgré tout possible que deux composants émettent chacun une donnée en même temps, car ils émettent un bit START à peu près en même temps. Dans ce cas, l'arbitrage du bus utilise intelligemment le fait que les entrées-sorties sont à drain ouvert. Nous avions dit que le bus est à 1 au repos, mais qu'il est mis à 0 dès qu'au moins un composant veut envoyer un 0. Pour le dire autrement, on peut voir le contenu du bus comme un ET des bits envoyés sur les sorties des composants connectés au bus. Ce détail est utilisé pour l'arbitrage.

Si deux émetteurs envoient chacun une donnée, le bus accepte cette double transmission. Tant que les bits transmis sont identiques, cela ne pose pas de problème : le bus est à 1 si les deux composants veulent envoyer un 1 en même temps, idem pour un 0. Par contre, si un composant veut envoyer un 1 et l'autre un 0, le bus est mis à 0 du fait des sorties à drain ouvert. Le truc est que les émetteurs vérifient si les bits transmis sur le bus correspondent aux bits envoyés. Si l'émetteur émet un 1 et voit un 0 sur le bus, il comprend qu'il y a une collision et cesse sa transmission pour laisser la place à l'autre émetteur. Il retentera une nouvelle transmission plus tard.

Un exemple de bus parallèle : le bus PCI modifier

Ports PCI version 32 bits sur une carte mère grand public.

Le bus PCI est un bus autrefois très utilisé dans les ordinateurs personnels, qui a eu son heure de gloire entre les années 90 et 2010. Il était utilisé pour la plupart des cartes d'extension, à savoir les cartes son, les cartes graphiques et d'autres cartes du genre. Il remplace le bus ISA, un ancien bus devenu obsolète dans les ordinateurs personnels.

Les lecteurs aguerris qui veulent une description détaillée du bus PCI peuvent lire le livre nommé "PCI Bus Demystified".

Les performances théoriques du bus PCI modifier

Le bus ISA avait une largeur de seulement 16 bits et une fréquence de 8 MHz, ce qui était suffisant lors de son adoption, mais était devenu trop limitant dès les années 90. Le bus PCI avait de meilleures performances : un bus de 32 bits et une fréquence de 33 MHz dans sa première version, ce qui faisait un débit maximum de 133 mébioctets par secondes. Des extensions faisaient passer le bus de données de 32 à 64 bits, augmentaient la fréquence à 66/133 MHz, ou alors ajoutaient des fonctionnalités. Les versions 64 bits du bus PCI avaient généralement une fréquence plus élevée, de 66 MHz pour le PCI version 2.3, de 133 MHz pour le PCI-X.

La tension d'alimentation : deux normes modifier

Il existait aussi une version 3,3 volts et une version 5 volts du bus PCI, la tension faisant référence à la tension utilisée pour alimenter le bus. L'intérêt était de mieux s'adapter aux circuits imprimés de l'époque : certains fonctionnaient en logique TTL à 5 volts, d'autres avec une logique différente en 3,3 volts. La logique ici mentionnée est la manière dont sont construits les transistors et portes logiques. Concrètement, le fait qu'il s'agisse de deux logiques différentes change tout au niveau électrique. La norme du bus PCI en 3,3 volts est fondamentalement différente de celle en 5 volts, pour tout ce qui touche aux spécifications électriques (et elles sont nombreuses). Une carte conçue pour le 3,3 volts ne pourra pas marcher sur un bus PCI 5 volts, et inversement. Il existe cependant des cartes universelles capables de fonctionner avec l'une ou l'autre des tensions d'alimentation, mais elles sont rares. Pour éviter tout problème, les versions 3,3 et 5 volts du bus PCI utilisaient des connecteurs légèrement différents, de même que les versions 32 et 64 bits.

Connecteurs et cartes PCI.

L'arbitrage du bus PCI modifier

Le bus PCI utilise un arbitrage centralisé, avec un arbitre qui commande plusieurs composants maîtres. Chaque composant maitre peut envoyer des données sur le bus, ce qui en fait des émetteurs-récepteurs, contrairement aux composants esclaves qui sont toujours récepteurs. Chaque maître a deux broches spécialisées dans l'arbitrage : un fil REQ (Request) pour demander l'accès au bus à l'arbitre, et un fil GNT (Grant) pour recevoir l'autorisation d'accès de la part de l'arbitre de bus. Les deux signaux sont actifs à l'état bas, à zéro. Un seul signal GNT peut être actif à la fois, ce qui fait qu'un seul composant a accès au bus à un instant donné.

L'arbitrage PCI gère deux niveaux de priorité pour l'arbitrage. Les composants du premier niveau sont prioritaires sur les autres pour l'arbitrage. En cas d'accès simultané, le composant de niveau 1 aura accès au bus alors que ceux de niveau 2 devront attendre. En général, les cartes graphiques sont de niveau 1, alors que les cartes réseau, son et SCSI sont dans le niveau 2.

Un composant ne peut pas monopoliser le bus en permanence, mais doit laisser la place aux autres après un certain temps. Une fois que l'émetteur a reçu l'accès au bus et démarré une transmission avec le récepteur, il a droit à un certain temps avant de devoir laisser la place à un autre composant. Le temps en question est déterminé par un timer, un compteur qui est décrémenté à chaque cycle d'horloge. Au démarrage de la transaction, ce compteur est initialisé avec le nombre de cycle maximal, au-delà duquel l'émetteur doit laisser le bus. Si le compteur atteint 0, que d'autres composants veulent accéder au bus, et que l'émetteur ait terminé sa transmission, la transmission est arrêtée de force. Le composant peut certes redemander l'accès au bus, mais elle ne lui sera pas accordée car d'autres composants veulent accéder au bus.

Il est possible que, quand aucune transaction n'a lieu, le bus soit attribué à un composant maître choisit par défaut. On appelle cela le bus parking. Cela garantit qu'il y a toujours un composant qui a son signal REQ actif, il ne peut pas avoir de situation où aucun composant PCI n'a accès au bus. Quand un autre composant veut avoir accès au bus, l'autre composant est choisit, sauf si une transmission est en cours. L'avantage est que le composant maître choisit par défaut n'a pas besoin de demander l'accès au bus au cas où il veut faire une transmission, ce qui économise quelques cycles d'horloge. L'arbitre du bus doit cependant être configuré pour. Le réglage par défaut du bus PCI est que le maître choisi par défaut est le dernier composant à avoir émis une donnée sur le bus.

L'adressage et le bus PCI modifier

Le bus PCI est multiplexé, ce qui signifie que les mêmes fils sont utilisés pour transmettre successivement adresse ou données. Les adresses ont la même taille que le bus de données : 32 bits ou 64 bits, suivant la version du bus. On trouve aussi un bit de parité, transmis en même temps que les données et adresses. Notons que les composants 32 bits pouvaient utiliser des adresses 64 bits sur un bus PCI : il leur suffit d'envoyer ou de recevoir les adresses en deux fois : les 32 bits de poids faible d'abord, les 32 bits de poids fort ensuite. Fait important, le PCI ne confond pas les adresses des périphériques et de la mémoire RAM. Il existe trois espaces d'adressage distincts : un pour la mémoire RAM, un pour les périphériques, et un pour la configuration qui est utilisé au démarrage de l'ordinateur pour détecter et configurer les périphériques branchés sur le bus.

Le bus de commande possède 4 fils/broches sur lesquelles on peut transmettre une commande à un périphérique. Il existe une commande de lecture et une commande d'écriture pour chaque espace d'adressage. On a donc une commande de lecture pour les adresses en RAM, une commande de lecture pour les adresses de périphériques, une autre pour les adresses de configuration, idem pour les commandes d'écritures. Il existe aussi des commandes pour les adresses en RAM assez spéciales, qui permettent de faire du préchargement, de charger des données à l'avance. Ces commandes permettent de faire une lecture, mais préviennent le contrôleur PCI que les données suivantes seront accédées par la suite et qu'il vaut mieux les précharger à l'avance.

Les commandes en question sont transmises en même temps que les adresses. Lors de la transmission d'une donnée, les 4 broches sont utilisées pour indiquer quels octets du bus sont valides et quels sont ceux qui doivent être ignorés.

Bits de commande Nom de la commande Signification
0000 Interrupt Acknowledge Commande liée aux interruptions
0001 Special Cycle Envoie une commande/donnée à tous les périphériques PCI
0010 I/O Read Lecture dans l'espace d’adressage des périphériques
0011 I/O Write Écriture dans l'espace d’adressage des périphériques
0100 Reserved
0101 Reserved
0110 Memory Read Lecture dans l'espace d’adressage de la RAM
0111 Memory Write Écriture dans l'espace d’adressage de la RAM
1000 Reserved
1010 Reserved
1011 Configuration Read Lecture dans l'espace d’adressage de configuration
1011 Configuration Write Écriture dans l'espace d’adressage de configuration
1100 Memory Read Multiple Lecture dans l'espace d’adressage de la RAM, avec préchargement
1101 Dual-Address Cycle Lecture de 64 bits, sur un bus PCI de 32 bits
1110 Memory Read Line Lecture dans l'espace d’adressage de la RAM, avec préchargement
1111 Memory Write and Invalidate Écriture dans l'espace d’adressage de la RAM, avec préchargement

Plusieurs fils optionnels ajoutent des interruptions matérielles (IRQ), une fonctionnalité que nous verrons d'ici quelques chapitres. Pour le moment, sachez juste qu'il y a quatre fils dédiés aux interruptions, qui portent les noms INTA, INTB, INTC et INTD. En théorie, un composant peut utiliser les quatre fils d'interruptions s'il le veut, mais la pratique est différente. Tous les composants PCI, sauf en quelques rares exceptions, utilisent une seule sortie d'interruption pour leurs interruptions. Sachant qu'il y a généralement quatre ports PCI dans un ordinateur, le câblage des interruptions est simplifié, avec un fil par composant. Lorsqu'une interruption est levée par un périphérique, le composant qui répond aux interruption, typiquement le processeur, répond alors par une commande Interrupt Acknowledge.

Le protocole de transmission sur le bus PCI modifier

En tout, 6 fils commandent les transactions sur le bus. On a notamment un fil FRAME qui est maintenu à 0 pendant le transfert d'une trame. Le fil STOP fait l'inverse : il permet à un périphérique de stopper une transaction dont il est le récepteur. Les deux signaux IRDY et TRDY permettent à l'émetteur et le récepteur de se mettre d'accord pour démarrer une transmission. Le signal IRDY (Initiator Ready) est mis à 1 par le maître quand il veut démarrer une transmission, le signal TRDY (Target Ready) est la réponse que le récepteur envoie pour indiquer qu'il est près à démarrer la transmission. Le signal DEVSEL est mis à zéro quand le récepteur d'une transaction a détecté son adresse sur le bus, ce qui lui permt d'indiquer qu'il a bien compris qu'il était le récepteur d'une transaction.

Pour la commande Special Cycle, qui envoie une donnée à tous les périphériques PCI en même temps, les signaux IRDY, TRDY et DEVSEL ne sont pas utilisés. Ces signaux n'ont pas de sens dans une situation où il y a plusieurs récepteurs. Seul le signal FRAME est utilisé, ainsi que le bus de données.

Une transaction en lecture procède comme suit :

  • En premier lieu, l'émetteur acquiert l'accès au bus et son signal GNT est mis à 0.
  • Ensuite, il fait passer le fil FRAME à 0, qui pour indiquer le début d'une transaction, et envoie l'adresse et la commande adéquate.
  • Au cycle suivant, le récepteur met le signal IRDY à 0, pour indiquer qu'il est près pour recevoir la donnée lue.
  • Dans un délai de 3 cycles d'horloge maximum, le récepteur doit avoir reçu l'adresse et le précise en mettant le signal DEVSEL à 0.
  • Le récepteur place la donnée lue sur le bus, et met le signal TRDY à 0.
  • Le signal TRDY remonte à 1 une fois la donnée lue. En cas de lecture en rafale, à savoir plusieurs lectures consécutives à des adresses consécutives, on reprend à l'étape précédente pour transmettre une nouvelle donnée.
  • Puis tous les signaux du bus repassent à 1 et le bus revient à son état initial, le signal GNT est réattribué à un autre composant.

Le Plug And Play modifier

Outre sa performance, le bus PCI était plus simple d'utilisation. La configuration des périphériques ISA était laborieuse. Il fallait configurer des jumpers ou des interrupteurs sur chaque périphérique impliqué, afin de configurer le DMA, les interruptions et d'autres paramètres cruciaux pour le fonctionnement du bus. La moindre erreur était source de problèmes assez importants. Autant ce genre de chose était acceptable pour des professionnels ou des power users, autant le grand public n'avait ni les compétences ni l'envie de faire cela. Le bus PCI était lui beaucoup plus facile d'accès, car il intégrait la fonctionnalité Plug And Play, qui fait que chaque périphérique est configuré automatiquement lors de l'allumage de l'ordinateur.


Les mémoires modifier

Mémoire. Ce mot signifie dans le langage courant le fait de se rappeler quelque chose, de pouvoir s'en souvenir. La mémoire d'un ordinateur fait exactement la même chose (le nom de mémoire n'a pas été donné par hasard) mais pour un ordinateur. Son rôle est de retenir des données stockées sous la forme de suites de bits, afin qu'on puisse les récupérer si nécessaire et les traiter. Il existe différents types de mémoires, au point que tous les citer demanderait un chapitre entier. Il faut avouer qu'entre les DRAM, SRAM, eDRAM, SDRAM, DDR-SDRAM, SGRAM, LPDDR, QDRSRAM, EDO-RAM, XDR-DRAM, RDRAM, GDDR, HBM, ReRAM, QRAM, CAM, VRAM, ROM, EEPROM, EPROM, Flash, et bien d'autres, il y a de quoi se perdre. Dans ce chapitre, nous allons parler des caractéristiques basiques qui permettent de classer les mémoires. Nous allons voir différents critères qui permettent de classer assez simplement les mémoires, sans évidemment rentrer dans les détails les plus techniques. Nous allons aussi voir les classifications basiques des mémoires.

La technologie utilisée pour le support de mémorisation modifier

La première distinction que nous allons faire est la différence entre mémoire électronique, magnétique, optique et mécanique. Cette distinction n'est pas souvent évoquée dans les cours sur les mémoires, car elle est assez évidente et que l'on ne peut pas dire grand chose dessus. Mais elle a cependant son importance et elle mérite qu'on en parle. Cette distinction porte sur la manière dont sont mémorisées les données. En effet, une mémoire informatique contient forcément des circuits électroniques, qui servent pour interfacer la mémoire avec le reste de l'ordinateur, pour contrôler la mémoire, et bien d'autres choses. Par contre, cela n'implique pas que le stockage des données se fasse forcément de manière électronique. Il faut bien distinguer le support de mémorisation, c'est à dire la portion de la mémoire qui mémorise effectivement des données, et le reste des circuits de la mémoire. Cette distinction sera décrite dans les prochains chapitres, mais elle est très importante.

Le support de mémorisation peut être un support électronique, comme sur les registres ou les mémoires ROM/RAM/SSD et autres, et le codage des données n'est pas différent de celui observé dans les registres. Les bits sont alors codées par une tension électrique. La quasi-totalité des mémoires actuelles utilisent un support électronique, les exceptions étant rares. Il faut dire que les mémoires électroniques ont l'avantage d'être généralement assez rapides, avec un débit binaire élevé et un temps d'accès faible. Mais en contrepartie, elles ont tendance à avoir une faible capacité comparé aux autres technologies. En conséquence, les mémoires électroniques ont surtout été utilisées dans le passé pour les niveaux élevés de la hiérarchie mémoire, mais pas comme mémoire de masse. Ce n'est qu'avec l'avancée des techniques de miniaturisation que les mémoires électroniques ont pu obtenir des capacités suffisantes pour servir de mémoire de masse. Là où les anciennes mémoires de masse étaient des mémoires magnétiques ou optiques, comme les disques durs ou les DVD/CD, la tendance actuelle est aux remplacement de celles-ci par des supports électroniques, comme les clés USB ou les disques SSD.

Mémoire à bande magnétique.

Les mémoires assez anciennes utilisaient un support de mémorisation magnétique, dont l'aimantation permet de coder un 0 ou un 1. Le support magnétique est généralement un plateau dont la surface est aimantée et aimantable, comme sur les disques durs et disquettes, ou une bande magnétique similaire à celle des vielles cassettes audio. Elles avaient des performances inférieures aux mémoires électroniques, mais une meilleure capacité, d'où leur utilisation en tant que mémoire de masse. Un autre de leur avantage est qu'elles ont une durée de vie assez importante, liée au support de mémorisation. On peut aimanter, ré-aimanter, désaimanter le support de mémorisation un très très grand nombre de fois sans que cela endommage le support de mémorisation. Le support de mémorisation magnétique tient donc dans le temps, bien plus que les supports de mémorisation électronique dont le nombre d'accès avant cassure est généralement limité. Malheureusement, les mémoires magnétiques contiennent des circuits électroniques faillibles, ce qui fait qu'elles ne sont pas éternelles.

Mémoire optique, en l’occurrence en DVD.

Enfin, n’oublions pas les mémoires optiques comme les CD ou les DVD, dont le support de mémorisation est une surface réfléchissante. Elles sont composées d'une couche de plastique dans laquelle on fait des creux, creux qui sont utilisés pour coder des bits. Elles ont l'avantage d'avoir une bonne capacité, même si les temps d'accès et les débits sont minables. Elles ont une capacité et des performances plus faibles que celles des disques durs magnétiques, mais souvent meilleure que les autres formes de mémoire magnétique. Cette capacité intermédiaire est un avantage sur les mémoires magnétiques, hors disque dur. Leur inconvénient majeur est qu'elles s’abîment facilement. Toute personne ayant déjà eu des CD/DVD sait à quel point ils se rayent facilement et à quel point ces rayures peuvent tout simplement rendre le disque inutilisable.

Enfin, il faut mentionner les mémoires de type mécaniques, basées sur un support physique. L'exemple le plus connu est celui des cartes perforées, et d'autres mémoires similaires basées sur du papier. Mais il existe d'autres types de mémoire basées sur un support électro-acoustique comme les lignes à délai, des techniques de stockage basées sur de l'ADN ou des polymères, et bien d'autres. L'imagination des ingénieurs en terme de supports de stockage n'est plus à démontrer et leur créativité a donné des mémoires étonnantes.

Les mémoires ROM et RWM modifier

Mémoire EPROM. On voit que le boîtier incorpore une sorte de vitrine luisante, qui laisse passer les UV, nécessaires pour effacer l'EPROM.

Une seconde différence concerne la façon dont on peut accéder aux informations stockées dans la mémoire. Celle-ci permet de faire la différence entre les mémoires ROM et les mémoires RWM. Dans une mémoire ROM, on peut seulement récupérer les informations dans la première, mais pas les modifier individuellement. À l'inverse, les mémoires RWM permettent de récupérer les données, mais aussi de les modifier individuellement.

Les mémoires ROM modifier

Avec les mémoires ROM, on peut récupérer les informations dans la mémoire, mais pas les modifier : la mémoire est dite accessible en lecture, mais pas en écriture. Si on ne peut pas modifier les données d'une ROM, certaines permettent cependant de réécrire intégralement leur contenu : on dit qu'on reprogramme la ROM. Insistons sur la différence entre reprogrammation et écriture : l'écriture permet de modifier un byte bien précis, alors que la reprogrammation efface toute la mémoire et la réécrit en totalité. De plus, la reprogrammation est généralement beaucoup plus lente qu'une écriture, sans compter qu'il est plus fréquent d'écrire dans une mémoire que la reprogrammer. Ce terme de programmation vient du fait que les mémoires ROM sont souvent utilisées pour stocker des programmes sur certains ordinateurs assez simples.

Les mémoires ROM sont souvent des mémoires électroniques, même si les exceptions sont loin d'être rares. On peut classer les mémoires ROM électroniques en plusieurs types :

  • les mask ROM sont fournies déjà programmées et ne peuvent pas être reprogrammées ;
  • les mémoires PROM sont fournies intégralement vierges, et on peut les programmer une seule fois ;
  • les mémoires RPROM sont reprogrammables, ce qui signifie qu'on peut les effacer pour les programmer plusieurs fois ;
    • les mémoires EPROM s'effacent avec des rayons UV et peuvent être reprogrammées plusieurs fois de suite ;
    • certaines RPROM peuvent être effacées par des moyens électriques : ce sont les mémoires EEPROM.
Les mémoires Flash sont un cas particulier d'EEPROM, selon la définition utilisée plus haut.

Les mémoires de type mask ROM sont utilisées dans quelques applications particulières. Par exemple, elles étaient utilisées sur les vieilles consoles de jeux, pour stocker le jeu vidéo dans les cartouches. Elles servent aussi pour les firmware divers et variés, comme le firmware d'une imprimante ou d'une clé USB. Par contre, le BIOS d'un PC (qui est techniquement un firmware) est stocké dans une mémoire EEPROM, ce qui explique qu'on peut le mettre à jour (on dit qu'on flashe le BIOS).

Il existe des mémoires ROM qui ne sont pas électroniques. Par exemple, prenez le cas des CD-ROM : une fois gravés, on ne peut plus modifier leur contenu. Cela en fait naturellement des mémoires ROM. D'ailleurs, c'est pour cela qu'on les appelle des CD-ROM : Compact Disk Read Only Memory ! Même chose pour les DVD-ROM ou les Blue-Ray.

Les mémoires RWM modifier

Sur les mémoires RWM, on peut récupérer les informations dans la mémoire et les modifier : la mémoire est dite accessible en lecture et en écriture. Attention aux abus de langage : le terme mémoire RWM est souvent confondu dans le langage commun avec les mémoires RAM. Les mémoires RAM sont un cas particulier de mémoire RWM. La définition souvent retenue est qu'une mémoire RAM est une mémoire RWM dont le temps d'accès est approximativement le même pour chaque byte, contrairement aux autres mémoires RWM comme les disques durs ou les disques optiques où le temps d'accès dépend de la position de la donnée. Mais nous verrons dans la suite du cours que cette définition est quelques peu trompeuse et qu'elle omet des éléments importants. Un point important est que les mémoires RAM sont des mémoires électroniques : les mémoires RWM magnétiques, optiques ou mécaniques ne sont pas considérées comme des mémoires RAM.

Précisons que la définition des mémoires RWM contient quelques subtilités assez contre-intuitives. Par exemple, prenez les CD et DVD. Ceux qui ne sont pas réinscriptibles sont naturellement des mémoires ROM, comme l'a dit plus haut. Mais qu'en est-il des CD/DVD réinscriptibles ? On pourrait croire que ce sont des mémoires RWM, car on peut modifier leur contenu sans avoir formellement à les reprogrammer. Mais en fait non, ce n'en sont pas. Là encore, on retrouve la distinction entre écriture et reprogrammation des mémoires ROM. Pouvoir effacer totalement une mémoire pour y réinscrire de nouvelles données ensuite n'en fait pas une mémoire RWM. Il faut que l'écriture puisse être localisée, qu'on puisse modifier des données sans avoir à réécrire toute la mémoire. La capacité de modifier les données des mémoires RWM doit porter sur des données individuelles, sur des morceaux de données bien précis.

Une classification des mémoires suivant la possibilité de lecture/écriture/reprogrammation modifier

Pour résumer, les mémoires peuvent être lues, écrites, ou reprogrammées. La distinction entre lecture et écriture permet de distinguer les mémoires ROM et RWM. Mais la distinction entre écriture et reprogrammation rend les choses plus compliquées. S'il fallait faire une classification des mémoires en fonction des opérations possibles en lecture et modification, cela donnerait quelque chose comme ceci :

  • Les mémoires ROM (Read Only Memory) sont accessibles en lecture uniquement, mais ne peuvent pas être écrites ou reprogrammées. Les mémoires mask ROM ainsi que les CD-ROM sont de ce type.
  • Les mémoires de type WOM (Write Once Memory), aussi appelées mémoires à programmation unique, sont des mémoires fournies vierges, que l'on peut reprogrammer une seule fois. Les mémoires PROM et les CD/DVD vierges inscriptibles une seule fois, sont de ce type.
  • Les mémoires PROM, aussi appelées mémoires reprogrammables peuvent être lues, mais aussi reprogrammées plusieurs fois, voire autant de fois que possible. Les mémoires EPROM et EEPROM, ainsi que les CD/DVD réinscirptibles sont dans ce cas.
  • Les mémoires RWM (Read Write Memory) peuvent être lues et écrites, la reprogrammation étant parfois possible sur certaines mémoires, bien que peu utile.

Notons que la technologie utilisée influence le caractère RWM/ROM/WOM/PROM d'une mémoire. Les mémoires magnétiques sont presque systématiquement de type RWM. En effet, un support magnétisable peut être démagnétisé facilement, ce qui les rend reprogrammables. On peut aussi changer son aimantation localement, et donc changer les bits mémorisés, ce qui les rend faciles à utiliser en écriture. Les mémoires électroniques peuvent être aussi bien de type ROM, ROM, WOM que RWM. Les mémoires optiques ne peuvent pas être des mémoires RWM, et ce sont les seules. En effet, les mémoires optiques sont composées d'une couche de plastique dans laquelle on fait des creux, creux qui sont utilisés pour coder des bits. Une fois la surface plastique altérée, on ne peut pas la remettre dans l'état initiale. Cela explique que les CD-ROM et DVD-ROM sont donc des mémoires de type ROM. les CD et DVD vierges sont vierges, mais on peut les programmer en faisant des trous dedans, ce qui en fait des mémoires de type WOM. Les CD/DVD réinscriptibles ont plusieurs couches de plastiques, ce qui permet de les reprogrammer plusieurs fois. La reprogrammation demande juste d'enlever une couche de plastique, ce qui est facile quand on sait faire des trous dans cette couche pour écrire des bits. On peut alors entamer la couche d'en-dessous.

Le tableau suivant montre le lien entre la technologie de fabrication et les autres caractères.

Mémoire RWM/ROM
Mémoires électroniques ROM, WOM, reprogrammables ou RWM.
Mémoires magnétiques RWM
Mémoires optiques ROM, WOM ou reprogrammable

Les mémoires volatiles et non-volatiles modifier

Lorsque vous éteignez votre ordinateur, le système d'exploitation et les programmes que vous avez installés ne s'effacent pas, contrairement au document Word que vous avez oublié de sauvegarder. Les programmes et le système d'exploitation sont placés sur une mémoire qui ne s'efface pas quand on coupe le courant, contrairement à votre document Word non-sauvegardé. Cette observation nous permet de classer les mémoires en deux types : les mémoires non-volatiles conservent leurs informations quand on coupe le courant, alors que les mémoires volatiles les perdent.

Les mémoires volatiles (statiques et dynamiques) modifier

Les mémoire volatiles sont presque toutes des mémoires électroniques. Comme exemple de mémoires volatiles, on peut citer la mémoire principale, aussi appelée mémoire RAM, les registres du processeur, la mémoire cache et bien d'autres. Globalement, toutes les mémoires qui ne sont pas soit des mémoires ROM/PROM/..., soit des mémoires de masse (des mémoires non-volatiles capables de conserver de grandes quantités de données, comme les disques durs ou les clés USB) sont des mémoires volatiles. La raison à cela est simplement liée à la hiérarchie mémoire : là où les mémoires ROM et les mémoires de masse conservent des données permanentes, les autres mémoires servent juste à accélérer les temps d'accès en stockant des données temporaires ou des copies des données persistantes.

Typiquement, si on omet quelques mémoires historiques aujourd'hui obsolètes, les mémoires volatiles sont toutes des mémoires RAM ou associées. Il existe cependant des projets de mémoires RAM (donc des mémoires RWM électroniques) destinées à être non-volatiles. C'est le cas de la FeRAM, la ReRAM, la CBRAM, la FeFET memory, la Nano-RAM, l'Electrochemical RAM et de bien d'autres encore. Mais ce sont encore des projets en cours de développement, la recherche poursuivant lentement son cours. Elles ne sont pas prêtes d'arriver dans les ordinateurs grand publics de si tôt. Pour le moment, la correspondance entre mémoires RAM et mémoires volatile tient bien la route.

Parmi les mémoires volatiles, on peut distinguer les mémoires statiques et les mémoires dynamiques.

  • Les données d'une mémoire statique ne s'effacent pas tant qu'elles sont alimentées en courant.
  • Pour les mémoires dynamiques, les données s'effacent en quelques millièmes ou centièmes de secondes si l'on n'y touche pas.

Sur les mémoires volatiles dynamiques, il faut réécrire chaque bit de la mémoire régulièrement, ou après chaque lecture, pour éviter qu'il ne s'efface. On dit qu'on doit effectuer régulièrement un rafraîchissement mémoire. La mémoire principale de l'ordinateur, la fameuse mémoire RAM, est actuellement une mémoire dynamique sur tous les PC actuels. Le rafraîchissement prend du temps, et a tendance à légèrement diminuer la rapidité des mémoires dynamiques. Mais en contrepartie, les mémoires dynamiques ont une meilleure capacité, car leurs bits prennent moins de place, utilisent moins de transistors.

Pour les mémoires RAM, on peut les trouver soit sous forme statique, soit sous forme dynamique. Les RAM statiques sont appelées des SRAM (Static RAM), alors que les RAM dynamiques sont appelées des DRAM (Dynamic RAM). Les deux ne sont pas fabriquées de la même manière : transistors ou portes logiques pour la première, transistors et condensateurs (des réservoirs à électrons) pour l'autre. La SRAM est intégrée dans le processeur, et est utilisée pour les registres et le cache du processeur, éventuellement pour les local store. A l'opposé, la DRAM est utilisée pour fabriquer des barrettes de mémoire, pour la RAM principale.

Il existe cependant une exception : la eDRAM, pour embedded DRAM, qui est intégrée sur le même circuit que le processeur. Pour le dire autrement, la eDRAM est de la DRAM qui est placée dans le même circuit intégré, le même boitier, la même puce que le processeur (ou qu'un autre circuit). Tout le défi est de mettre des condensateurs sur une puce en silicium, dans un circuit intégré, alors que les techniques de fabrication des processeurs font que ce n'est pas facile. L'eDRAM a été utilisée sur certains processeurs comme mémoire cache (cache L4, le plus proche de la mémoire sur ces puces). Elle a aussi été utilisée dans des consoles de jeu vidéo, pour la carte graphique des consoles suivantes : la PlayStation 2, la PlayStation Portable, la GameCube, la Wii, la Wii U, et la XBOX 360. Sur ces consoles, la RAM de la carte graphique était intégrée avec le processeur graphique dans le même circuit. La fameuse mémoire vidéo et le GPU n'étaient qu'une seule et même puce électronique, un seul circuit intégré.

Les mémoires non-volatiles modifier

Les mémoires de masse, à savoir celles destinées à conserver un grand nombre de données sur une longue durée, sont presque toutes des mémoires non-volatiles. Il faut dire qu'on attend d'elles de conserver des données sur un temps très long, y compris quand l'ordinateur s’éteint. Personne ne s'attend à ce qu'un disque dur ou SSD s'efface quand on éteint l'ordinateur. Ainsi, les mémoires suivantes sont des mémoires non-volatiles : les clés USB, les disques SSD, les disques durs, les disquettes, les disques optiques comme les CD-ROM et les DVD-ROM, de vielles mémoires comme les bandes magnétiques ou les rubans perforés, etc. Comme on le voit, les mémoires non-volatiles peuvent être des mémoires magnétiques (disques durs, disquettes, bandes magnétiques), électroniques (clés USB, disques SSD), optiques (CD, DVD) ou autres. Vous remarquerez que certaines de ces mémoires sont de type RWM (disques SSD, clés USB), alors que d'autres sont de type ROM (les CD/DVD non-réinscriptibles).

Il faut cependant noter qu'il existe quelques exceptions, où des mémoires RAM sont rendues non-volatiles et utilisées pour du stockage de long terme. Nous ne parlons pas ici des projets de mémoires RAM non-volatiles comme la FeRAM ou la CBRAM, évoqués plus haut. Nous parlons de cas où la mémoire volatile est couplée à un système qui empêche toute perte de données. Un exemple de mémoire de masse volatile est celui des nvSRAM et des BBSRAM. Ce sont des mémoires RAM, donc volatiles, de petite taille, qui sont rendues non-volatiles par divers stratagèmes.

  • Sur les BBSRAM (Battery Backed SRAM), la mémoire SRAM est couplée à une petite batterie/pile/super-condensateur qui l'alimente en permanence, ce qui lui empêche d'oublier des données. La batterie est généralement inclue dans le même boîtier que la mémoire SRAM. Ce sont des composants qui consomment peu de courants et qui peuvent tenir des années en étant alimentés par une simple pile bouton. Vous en avez une dans votre ordinateur, appelée la CMOS RAM, qui mémorise les paramètres du BIOS, la date, l'heure et divers autres informations.
  • Contrairement aux BBSRAM, les nvRAM (non-volatile RAM) n'ont pas de circuit d'alimentation qui prend le relai en cas de coupure de l’alimentation électrique. À la place, elles contiennent une mémoire non-volatile RWM dans laquelle les données sont sauvegardées régulièrement ou en cas de coupure de alimentation. Typiquement, la SRAM est couplée à une petite mémoire FLASH (la mémoire des clés USB et des SSD) dans laquelle on sauvegarde les données quand le courant est coupé. Si la tension d'alimentation descend en dessous d'un certain seuil critique, la sauvegarde dans l'EEPROM démarre automatiquement. Pendant la sauvegarde, la mémoire est alimentée durant quelques secondes par un condensateur de secours qui sert de batterie temporaire.
I9ntérieur de plusieurs cartouches de Nintendo Super NES. On voit la pile de sauvegarde sur les deux du haut.

Les ordinateurs personnels de type PC contiennent tous une mémoire nvRAM ou BBSRAM, appelée la CMOS RAM. Elle mémorise des paramètres de configuration basique (les paramètres du BIOS). Les paramètres en question permettent de configurer le matériel lors de l'allumage, de stocker l'heure et la date et quelques autres paramètres du genre. Les BBSRAM ont aussi été utilisées dans les cartouches de jeux vidéo, pour stocker les sauvegardes. C'est le cas sur les cartouches de jeux vidéo NES ou d'anciennes consoles de cette époque, qui contenaient une puce de sauvegarde interne à la cartouche. La puce de sauvegarde n'était autre qu'une BBSRAM dans laquelle le processeur de la console allait écrire les données de sauvegarde. Les données de sauvegarde n'étaient pas effacée quand on retirait la cartouche de la console grâce à une petit pile bouton qui alimentait la BBSRAM. Si vous ouvrez une cartouche de ce type, vous verrez la pile assez facilement. Il y avait la même chose sur les cartouches de GameBoy ou de GBA, et la pile était parfois visible sur certaines cartouches transparentes (c'était notamment le cas sur la cartouche de Pokemon Cristal).

RAM drive.

Un autre exemple de mémoire non-volatile fabriquée à partir de mémoires RAM est celui des RAM drive matériels, des disques durs composés de barrettes de RAM connectées à une carte électronique et une batterie. La carte électronique fait l'interface entre le connecteur du disque dur et les barrettes de mémoire, afin de simuler un disque dur à partir des barrettes de RAM. Les barrettes sont alimentées par une batterie, afin qu'elles ne s'effacent pas. Ces RAM drive sont plus rapides que les disques durs normaux, mais ont le défaut de consommer beaucoup plus d'électricité et d'avoir une faible capacité mémoire. Ils sont peu utilisés, car très cher et peu utiles au quotidien.

Le lien avec la technologie de fabrication et les autres critères modifier

La technologie de fabrication influence le caractère volatile ou non d'une mémoire. Prenons par exemple le cas des mémoires magnétiques. L'aimantation du support magnétique est persistante, ce qui veut dire qu'il est rare qu'un support aimanté perde son aimantation avec le temps. Le support de mémorisation ne s'efface pas spontanément et lui faire perdre son aimantation demande soit de lui appliquer un champ magnétique adapté, soit de le chauffer à de très fortes températures. Il n'est donc pas surprenant que toutes les mémoires magnétiques soient non-volatiles. Pareil pour les mémoires optiques : le plastique qui les compose ne se dégrade pas rapidement, ce qui lui permet de conserver des informations sur le long-terme. Mais pour les mémoires électroniques, ce n'est pas étonnant que des mémoires qui marche à l'électricité s'effacent quand on coupe le courant. On s'attend donc à ce que les mémoires électroniques soient volatiles, sauf pour les mémoires ROM/PROM/EPROM/EEPORM qui sont rendues non-volatiles par une conception adaptée.

Mémoire non-volatile/volatile
Mémoires électroniques Volatile ou non-volatile
Mémoires magnétiques Non-volatiles
Mémoires optiques Non-volatiles

Le lien entre caractère volatile/non-volatile et le caractère RWM/ROM est lui plus compliqué. Toutes les mémoires volatiles sont des mémoires de type RWM, le caractère volatile impliquant d'une manière ou d'une autre que la mémoire est de type RWM ou au minimum reprogrammable. Après tout, si une mémoire s'efface quand on l'éteint, c'est signe qu'on doit écrire des données utiles demande au prochain allumage pour qu'elle serve à quelque chose. Par contre, la réciproque n'est pas vraie : il existe des mémoires RWM non-volatiles, comme les disque SSD ou les disques dur. Inversement, si on ne peut pas reprogrammer ou écrire dans une mémoire, c'est signe qu'elle ne peut pas s'effacer : les mémoires de type ROM, WOM ou reprogrammables, sont forcément non-volatiles. On a donc :

  • mémoire ROM/WOM => mémoire non-volatile
  • mémoire volatile => mémoire RWM et/ou reprogrammable.

Le tableau suivant résume les liens entre le caractère volatile/non-volatile d'une mémoire et son caractère ROM/RWM. La liste des mémoires n'est pas exhaustive.

Mémoire non-volatile Mémoire volatile
Mémoire RWM
  • Disques durs, disquettes.
  • Quelques mémoires historiques, comme la mémoire à tores de ferrites.
  • Disques SSD, clés USB et autres mémoires à base de mémoire Flash.
  • Mémoires RAM non-volatiles : nvSRAM et BBSRAM.
  • Mémoires adressables de type RAM (SRAM, DRAM) : registres du processeur, mémoire cache, mémoire RAM principale.
  • Mémoires associatives (voir dans la prochaine section).
  • Mémoires tampons FIFO et LIFO (voir dans la prochaine section).
Mémoire reprogrammable
  • EPROM, EEPPROM, mémoire Flash, ...
  • Disques optiques réinscriptibles : CD, DVD, Blue-Ray.
Théoriquement possible, mais pas utilisé en pratique
Mémoire WOM
  • PROM
  • Disques optiques vierge, non-réinscriptibles : CD, DVD, Blue-Ray.
Impossible
Mémoire ROM
  • Mémoires ROM, PROM, EPROM, EEPROM, Flash.
  • Disques optiques non-vierges, non-réinscriptibles : CD, DVD, Blue-Ray.

L'adressage et les accès mémoire modifier

Les mémoires se différencient aussi par la méthode d'accès aux données mémorisées.

Les mémoires adressables modifier

Les mémoires actuelles utilisent l'adressage : chaque case mémoire se voit attribuer un numéro, l'adresse, qui va permettre de la sélectionner et de l'identifier parmi toutes les autres. On peut comparer une adresse à un numéro de téléphone (ou à une adresse d'appartement) : chacun de vos correspondants a un numéro de téléphone et vous savez que pour appeler telle personne, vous devez composer tel numéro. Les adresses mémoires en sont l'équivalent pour les cases mémoires. Ces mémoires adressables peuvent se classer en deux types : les mémoires à accès aléatoire, et les mémoires adressables par contenu.

Exemple : on demande à notre mémoire de sélectionner le byte d'adresse 1002 et on récupère son contenu (ici, 17).

Les mémoires à accès aléatoire sont des mémoires adressables, sur lesquelles on doit préciser l'adresse de la donnée à lire ou modifier. Certaines d'entre elles sont des mémoires électroniques non-volatiles de type ROM, d'autres sont des mémoires volatiles RWM, et d'autres sont des mémoires RWM non-volatiles. Comme exemple, les disques durs de type SSD sont des mémoires adressables. La mémoire principale, la fameuse mémoire RAM est aussi une mémoire adressable. D'ailleurs, le terme de mémoire RAM (Random Access Memory) désigne des mémoires qui sont à la fois adressables, de type RWM et surtout volatiles.

Les mémoires associatives fonctionnent comme une mémoire à accès aléatoire, mais dans le sens inverse. Au lieu d'envoyer l'adresse pour accéder à la donnée, on va envoyer la donnée pour récupérer son adresse : à la réception de la donnée, la mémoire va déterminer quelle case mémoire contient cette donnée et renverra l'adresse de cette case mémoire. Cela peut paraître bizarre, mais ces mémoires sont assez utiles dans certains cas de haute volée. Dès que l'on a besoin de rechercher rapidement des informations dans un ensemble de données, ou de savoir si une donnée est présente dans un ensemble, ces mémoires sont reines. Certains circuits internes au processeur ont besoin de mémoires qui fonctionnent sur ce principe. Mais laissons cela à plus tard.

Les mémoires caches modifier

Sur les mémoires caches, chaque donnée se voit attribuer un identifiant, qu'on appelle le tag. Une mémoire à correspondance stocke non seulement la donnée, mais aussi l'identifiant qui lui est attribué : cela permet ainsi de mettre à jour l'identifiant, de le modifier, etc. En somme, le Tag remplace l'adresse, tout en étant plus souple. La mémoire cache stocke donc des couples tag-donnée. À chaque accès mémoire, on envoie le tag de la donnée voulue pour sélectionner la donnée.

Fonctionnement d'une mémoire cache.

Les mémoires séquentielles modifier

Sur d'anciennes mémoires, comme les bandes magnétiques, on était obligé d'accéder aux données dans un ordre prédéfini. On parcourait la mémoire dans l'ordre, en commençant par la première donnée : c'est l'accès séquentiel. Pour lire ou écrire une donnée, il fallait visiter toutes les cases mémoires dans l'ordre croissant avant de tomber sur la donnée recherchée. Et impossible de revenir en arrière ! Et ces mémoires sont loin d'être les seules. Les CD-ROM et les DVD/Blue-Ray sont dans le même cas, dans une certaine mesure. Les mémoires de ce type sont appelées des mémoires séquentielles. Ce sont des mémoires spécialisées qui ne fonctionnent pas avec des adresses et qui ne permettent d’accéder aux données que dans un ordre bien précis, qui contraint l'accès en lecture ou en écriture.

Mémoire à accès séquentiel.

Il existe plusieurs types de mémoires séquentielles, qui se différencient par l'ordre dans lequel les données sont lues ou écrites, ou encore par leur caractère électronique, magnétique ou optique. Dans ce qui va suivre, nous allons nous restreindre aux mémoires séquentielles qui sont volatiles, la totalité étant électroniques. Si on omet les registres à décalage, les mémoires séquentielles électroniques sont toutes soit des mémoires FIFO, soit des mémoires LIFO. Ces deux types de mémoire conservent les données triées dans l'ordre d'écriture (l'ordre d'arrivée). La différence est qu'une lecture dans une mémoire FIFO renvoie la donnée la plus ancienne, alors qu'elle renverra la donnée la plus récente pour une mémoire LIFO, celle ajoutée en dernier dans la mémoire. Dans les deux cas, la lecture sera destructrice : la donnée lue est effacée.

On peut voir les mémoires FIFO comme des files d'attente, des mémoires qui permettent de mettre en attente des données tant qu'un composant n'est pas prêt. Seules deux opérations sont possibles sur de telles mémoires : mettre en attente une donnée (enqueue, en anglais) et lire la donnée la plus ancienne (dequeue, en anglais).

Fonctionnement d'une file (mémoire FIFO).

De même, on peut voir les mémoires LIFO comme des piles de données : toute écriture empilera une donnée au sommet de cette mémoire LIFO (on dit qu'on push la donnée), alors qu'une lecture enlèvera la donnée au sommet de la pile (on dit qu'on pop la donnée).

Fonctionnement d'une pile (mémoire LIFO).

Le lien entre les différents types de mémoires modifier

Le tableau suivant montre le lien entre la technologie de fabrication et les autres caractères.

Mémoire non-volatile/volatile Mémoire RWM/ROM Méthode d'accès
Mémoires électroniques Volatile ou non-volatile ROM, WOM, reprogrammables ou RWM Adressables, séquentielles, autres
Mémoires magnétiques Non-volatiles RWM Séquentielles, adressables pour les disques durs et disquettes
Mémoires optiques Non-volatiles ROM, WOM ou reprogrammable Séquentielles


Une mémoire communique avec d'autres composants : le processeur, les entrées-sorties, et peut-être d'autres. Pour cela, la mémoire est reliée à un ou plusieurs bus, des ensembles de fils qui permettent de la connecter aux autres composants. Suivant la mémoire et sa place dans la hiérarchie mémoire, le bus sera plus ou moins spécialisé. Par exemple, la mémoire principale est reliée au processeur et aux entrées-sorties via le bus système. Pour les autres mémoires, la logique est la même, si ce n'est que la mémoire est reliée à d'autres composants électroniques : une unité de calcul pour les registres, par exemple.

Dans tous les cas, le bus connecté à la mémoire est composé de deux ensembles de fils : le bus de données et le bus de commande. Le bus de données permet les transferts de données avec la mémoire, alors que le bus de commande prend en charge tout le reste. Nous allons commencer par voir le bus de données avant le bus de commandes, vu que son abord est plus simple. Le bus de commande est un ensemble d'entrées, là où ce n'est pas forcément le cas pour le bus de données. Le bus de données est soit une sortie (sur les mémoires ROM), soit une entrée-sortie (sur les mémoires RAM), les exceptions étant rares.

Le bus de commande et d'adresse modifier

Bus d'une mémoire RAM.

Le bus de commande transmet des commandes mémoire, des ordres auxquels la mémoire va devoir réagir pour faire ce qu'on lui demande. Dans les grandes lignes, chaque commande contient des bits qui ont une fonction fixée lors de la conception de la mémoire. Et les bits utilisés sont rarement les mêmes d'une mémoire à l'autre. Dans ce qui suit, nous verrons quelques bits qui reviennent régulièrement dans les bus de commande les plus communs, mais sachez qu'ils sont en réalité facultatifs. Le bus de commande dépend énormément du bus utilisé ou de la mémoire. Certains bus de commande se contentent d'un seul bit, d'autres en ont une dizaine, et d'autres en ont une petite centaine.

Comme on le verra plus bas, les mémoires adressables ont des broches dédiées aux adresses, qui sont connectées au bus d'adresse. Mais les autres mémoires s'en passent et il arrive que certaines mémoires adressables arrivent à s'en passer. Pour résumer, le bus d'adresse est facultatif, seules certaines mémoires en ayant réellement un. On peut d'ailleurs voir le bus d'adresse comme une sous-partie du bus de commandes, raison pour laquelle nous voyons les deux en même temps.

Les bits Chip Select et Output Enable modifier

La majorité des mémoires possède deux broches/bits qui servent à l'activer ou la désactiver : le bit CS (Chip Select). Lorsque ce bit est à 1, toutes les autres broches sont désactivées, qu'elles appartiennent au bus de données ou de commande. On verra dans quelques chapitres l'utilité de ce bit. Pour le moment, on peut dire qu'il permet d'éteindre une mémoire (temporairement) inutilisée. L'économie d'énergie qui en découle est souvent intéressante.

Tout aussi fréquent, le bit OE (Output Enable) désactive les broches du bus de données, laissant cependant le bus de commande fonctionner. Ce bit déconnecte la mémoire du bus de données, stoppant les transferts. Il a une utilité similaire au bit CE, avec cependant quelques différences. Ce bit ne va pas éteindre la mémoire, mais juste stopper les transmissions. L'économie d'énergie est donc plus faible. Cependant, déconnecter la mémoire est beaucoup plus rapide que de l'éteindre. On verra dans quelques chapitres l'utilité de ce bit. Grossièrement, il permet de déconnecter une mémoire quand un composant prioritaire souhaite communiquer sur le bus, en même temps que la mémoire.

L'entrée d'horloge ou de synchronisation modifier

Certaines mémoires assez anciennes n'étaient pas synchronisées par un signal d'horloge, mais par d'autres procédés : on les appelle des mémoires asynchrones. Les bus de commande de ces mémoires devaient transmettre les informations de synchronisation, sous la forme de bits de synchronisation.

D'autres mémoires sont cadencées par un signal d'horloge : elles portent le nom de mémoires synchrones. Ces mémoires ont un bus de commande beaucoup plus simple, qui n'a qu'une seule broche de synchronisation. Celle-ci reçoit le signal d'horloge, d'où le nom d'entrée d'horloge qui lui est donné.

Les bits de lecture/écriture modifier

Le bus de commande doit préciser à la mémoire s'il faut effectuer une lecture ou une écriture. Pour cela, le bus envoie sur le bus de commande un bit appelé bit R/W, qui indique s'il faut faire une lecture ou une écriture. Il est souvent admis par convention que R/W à 1 correspond à une lecture, tandis que R/W vaut 0 pour les écritures. Ce bit de commande est évidemment inutile sur les mémoires ROM, vu qu'elles ne peuvent effectuer que des lectures. Notons que les mémoires qui ont un bit R/W ont souvent un bit OE, bien que ce ne soit pas systématique. En effet, une mémoire n'a pas toujours une lecture ou écriture à effectuer et il faut préciser à la mémoire qu'elle n'a rien à faire, ce que le bit OE peut faire.

Bit OE Bit R/W Opération demandée à la mémoire
0 0 NOP (pas d'opération)
0 1 NOP (pas d'opération)
1 0 Écriture
1 1 Lecture

Une autre solution est d'utiliser un bit pour indiquer qu'on veut faire une lecture, et un autre bit pour indiquer qu'on veut démarrer une écriture. On pourrait croire que c'est un gâchis, mais c'est en réalité assez pertinent. L'avantage est que la combinaison des deux bits permet de coder quatre valeurs : 00, 01, 10 et 11. En tout, on a donc une valeur pour la lecture, une pour l'écriture, et deux autres valeurs. La logique veut qu'une de ces valeur, le plus souvent 00, indique l'absence de lecture et d'écriture. Cela permet de fusionner le bit R/W avec le bit OE. Au lieu de mettre un bit OE à 0 quand la mémoire n'est pas utilisée, on a juste à mettre le bit de lecture et le bit d'écriture à 0 pour indiquer à la mémoire qu'elle n'a rien à faire. La valeur restante peut être utilisée pour autre chose, ce qui est utile sur les mémoires qui gèrent d'autres opérations que la lecture et l'écriture. Par exemple, les mémoires EPROM et EEPROM gèrent aussi l'effacement et il faut pouvoir le préciser.

Bit de lecture Bit d'écriture Opération demandée à la mémoire
0 0 NOP (pas d'opération)
0 1 Ecriture
1 0 Lecture
1 1 Interdit, ou alors code pour une autre opération (reprogrammation, effacement, NOP sur certaines mémoires)

Le bus d'adresse modifier

Toutes les mémoires adressables sont naturellement connectées au bus. Mais celui-ci ne se limite plus à un bus de données couplé à un bus de commande : il faut ajouter un troisième bus pour envoyer les adresses à la mémoire (ou les récupérer, sur une mémoire associative). Ce dernier est appelé le bus d'adresse.

Entrées et sorties d'un bus normal.

Les bus d'adresse multiplexés modifier

Il existe quelques astuces pour économiser des fils. La première astuce est d'envoyer l'adresse en plusieurs fois. Sur beaucoup de mémoires, l'adresse est envoyée en deux fois. Les bits de poids fort sont envoyés avant les bits de poids faible. On peut ainsi envoyer une adresse de 32 bits sur un bus d'adresse de 16 bits, par exemple. Le bus d'adresse contient alors environ moitié moins de fils que la normale. Cette technique est appelée un bus d'adresse multiplexé.

Elle est surtout utilisée sur les mémoires de grande capacité, pour lesquelles les adresses sont très grandes. Songez qu'il faut 32 fils d'adresse pour une mémoire de 4 gibioctet, ce qui est déjà assez peu pour la mémoire principale d'un ordinateur personnel. Et câbler 32 fils est déjà un sacré défi en soi, là où 16 bits d'adresse est déjà largement plus supportable. Aussi, la mémoire RAM d'un ordinateur utilise systématiquement un envoi de l'adresse en deux fois. Les SRAM étant de petite capacité, elles n'utilisent que rarement un bus d'adresse multiplexé. Inversement, les DRAM utilisent souvent un bus d'adresse multiplexé du fait de leur grande capacité.

Relation entre le type de mémoire et l'envoi des adresses en une ou deux fois
Type de la mémoire Bus d'adresse normal ou multiplexé
ROM/PROM/EPROM/EEPROM Bus d'adresse normal (envoi de l'adresse en une seule fois)
SRAM Bus d'adresse normal
DRAM Bus d'adresse multiplexé (envoi de l'adresse en deux fois)

Les bus multiplexés modifier

Une autre astuce est celle des bus multiplexés, à ne pas confondre avec les bus précédents où seule l'adresse est multiplexée. Un bus multiplexé sert alternativement de bus de donnée ou d'adresse. Ces bus rajoutent un bit sur le bus de commande, qui précise si le contenu du bus est une adresse ou une donnée. Ce bit Adresse Line Enable, aussi appelé bit ALE, vaut 1 quand une adresse transite sur le bus, et 0 si le bus contient une donnée (ou l'inverse !).

Bus multiplexé avec bit ALE.

Un bus multiplexé est plus lent pour les écritures : l'adresse et la donnée à écrire ne peuvent pas être envoyées en même temps. Par contre, les lectures ne posent pas de problèmes, vu que l'envoi de l'adresse et la lecture proprement dite ne sont pas simultanées. Heureusement, les lectures en mémoire sont bien plus courantes que les écritures, ce qui fait que la perte de performance due à l'utilisation d'un bus multiplexé est souvent supportable.

Un autre problème des bus multiplexé est qu'ils ont a peu-près autant de bits pour coder l'adresse que pour transporter les données. Par exemple, un bus multiplexé de 8 bits transmettra des adresses de 8 bits, mais aussi des données de 8 bits. Cela entraine un couplage entre la taille des données et la taille de la capacité de la mémoire. Cela peut être compensé avec un bus d'adresse multiplexé, les deux techniques pouvant être combinées sans problèmes. Dans ce cas, les transferts avec la mémoire se font en plusieurs fois : l'adresse est transmise en plusieurs fois, la donnée récupérée/écrite ensuite.

Le bus de données et les mémoires multiports modifier

Le bus de données transmet un nombre fixe de bits. Dans la plupart des cas, le bus de données peut transmettre un byte à chaque transmission (à chaque cycle d'horloge). Un bus qui permet cela est appelé un bus parallèle. Quelques mémoires sont cependant connectées à un bus qui ne peut transmettre qu'un seul bit à la fois. Un tel bus est appelé un bus série. Les mémoires avec un bus série ne sont pas forcément adressables bit par bit. Elles permettent de lire ou écrire par bytes complets, mais ceux-ci sont transmis bits par bits sur le bus de données. La conversion entre byte et flux de bits sur le bus est réalisée par un simple registre à décalage. On pourrait croire que de telles mémoires séries sont rares, mais ce n'est pas le cas : les mémoires Flash, très utilisées dans les clés USB ou les disques durs SSD sont des mémoires séries.

Mémoire série et parallèle

Le sens de transmission sur le bus modifier

Le bus de données est généralement un bus bidirectionnel, rarement unidirectionnel (pour les mask ROM qui ne gèrent que la lecture). Sur la plupart des mémoires, le bus de données est bidirectionnel et sert aussi bien pour les lectures que pour les écritures.

Mémoire simple-port

Sur d'autres mémoires, on trouve deux bus de données : un dédié aux lectures et un autre pour les écritures. Le bus de commande est alors assez compliqué, dans le sens où il y a deux bus d'adresses : un qui commande l'entrée d'écriture et un pour la sortie de lecture. Le bus d'adresse est donc dupliqué et d'autres bits du bus de commande le sont aussi, mais les signaux d'horloge et le bit CS ne sont pas dupliqués. En théorie, il n'y a pas besoin de bit R/W, qui est remplacé par deux bits : un qui indique qu'on veut faire une écriture sur le bus dédié, un autre pour indiquer qu'on veut faire une lecture sur l'autre bus. L’avantage d'utiliser un bus de lecture séparé du bus d'écriture est que cela permet d'effectuer une lecture en même temps qu'une écriture. Cependant, cet avantage signifie que la conception interne de la mémoire est naturellement plus compliquée. Par exemple, la mémoire doit gérer le cas où la donnée lue est identique à celle écrite en même temps. L'augmentation du nombre de broches est aussi un désavantage.

Mémoire double port (lecture et écriture séparées)

Les mémoires multiport modifier

Le cas précédent, avec deux bus séparés, est un cas particulier de mémoire multiport. Celles-ci sont reliées non pas à un, mais à plusieurs bus de données. Évidemment, le bus de commande d'une telle mémoire est adapté à la présence de plusieurs bus de données. La plupart des bits du bus de commande sont dupliqués, avec un bit par bus de données. c'est le cas pour les bits R/W, les bits d'adresse, le bit OE, etc. Par contre, d'autres entrées du bus de commande ne sont pas dupliquées : c'est le cas du bit CS, de l'entrée d'horloge, etc. Les entrées de commandes associés à chaque bus de données, ainsi que les broches du bus de données, sont regroupées dans ce qu'on appelle un port.

Mémoire multiport, où chaque port est bidirectionnel.

Les mémoires multiport permettent de transférer plusieurs données à la fois, une par port. Le débit est sont donc supérieur à celui des mémoires mono-port. De plus, chaque port peut être relié à des composants différents, ce qui permet de partager une mémoire entre plusieurs composants.

Dans l'exemple de la section précédente, on a un port pour les lectures et un autre pour les écritures. Chaque port est donc spécialisé soit dans les lectures, soit dans les écritures. D'autres mémoires suivent ce principe et ont deux/trois ports de lecture et un d'écriture, d'autres trois ports de lecture et deux d'écriture, bref : les combinaisons possibles sont légion. Mais d'autres mémoires ont des ports bidirectionnels, capables d'effectuer soit une lecture, soit une écriture. On peut imaginer une mémoire avec 5 ports, chacun faisant lecture et écriture.

L'interface d'une mémoire ne correspond pas forcément à celle du bus mémoire modifier

En théorie, une mémoire est utilisé avec un bus qui utilise la même interface. Par exemple, une mémoire multiport est utilisée avec un bus lui-même multiport, avec des fils séparés pour les lectures et les écritures. De même, un bus multipléxé est utilisé avec une mémoire multiplexé. Mais dans certains cas, ce n'est pas le cas.

La raison à de telles configurations tient dans un fait simple : le processeur doit économiser des broches, alors que les mémoires sont épargnée par cette économie. Il faut dire qu'un processeur a besoin de beaucoup plus de broches qu'une mémoire pour faire son travail, vu que l'interface d'une mémoire est plus simple que celle du processeur. Les processeurs doivent donc utiliser pas mal de ruses pour économiser des broches, comme un usage de bus multiplexés, de bus d'adresse multiplexé, etc. A l'inverse, les mémoires peuvent parfaitement s'en passer. Les mémoires de faible capacité sont souvent sans bus multiplexés, alors que les processeurs à bas cout avec bus multiplexés sont plus fréquents.

Les bus et mémoires multiplexés modifier

Il est possible d'utiliser un bus multiplexé avec une mémoire qui ne l'est pas. La raison est que le processeur que l'on utilise un bus multiplexé pour économiser des broches, mais que la mémoire n'a pas besoin de faire de telles économies. Cela arrive si l'on prend un processeur et une mémoire à bas prix. Les mémoires multiplexées ont tendance à être plus rares et plus chères, alors que c'est l'inverse pour les processeurs.

Un exemple est donné dans le schéma ci-dessous. On voit que le processeur et une mémoire EEPROM sont reliées à un bus multiplexé très simple. Le processeur possède un bus multiplexé, alors que la mémoire a un bus d'adresse séparé du bus de données. Dans cet exemple, le processeur ne peut faire que des lectures, vu que la mémoire est une mémoire EEPROM, mais la solution marche bien dans le cas où la mémoire est une RAM. L'interface entre bus multiplexé et mémoire qui ne l'est pas se résume à deux choses : l'ajoput d'un registre en amont de l'entrée d'adresse de la mémoire, et une commande adéquate de l'entrée OE.

Pour faire une lecture, le processeur procède en deux étapes, comme sur un bus multiplexé normale : l'envoi de l'adresse, puis la lecture de la donnée.

  • Lors de l'envoi de l'adresse, l'adresse est mémorisée dans le registre, la broche ALE étant reliée à l'entrée Enable du registre. De plus, on doit déconnecter la mémoire du bus de donnée pour éviter un conflit entre l'envoi de la donnée par la mémoire et l'envoi de l'adresse par le processeur. Pour cela, on utilise l'entrée OE (Output Enable).
  • La lecture de la donnée consiste à mettre ALE à 0, et à récupérer la donnée sur le bus. Pendant cette étape, le registre maintient l'adresse sur le bus d'adresse. Le bit OE est configuré de manière à activer la sortie de données.
8051 ALE


La micro-architecture d'une mémoire adressable modifier

De nos jours, ces cellules mémoires sont fabriquées avec des composants électroniques et il nous faudra impérativement passer par une petite étude de ces composants pour comprendre comment fonctionnent nos mémoires. Dans les grandes lignes, les mémoires RAM et ROM actuelles sont toutes composées de cellules mémoires, des circuits capables de retenir un bit. En prenant plein de ces cellules et en ajoutant quelques circuits électroniques pour gérer le tout, on obtient une mémoire. Dans ce chapitre, nous allons apprendre à créer nos propres bits de mémoire à partir de composants élémentaires : des transistors et des condensateurs.

L'interface d'une cellule mémoire (généralités) modifier

Les cellules mémoires se présentent avec une interface simple, limitée à quelques broches. Et cette interface varie grandement selon la mémoire : elle n'est pas la même selon qu'on parle d'une DRAM ou d'une SRAM, avec quelques variantes selon les sous-types de DRAM et de SRAM. Là où les DRAM se limitent souvent à deux broches, les SRAM peuvent aller jusqu'à quatre. Nous reparlerons dans la suite des interfaces pour chaque type (voire sous-type) de mémoire. Pour le moment, nous allons commencer par voir le cas général. Dans les grandes lignes, on peut grouper les broches d'une cellule mémoire en plusieurs types :

  • Les broches de données, sur lesquelles on va lire ou écrire un bit.
  • Les broches de commande, sur lesquelles on envoie des ordres de lecture/écriture.
  • D'autres broches, comme la broche pour le signal d'horloge ou les broches pour l’alimentation électrique et la masse.

Les broches de données modifier

Concernant les broches de données, il y a plusieurs possibilités qui comprennent une, deux ou trois broches.

  • Dans le cas le plus simple, la cellule mémoire n'a qu'une seule broche d'entrée-sortie, sur laquelle on peut écrire ou lire un bit. On parle alors de cellule mémoire simple port.
  • Les cellules mémoires plus compliquées ont une sortie de lecture, sur laquelle on peut lire le bit stocké dans la cellule, et une entrée d'écriture, sur laquelle on place le bit à stocker dans la cellule. Dans ce cas, on parle de cellule mémoire double port.
  • Sur les cellules mémoires différentielles, la cellule mémoire dispose de deux broches d’entrée-sortie, dites différentielles, c'est à dire que le bit présent sur la seconde broche est l'inverse de la première. L'utilité de ces deux broches inversées n'est pas évidente, mais elle deviendra évidente dans le chapitre sur le plan mémoire.
Broches de données d'une cellule mémoire

Les cellules mémoires simple port se trouvent dans les DRAM modernes, comme nous le verrons plus bas dans la section sur les 1T-DRAM.

Les cellules mémoire double port sont présentes dans les SRAM et les DRAM anciennes. Quelques vieilles DRAM, nommées 2T et 3T-DRAM, étaient de ce type. Quand aux cellules SRAM double port, elles sont plus rares, mais on en trouve dans les mémoires SRAM multiport avec un port d'écriture séparé du port de lecture. Après tout, les bascules elles-mêmes ont un "port" d'écriture et un de lecture, ce qui se marie bien avec ce genre de mémoire multiport.

Quand aux cellules mémoires différentielles, toutes les SRAM modernes sont de ce type.

Les broches de commande modifier

Pour les broches de commande, il y a deux possibilités : soit la cellule reçoit un bit Enable couplé à un bit R/W, soit elle possède deux bits qui autorisent respectivement les lectures et écriture.

  • La forme la plus simple est une broche de sélection qui autorise/interdit les communications avec la cellule mémoire. Cette broche de sélection connecte ou déconnecte les autres broches du reste de la mémoire. Elle est couplée, dans le cas des mémoires RAM, à une broche R/W, sur laquelle on vient placer le fameux bit R/W, qui dit s'il faut faire une lecture ou une écriture.
  • Encore une fois, elle peut être scindée en une broche d'autorisation de lecture et une broche d'autorisation d'écriture, sur laquelle on place le bit R/W (ou son inverse) pour autoriser/interdire les écritures.
Broches de commande d'une cellule mémoire.

Il est possible de passer d'une interface à l'autre assez simplement, grâce à un petit circuit à ajouter à la cellule mémoire. Cela est utile pour faciliter la conception du contrôleur mémoire. Celui-ci peut en effet générer assez simplement le signa Enable, à envoyer sur la broche de sélection. Quant au bit R/W, il est fournit directement à la mémoire, via le bus de commande. L'interface avec les broches Enable et R/W est donc la plus facile à utiliser. Mais si on regarde l'intérieur de certaines cellules mémoire (celles de SRAM, notamment), on s’aperçoit que leur organisation interne se marie très bien avec la seconde interface, celle avec une broche d'autorisation de lecture et une pour autoriser les écritures. Il faut donc faire la conversion de la seconde interface vers la première.

Pour cela, on ajoute un petit circuit qui convertit les bits Enable et R/W en signaux d'autorisation de lecture/écriture. On peut établir la table de vérité de ce circuit assez simplement. Déjà, les deux bits d'autorisation ne sont à 1 que si le signal de sélection est à 1 : s'il est à 0, la cellule mémoire doit être totalement déconnectée du bus. Ensuite, la valeur de ces deux bits sont l'inverse l'une de l'autre : soit on fait une lecture, soit on fait une écriture, mais pas les deux en même temps. Pour finir, on peut utiliser la valeur de R/W pour savoir lequel des deux bit est à mettre à 1. On a donc le circuit suivant.

Circuit de gestion des signaux de commande d'une cellule de SRAM

Les cellules de SRAM modifier

Les cellules de SRAM ont bien évoluées depuis les toutes premières versions jusqu’au SRAM actuelles. Les toutes premières versions ne sont rien de plus que les bascules D vues dans le chapitre sur les circuits séquentiels. Elles étaient fabriquées à partir de portes logiques, ce qui donne un circuit composé de 10 à 20 transistors. Nous ne reviendrons pas sur celles-ci, pour ne pas faire de redite des chapitres précédents. De plus, les SRAM modernes arrivent à se débrouiller avec un nombre de transistors qui se compte sur les doigts d'une main. Les variantes les plus légères se contentent de 4 transistors, les intermédiaires de 6, et les plus grosses de 8 transistors. Pour réaliser ce genre de prouesses, les cellules sont conçues directement en travaillant ua niveau des transistors et non des portes logiques.n En général, moins la cellule contient de transistors, moins elle prend de place et plus elle est avantagée dans la construction de RAM de grande capacité. L'interface d'une cellule SRAM varie beaucoup suivant la cellule utilisée, mais nous allons parler des trois cas les plus courants, à savoir les SRAM double port, simple port et différentielles.

Les cellules de SRAM différentielles de type 6T-SRAM et 4T-SRAM modifier

Les cellules SRAM différentielles n'ont pas une seule broche d'entrée-sortie, mais deux. Les deux broches sont dites différentielles, c'est à dire que le bit présent sur la seconde entrée est l'inverse du premier. Elles sont souvent notées et . L'utilité de ces deux broches inversées n'est pas évidente, mais elle deviendra évidente dans le chapitre sur le plan mémoire.

Dans les chapitres du début du cours, nous avions vu qu'une bascule D est composée de deux inverseurs reliés l'un à l'autre de manière à former une boucle, avec des circuits annexes associés (un multiplexeur, notamment). Les cellules de SRAM utilisent elles aussi une boucle avec deux portes NON, mais ajoutent à peine quelques transistors autour, le strict minimum pour que le circuit fonctionne. Les deux broches d'entrée-sortie de la cellule sont directement connectées aux entrées des inverseurs, à travers deux transistors de contrôle, comme montré ci-dessous. Le tout ressemble au circuit précédent, sauf qu'on aurait retiré le multiplexeur.

Cellule de SRAM.

Le circuit se comporte différemment entre les lectures et écritures. Lors d'une écriture, les broches servent d'entrée, et elles ont la priorité car le courant envoyé sur ces entrées est plus important que le courant qui circule dans la boucle. De plus, les transistors de contrôle sont plus gros et ont une amplification plus importante que les transistors des inverseurs. Si on veut écrire un 1 dans la cellule SRAM, la broche BL aura la priorité sur la sortie de l'inverseur et la bascule mémorisera bien un 1. Lors de l'écriture d'un 0, ce sera cette fois-ci l'entrée qui aura un courant plus élevé que la sortie de l'autre inverseur, et la cellule mémorisera bien un 0, mais qui sera injecté par l'autre entrée. Lors d'une lecture, les broches n'ont aucun courant d'entrée, ce qui fait que les inverseurs fourniront un courant plus fort que celui présent sur la broche. Le contenu de la bascule est cette fois-ci envoyé dans la broche d'entrée-sortie.

Un problème de cette cellule est que les portes logiques fournissent peu de courant, ce qui est gênant lors d'une lecture. Mais expliquer en quoi cela est un problème ne peut pas se faire pour le moment, il vous faudra attendre le chapitre suivant. Toujours est-il que la faiblesse de la sortie des inverseurs est compensée en-dehors de la cellule SRAM, par des circuits spécialisés d'une mémoire, que nous verrons dans le chapitre suivant. Il s'agit de l'amplificateur de lecture, ainsi que des circuits de précharge, qui sont partagés entre toutes les cellules mémoires. N'en disons pas plus pour le moment.

Il existe plusieurs types de cellules différentielles de SRAM, qui se distinguent par la technologie utilisée : bipolaire, CMOS, PMOS, NMOS, etc. Chaque type a ses avantages et inconvénients : certaines fonctionnent plus vite, d'autres prennent moins de place, d'autres consomment moins de courant, etc. La différence tient dans la manière dont ont conçus les portes NON dans la cellule.

La cellule en technologie CMOS, dite 6T-SRAM modifier

Les deux inverseurs peuvent être conçus en utilisant la technologie CMOS, bipolaire, NMOS ou PMOS. Dans le cas de la technologie CMOS, chaque inverseur est réalisé avec deux transistors, un PMOS et un NMOS, comme nous l'avons vu dans le chapitre sur les portes logiques. La cellule mémoire obtenue est alors une cellule à 6 transistor : 2 pour l'autorisation des lectures et écritures et 4 pour la cellule de mémorisation proprement dite (les inverseurs tête-bêche).

Cellule mémoire de SRAM - rôle de chaque transistor.

Ce montage a divers avantages, le principal étant sa très faible consommation électrique. Mais son grand nombre de transistors fait que chaque cellule prend beaucoup de place. On ne peut donc pas l'utiliser pour construire des mémoires de grande capacité.

La cellule en technologie MOS, NMOS ou PMOS, dite 4T-SRAM modifier

En technologie MOS, ainsi qu'en technologie PMOS et NMOS, chaque porte logique est créée avec un transistor et une résistance. La cellule contient alors, au total, quatre transistors et deux résistances. Le circuit obtenu avec la technologie MOS est illustré ci-dessous.

Cellule de SRAM de type 4T-2R (à 4 transistors et 2 résistances).

Il est possible de remplacer la résistance par un transistor MOS câblé d'une manière précise, avec un montage dit en résistance variable. Ce montage fait que le transistor MOS se comporte comme une source de courant, équivalente au courant qui traverse la résistance.

Exemple de cellule de SRAM de technologie MOS.

D'autres cellules retirent les résistances de charge, histoire de gagner un peu de place. La réduction de taille de la cellule mémoire est assez intéressante, mais se fait au prix d'une plus grande complexité de la cellule. L'alimentation VDD doit être fournie à l'extérieur de la cellule mémoire, qui doit être alimentée à travers les transistors d'accès. Ceux-ci sont ouverts en-dehors des lectures et écritures, une tension devant être fournie sur leur source/drain, par l'extérieur de la cellule.

Cellule de SRAM MOS alimentée par les bitlines.

La cellule en technologie bipolaire modifier

Les cellules de SRAM en version bipolaire sont de loin les plus rapides, mais leur consommation électrique est bien plus élevée. C'est pour cette raison que les RAM actuelles sont toutes réalisées avec une technologie MOS ou CMOS. Presque aucune mémoire n'est réalisée en technologie bipolaire à l'heure actuelle.

Dans le cas le plus simple, qui utilise le moins de composants, la commande du transistor a lieu au niveau des émetteurs des transistors, qui sont reliés ensemble.

Cellule de SRAM en technologie bipolaire.

Il est possible d'améliorer le montage en ajoutant deux diodes, une en parallèle de chaque résistance. Cela permet d'augmenter le courant dispensé par la cellule mémoire lors d'une lecture ou écriture. Cela a son utilité, comme on le verra dans le prochain chapitre (pour anticiper : cela rend plus rapide la charge/décharge de la ligne de bit, sans système de précharge). Mais cela demande d'inverser les connexions dans la cellule mémoire. Le circuit obtenu est le suivant.

Cellule SRAM en techno bipolaire, avec ajout de diodes en parallèle.

Les cellules SRAM double port modifier

Il existe des cellules SRAM double port, avec deux entrées d'écriture et une sortie spécifique pour la lecture. Elles sont similaires aux cellules précédentes, sauf qu'on a rajouté deux transistors pour la lecture. Cela fait en tout 8 transistor, d'om le nom de 8T-SRAM donné à ce type de cellules mémoires. Le circuit précédent à six transistors est utilisé tel quel pour l'écriture. Si on utilise deux transistors pour le port de lecture, c'est pour une raison assez simple. Le premier transistor sert à connecter la sortie de l'inverseur à la ligne de bit (le fil sur lequel on récupère le bit), quand on sélectionne la cellule mémoire. Le second transistor, quant à lui est facultatif. Il est utilisé pour amplifier le signal de sortie de l'inverseur, pour l'envoyer sur la ligne de bit, pour des raisons que nous verrons au prochain chapitre.

8T SRAM

Les cellules de DRAM modifier

Comme pour les SRAM, les DRAM sont composées d'un circuit qui mémorise un bit, entouré par des transistors pour autoriser les lectures et écritures. La différence avec les SRAM tient dans le circuit utilisé pour mémoriser un bit. Contrairement aux mémoires SRAM, les mémoires DRAM ne sont pas fabriquées avec des portes logiques. À la place, elles utilisent un composant électronique qui sert de "réservoir" à électrons. Un réservoir remplit code un 1, alors qu'un réservoir vide code un 0. La nature du réservoir dépend cependant de la version de la cellule de mémoire DRAM utilisée. Car oui, il existe plusieurs types de cellules de DRAM, qui utilisent des composants réservoir différents. Étudier les versions plus récentes, actuellement utilisées dans les mémoires DRAM modernes, est bien plus facile que de comprendre les versions plus anciennes. Aussi, nous allons commencer par le cas le plus simple : les cellules de DRAM dites 1T-DRAM. Nous verrons ensuite les cellules de type 3T-DRAM, plus complexes et plus anciennes.

Les DRAM à base de condensateurs : 1T-DRAM modifier

Condensateur.

Les DRAM actuelles n'utilisent qu'un seul transistor, associé à un autre composant électronique nommé condensateur. Ce condensateur est un réservoir à électrons : on peut le remplir d’électrons ou le vider en mettant une tension sur ses entrées. Il stocke un 1 s'il est rempli, un 0 s'il est vide. C'est donc lui qui sert de circuit de mémorisation. On peut naturellement remplir ou vider un condensateur (on dit qu'on le charge ou qu'on le décharge), ce qui permet d'écrire un bit à l'intérieur.

L'intérieur d'un condensateur n'est pas très compliqué : il est formé de deux couches de métal conducteur, séparées par un isolant électrique. Les deux plaques de conducteur sont appelées les armatures du condensateur. C'est sur celles-ci que les charges électriques s'accumulent lors de la charge/décharge d'un condensateur. L'isolant empêche la fuite des charges d'une armature à l'autre, ce qui permet au condensateur de fonctionner comme un réservoir, et non comme un simple fil. Mais sur les DRAM actuelles, les condensateurs sont tellement miniaturisés qu'ils en deviennent de vraies passoires. Il possède toujours quelques défauts et des imperfections qui font que l'isolant n'est jamais totalement étanche : des électrons passent de temps en temps d'une armature à l'autre et quittent le condensateur. En clair, le bit contenu dans la cellule de mémoire DRAM s'efface. Ce qui explique qu'on doive rafraîchir régulièrement les mémoires DRAM de ce type.

Autre problème : quand on veut lire ou écrire dans notre cellule mémoire, le condensateur va être connecté sur le bus de données et se vider entièrement : on prend son contenu et il faut le récrire après chaque lecture. Pire : le condensateur se vide sur le bus, mais cela ne suffit pas à créer une tension de plus de quelques millivolts dans celui-ci. Pas de quoi envoyer un 1 sur le bus ! Mais il y a une solution : amplifier la tension de quelques millivolts induite par la vidange du condensateur sur le bus, avec un circuit adapté.

À côté du condensateur, on ajoute un transistor qui va autoriser l'écriture ou la lecture dans notre condensateur. Tant que notre transistor se comporte comme un interrupteur ouvert, le condensateur est isolé du reste du circuit : pas d'écriture ou de lecture possible. Si on l'ouvre, on pourra alors lire ou écrire dedans. Une DRAM peut stocker plus de bits pour la même surface qu'une SRAM : un transistor couplé à un condensateur prend moins de place que 6 transistors.

1T-DRAM.

Les DRAM à base de transistors : 3T-DRAM et 2D-DRAM modifier

Les premières mémoires DRAM fabriquées commercialement n'utilisaient pas un condensateur comme réservoir à électrons, mais un transistor. Pour rappel, tout transistor MOS a un pseudo-condensateur caché entre la grille et la liaison source-drain. Pour comprendre ce qui se passe dans ce transistor de mémorisation, il savoir ce qu'il y a dans un transistor CMOS. À l'intérieur, on trouve une plaque en métal appelée l'armature, un bout de semi-conducteur entre la source et le drain, et un morceau d'isolant entre les deux. L'ensemble forme donc un condensateur, certes imparfait, qui porte le nom de capacité parasite du transistor. Suivant la tension qu'on envoie sur la grille, l'armature va se remplir d’électrons ou se vider, ce qui permet de stocker un bit : une grille pleine compte pour un 1, une grille vide compte pour un 0.

Anatomie d'un transistor CMOS

L'armature n'est pas parfaite et elle se vide régulièrement, d'où le fait que la mémoire obtenue soit une DRAM. Comme avec les autres DRAM, le bit stocké dans la capacité parasite doit être rafraîchit régulièrement. Avec cette organisation, lire un bit ne le détruit pas : on peut relire plusieurs fois un bit sans que celui-ci ne soit effacé. C'est une qualité que les DRAM modernes n'ont pas.

3T-DRAM modifier

Les premières DRAM de ce type utilisaient 3 transistors, d'où leur nom de 3T-DRAM. Le bit est mémorisé dans celui du milieu, indiqué en bleu sur le schéma suivant, les deux autres transistors servant pour les lectures et écritures.

3T-DRAM.

Cette organisation donne donc, dans le cas le plus simple, une cellule avec quatre broches : deux broches de commandes, une pour la lecture et une pour l'écriture, ainsi qu'une entrée d'écriture et une sortie de lecture. Mais il est possible de fusionner certaines broches à une seule. Par exemple, on peut fusionner la broche de lecture avec celle d'écriture. De toute façon, les broches de commande diront si c'est une lecture ou une écriture qui doit être faite. De même, il est possible de fusionner les signaux de lecture et d'écriture, afin de faciliter le rafraîchissement de la mémoire. Avec une telle cellule, et en utilisant un contrôleur mémoire spécialement conçu, toute lecture réécrit automatiquement la cellule avec son contenu. Pour résumer, quatre cellules différentes sont possibles, selon qu'on fusionne ou non les broches de données et/ou les broches de commande. Ces quatre possibilités sont illustrées ci-dessous.

Organisations possibles d'une cellule de 3T-DRAM

2T-DRAM modifier

Une amélioration des 3T-DRAM permet d'éliminer un transistor. Plus précisément, l'idée est de fusionner le transistor qui stocke le bit et celui qui connecte la cellule à la bitline de lecture. Le tout donne une DRAM fabriquée avec seulement deux transistors, d'où leur nom de 2T-DRAM. La cellule 2T-DRAM est illustrée ci-dessous.

Cellule d'une 2T-DRAM.

Les cellules des mémoires EPROM, EEPROM et Flash modifier

Dans le chapitre sur les généralités des mémoires, nous avons vu les différents types de ROM : ROM proprement dite (mask ROM), PROM, EPROM, EEPROM, et mémoire Flash. Ces différents types ne fonctionnent évidement pas de la même manière, non seulement au niveau du contrôleur mémoire, mais aussi des cellules mémoires. Les cellules mémoires des mask ROM et PROM sont un peu à part, dans le sens où elles n'ont pas vraiment de cellule mémoire proprement dit. C'est ce qui fait que le fonctionnement des mémoires PROM et ROM seront vues plus tard, dans un chapitre dédié. Dans ce qui va suivre, nous n'allons voir que les mémoires de type EPROM et leurs dérivés (EEPROM, Flash). Toutes fonctionnent avec le même type de cellule mémoire, les différences étant assez mineures.

Les transistors à grille flottante modifier

Les mémoires EPROM, EEPROM et Flash sont fabriquées avec des transistors à grille flottante (un par cellule mémoire), des transistors qui possèdent deux armatures et deux couches d'isolant. La seconde armature est celle qui stocke un bit : il suffit de la remplir d’électrons pour stocker un 1, et la vider pour stocker un 0.

Transistor à grille flottante.

Pour effacer une EPROM, on doit soumettre la mémoire à des ultra-violets : ceux-ci vont donner suffisamment d'énergie aux électrons coincés dans l'armature pour qu'ils puissent s'échapper. Pour les EEPROM et les mémoires Flash, ce remplissage ou vidage se fait en faisant passer des électrons entre la grille et le drain, et en plaçant une tension sur la grille : les électrons passeront alors dans l'armature à travers l'isolant.

Les différents types de cellules EEPROM/Flash : SLC/MLC/TLC/QLC modifier

Sur la plupart des EEPROM, un transistor à grille flottante sert à mémoriser un bit. La tension contenue dans la seconde armature est alors divisée en deux intervalles : un pour le zéro, et un autre pour le un. De telles mémoires sont appelées des mémoires SLC (Single Level Cell). Mais d'autres EEPROM utilisent plus de deux intervalles, ce qui permet de stocker plusieurs bits par transistor : les mémoires MLC (Multi Level Cell) stockent 2 bits par cellules, les mémoires TLC (Triple Level Cell) stockent 3 bits, les mémoires QLC (Quad Level Cell) en stockent 4, etc.

Types de cellules mémoires d'EEPROM/Flash.

Évidemment, utiliser un transistor pour stocker plusieurs bits aide beaucoup les mémoires non-SLC à obtenir une grande capacité, mais cela se fait au détriment des performances et de la durabilité de la cellule mémoire. Typiquement, plus une cellule de mémoire FLASH contient de bits, moins elle est performante en lecture et écriture, et plus elle tolère un faible nombre d'écritures/lectures avant de rendre l'âme. Pour les performances, cela s'explique par le fait que la lecture et l'écriture doivent être plus précises sur les mémoires MLC/TLC/QLC, elles doivent distinguer des niveaux de tensions assez proches, là où l'écart entre un 0 et un 1 est assez important sur les mémoires SLC.

La lecture et l'écriture des cellules des mémoires EEPROM modifier

Sur les EEPROM, la reprogrammation et l'effacement de ces cellules demande de placer les bonnes tensions sur la grille, le drain et la source. Le procédé exact est en soit très simple, mais comprendre ce qui se passe exactement est une autre paire de manches. Les phénomènes qui se produisent dans le transistor à grille flottante lors d'une écriture ou d'un effacement sont très compliqués et font intervenir de sombres histoires de mécanique quantique. C’est la raison pour laquelle nous ne pouvons pas vraiment en dire plus.

Mise à 1 de la cellule mémoire (reprogrammation).
Mise à zéro de la cellule mémoire (effacement).


Avec le chapitre précédent, on sait que les RAM et ROM contiennent des cellules mémoires, qui mémorisent chacune un bit. On pourrait croire que cela suffit à créer une mémoire, mais il n'en est rien. Il faut aussi des circuits pour gérer l'adressage, le sens de transfert (lecture ou écriture), et bien d'autres choses. Schématiquement, on peut subdiviser toute mémoire en plusieurs circuits principaux.

  • La mémorisation des informations est prise en charge par le plan mémoire. Il est composé d'un regroupement de cellules mémoires, auxquelles on a ajouté quelques fils pour communiquer avec le bus.
  • La gestion de l'adressage et de la communication avec le bus sont assurées par un circuit spécialisé : le contrôleur mémoire, composé d'un décodeur et de circuits de contrôle.
  • L'interface avec le bus relie le plan mémoire au bus de données. C'est le plus souvent ici qu'est géré le sens de transfert des données, ainsi que tout ce qui se rapporte aux lectures et écritures.
Organisation interne d'une mémoire adressable.

Nous allons étudier le plan mémoire dans ce chapitre, le contrôleur mémoire et l'interface avec le bus seront vu dans les deux chapitres suivants. Cela peut paraitre bizarre de dédier un chapitre complet au plan mémoire, mais il y a de quoi. Celui-ci n'est pas qu'un simple amoncellement de cellules mémoire et de connexions vaguement organisées. On y trouve aussi des circuits électroniques aux noms barbares : amplificateur de tension, égaliseur de ligne de bit, circuits de pré-charge, etc. L'organisation des fils dans le plan mémoire est aussi intéressante à étudier, celle-ci étant bien plus complexe qu'on peut le croire.

Les fils et signaux reliés aux cellules modifier

Le plan mémoire est surtout composé de fils, sur lesquels on connecte des cellules mémoires. Rappelons que les cellules mémoires se présentent avec une interface simple, qui contient des broches pour le transfert des données et d'autres broches pour les commandes de lecture/écriture. Reste à voir comment toutes ses broches sont reliées aux différents bus et au contrôleur mémoire. Ce qui va nous amener à parler des lignes de bit et des signaux de sélection de ligne. Il faut préciser que la distinction entre broches de commande et de données est ici très importante : les broches de données sont connectées indirectement au bus, alors que les broches de commande sont reliées au contrôleur mémoire. Aussi, nous allons devoir parler des deux types de broches dans des sections séparées.

La connexion des broches de données : les lignes de bit modifier

Afin de simplifier l'exposé, nous allons étudier une mémoire série dont le byte est de 1 bit. Une telle mémoire est dite bit-adressable, c’est-à-dire que chaque bit de la mémoire a sa propre adresse. Nous étudierons le cas d'une mémoire quelconque plus loin, et ce pour une raison : on peut construire une mémoire quelconque en améliorant le plan mémoire d'une mémoire bit-adressable, d'une manière assez simple qui plus est. Parler de ces dernières est donc un bon marche-pied pour aboutir au cas général.

Le cas d'une mémoire bit-adressable modifier

Une mémoire bit-adressable est de loin celle qui a le plan mémoire le plus rudimentaire. Quand on sélectionne un bit, avec son adresse, son contenu va se retrouver sur le bus de données. Dit autrement, la cellule mémoire va se connecter sur ce fils pour y placer son contenu. On devine donc comment est organisé le plan mémoire : il est composé d'un fil directement relié au bus de donnée, sur lequel les cellules mémoire se connectent si besoin. Le plan mémoire se résume donc à un ensemble de cellules mémoires dont l'entrée/sortie est connectée à un unique fil. Ce fil s'appelle la ligne de bit (bitline en anglais). Une telle organisation se marie très bien avec les cellules de DRAM, qui disposent d'une unique broche d'entrée-sortie, par laquelle se font à la fois les lectures et écritures.

Plan mémoire simplifié d'une mémoire bit-adressable.

Il est possible d'utiliser une organisation avec deux lignes de bits, où la moitié des cellules est connectée à la première ligne, l'autre moitié à la seconde, avec une alternance entre cellules consécutives. Cela permet d'avoir moins de cellules mémoires connectées sur le même fil, ce qui améliore certains paramètres électriques des lignes de bit. Cette organisation porte le nom de ligne de bit repliée.

Lignes de bit repliées

Pour ce qui est des cellules mémoire double port, les choses sont un petit peu compliquées. Normalement, les cellules mémoire double port demandent d'utiliser deux lignes de bit : une pour le port de lecture, une autre pour le port d'écriture. Le tout est illustré ci-dessous. Mais certaines mémoires font autrement et utilisent des cellules mémoires double port avec des lignes de bit unique ou repliées. Dans ce cas, l'entrée et la sortie de la cellule mémoire sont connectées à la ligne de bit, et la lecture ou l'écriture sont contrôlés par l'entrée Enable de la cellule mémoire (qui autorise ou interdit les écritures).

Lignes de bits pour les cellules mémoires double port

En réalité, peu de mémoires suivent actuellement des lignes de bit normales. Les mémoires assez évoluées utilisent deux lignes de bit par colonne ! La première transmet le bit lu et l'autre son inverse. La mémoire utilise la différence de tension entre ces deux fils pour représenter le bit lu ou à écrire. Un tel codage est appelé un codage différentiel. L'utilité d'un tel codage assez difficile à expliquer sans faire intervenir des connaissances en électricité, mais tout est une histoire de fiabilité et de résistance aux parasites électriques.

De telles lignes de bits différentielles sont le plus souvent associées à des cellules mémoires elles aussi différentielles, notamment les cellules de SRAM abordées au chapitre précédent. Mais elles se marient très mal avec les cellules de SRAM non-différentielles, ainsi qu'avec les cellules mémoire de DRAM, qui n'ont qu'une seule broche d'entrée-sortie non-différentielle. Mais quelques astuces permettent d'utiliser des lignes de bit différentielles sur ces mémoires. La plus connue est de loin l'utilisation de cellules factices (dummy cells), des cellules mémoires vides placées aux bouts des lignes de bit. Lors d'une lecture, ces cellules vides se remplissent avec l'inverse du bit à lire. La ligne de bit inverse (celle qui contient l'inverse du bit) est alors remplie avec le contenu de la cellule factice, ce qui donne bien un signal différentiel. Le bit inversé est fournit par une porte logique qui inverse la tension fournie par la cellule mémoire. Cette tension remplis alors la cellule factice, avec l'inverse du bit lu.

Bitlines différentielles.

Certaines mémoires ont amélioré les lignes de bit différentielles en interchangeant leur place à chaque cellule mémoire. La ligne de bit change donc de côté à chaque passage d'une cellule mémoire. Cette organisation porte le nom de lignes de bit croisées.

Bitlines croisées.

Le cas d'une mémoire quelconque (avec byte > 1) modifier

Après avoir vu le cas des mémoires bit-adressables, il est temps d'étudier les mémoires quelconques, celles où un byte contient plus que 1 bit. Surprenamment, ces mémoires peuvent être conçues en utilisant plusieurs mémoires bit-adressables. Par exemple, prenons une mémoire dont le byte fait deux bits (ce qui est rare, convenons-en). On peut l'émuler à partir de deux mémoires de 1 bit : la première stocke le bit de poids faible de chaque byte, alors que l'autre stock le bit de poids fort. Et on peut élargir le raisonnement pour des bytes de 3, 4, 8, 16 bits, et autres. Par exemple, pour une mémoire dont le byte fait 64 bits, il suffit de mettre en parallèle 64 mémoires de 1 bit.

Mais cette technique n'est pas appliquée à la lettre, car il y a moyen d'optimiser le tout. En effet, on ne va pas mettre effectivement plusieurs mémoires bit-adressables en parallèle, car seuls les plans mémoires doivent être dupliqués. Si on utilisait effectivement plusieurs mémoires, chacune aurait son propre plan mémoire, mais aussi son propre contrôleur mémoire, ses propres circuits de communication avec le bus, etc. Or, ces circuits sont en fait redondants dans le cas qui nous intéresse.

Prenons le cas du contrôleur mémoire, qui reçoit l'adresse à lire/écrire et qui envoie les signaux de commande au plan mémoire. Avec N mémoires en parallèle, N contrôleurs mémoire recevront l'adresse et généreront les N mêmes signaux, qui seront envoyés à N plans mémoire distincts. Au lieu de cela, il est préférable d'utiliser un seul contrôleur mémoire, mais de dupliquer les signaux de commande en autant N exemplaires (autant qu'il y a de plan mémoire). Et c'est ainsi que sont conçues les mémoires quelconques : pour un byte de N bits, il faut prendre N plans mémoires de 1 bit. Cela demande donc d'utiliser N lignes de bits, reliée convenablement aux cellules mémoires. Le résultat est un rectangle de cellules mémoires, où chaque colonne est traversée par une ligne de bit. Chaque ligne du tableau/rectangle, correspond à un byte, c'est-à-dire une case mémoire.

Là encore, chaque colonne peut utiliser des lignes de bits différentielles ou croisées.

Plan mémoire, avec les bitlines.

La connexion des broches de commande : le transistor et le signal de sélection modifier

Évidemment, les cellules mémoires ne doivent pas envoyer leur contenu sur la ligne de bit en permanence. En réalité, chaque cellule est connectée sur la ligne de bit selon les besoins. Les cellules correspondant au mot adressé se connectent sur la ligne de bit, alors que les autres ne doivent pas le faire. La connexion des cellules mémoire à la ligne de bit est réalisée par un interrupteur commandable, c’est-à-dire par un transistor appelé transistor de sélection. Quand la cellule mémoire est sélectionnée, le transistor se ferme, ce qui connecte la cellule mémoire à la ligne de bit. À l'inverse, quand une cellule mémoire n'est pas sélectionnée, le transistor de sélection se comporte comme un interrupteur ouvert : la cellule mémoire est déconnectée du bus.

La commande du transistor de sélection est effectuée par le contrôleur mémoire. Pour chaque ligne de bit, le contrôleur mémoire n'ouvre qu'un seul transistor à la fois (celui qui correspond à l'adresse voulue) et ferme tous les autres. La correspondance entre un transistor de sélection et l'adresse est réalisée dans le contrôleur mémoire, par des moyens que nous étudierons dans les prochains chapitres. Toujours est-il que le contrôleur mémoire génère, pour chaque octet, un bit qui dit si celui-ci est adressé ou non. Ce bit est appelé le signal de sélection. Le signal de sélection est envoyé à toutes les cellules mémoire qui correspondent au byte adressé. Vu que tous les bits d'un byte sont lus ou écrits en même temps, toutes les cellules correspondantes doivent être connectées à la ligne de bit en même temps, et donc tous les transistors de sélection associés doivent se fermer en même temps. En clair, le signal de sélection est partagé par toutes les cellules d'un même mot mémoire.

Signal de sélection et Byte.

Le cas des lignes de bit simples et repliées modifier

Voyons comment les bitlines simples sont reliées aux cellules mémoires. Les mémoires 1T-DRAM n'ont qu'une seule broche entrée/sortie, sur laquelle on effectue à la fois les lectures et les écritures. Cela se marie très bien avec des bitlines simples, mais ça les rend incompatibles avec des bitlines différentielles. Le cas des DRAM à bitlines simples, avec une seule sortie, un seul transistor de sélection, est illustré ci-dessous.

Plan mémoire d'une mémoire bit-adressable.

La connexion des transistors de sélection pour des lignes de bit repliée n’est pas très différente de celle des lignes de bit simple. Elle est illustrée ci-dessous.

Ligne de bit repliée.

Le cas des lignes de bit différentielles modifier

Le cas des mémoires SRAM est de loin le plus simple à comprendre. Celles-ci utilisent toutes (ou presque) des bitlines différentielles, chose qui se marie très bien avec l'interface des cellules SRAM. Rappelons que celles-ci possèdent deux broches d'entrée-sortie pour les données : une broche Q sur laquelle on peut lire ou écrire un bit, et une broche complémentaire sur laquelle on envoie/récupère l'inverse du bit lu/écrit. À chaque broche correspond un transistor de sélection différent, qui sont intégrés dans la cellule de mémoire SRAM.

Connexion d'une cellule mémoire de SRAM à une bitline différentielle.

Le cas des cellules mémoires double port modifier

Après avoir vu les cellules mémoire "normales" plus haut, il est temps de passer aux cellules mémoire de type double port (celles avec une sortie pour les lectures et une entrée pour les écritures). Elles contiennent deux transistors : un pour l'entrée d'écriture et un pour la sortie de lecture. Le contrôleur mémoire est relié directement aux transistors de sélection. Il doit générer à la fois les signaux d'autorisation de lecture que ceux pour l'écriture. Ces deux signaux peuvent être déduit du bit de sélection et du bit R/W, comme vu dans le chapitre précédent.

Circuit d'interface entre contrôleur mémoire et cellule mémoire.

Sur les mémoires double port, le transistor de lecture est connecté à la ligne de bit de lecture, alors que celui pour l'écriture est relié à la ligne de bit d'écriture.

Plan mémoire d'une SRAM double port.

Pour les mémoires simple port, les deux transistors sont reliés à la même ligne de bit. Ils vont s'ouvrir ou se fermer selon les besoins, sous commande du contrôleur mémoire.

Plan mémoire d'une SRAM simple port.

L'amplificateur de tension modifier

Quand on connecte une cellule mémoire à une ligne de bit, c'est à la cellule mémoire de fournir le courant pour mettre la ligne à 0 ou à 1. Mais dans certains cas, la cellule mémoire ne peut pas fournir assez courant pour cela. Cela arrive sur les mémoires DRAM, basées sur un condensateur, mais les SRAM ne font pas exception ! Sur les DRAM, ces condensateurs ont une faible capacité et ne peuvent pas conserver beaucoup d'électrons, surtout sur les mémoires modernes. Du fait de la miniaturisation, les condensateurs des DRAM actuelles ne peuvent stocker que quelques centaines d'électrons, parfois beaucoup moins. Autant dire que la vidange du condensateur dans la ligne de bit ne suffit pas à la mettre à 1, même si la cellule mémorisait bien un 1. La lecture crée à peine une tension de quelques millivolts dans la ligne de bit, pas plus. Mais il y a une solution : amplifier la tension de quelques millivolts induite par la vidange du condensateur sur le bus. Pour cela, il faut donc placer un dispositif capable d'amplifier cette tension, bien nommé amplificateur de lecture.

Amplificateur différentiel.

L’amplificateur utilisé n'est pas le même avec des lignes de bit simples et des lignes de bit différentielles. Dans le cas différentiel, l'amplificateur doit faire la différence entre les tensions sur les deux lignes de bit et traduire cela en un niveau logique. C'est l'amplificateur lui-même qui fait la conversion entre codage différentiel (sur deux lignes de bit) et codage binaire. Pour le distinguer des autres amplificateurs, il porte le nom d'amplificateur différentiel. L'amplificateur différentiel possède deux entrées, une pour chaque ligne de bit, et une sortie. Dans ce qui va suivre, les entrées seront notées et , la sortie sera notée . L’amplificateur différentiel fait la différence entre ces deux entrées et amplifie celle-ci. En clair :

Il faut noter qu'un amplificateur différentiel peut fonctionner aussi bien avec des lignes de bit différentielles qu'avec des lignes de bit simples. Avec des lignes de bit simples, il suffit de placer l'autre entrée à la masse, au 0 Volts, et de n'utiliser qu'une seule sortie.

Il existe de nombreuses manières de concevoir un amplificateur différentiel, mais nous n'allons aborder que les circuits les plus simples. Dans les grandes lignes, il existe deux types d'amplificateurs de lecture : ceux basés sur des bascules et ceux basés sur une paire différentielle. Bizarrement, vous verrez que les deux ont une certaine ressemblance avec les cellules de SRAM ! Il faut dire qu'une porte NON, fabriquée avec des transistors, est en réalité un petit amplificateur spécialisé, chose qui tient au fonctionnement de son circuit.

L'amplificateur de lecture à paire différentielle modifier

Le premier type d'amplificateur que nous allons voir est fabriqué à partir de transistors bipolaires. Pour rappel, un transistor bipolaire contient deux entrées, la base et l'émetteur, et une sortie appelée le collecteur. Il prend en entrée un courant sur sa base et fournit un courant amplifié sur l'émetteur. Pour cela, il faut fournir une source de courant sur le collecteur, obtenue en faisant une tension aux bornes d'une résistance.

Transistor bipolaire, explication simplifiée de son fonctionnement

La paire différentielle est composée de deux amplificateurs de ce type, reliés à un générateur de courant. La résistance est placée entre la tension d'alimentation et le transistor, alors que le générateur de courant est placé entre le transistor et la tension basse (la masse, ou l'opposé de la tension d'alimentation, selon le montage). Le circuit ci-contre illustre le circuit de la paire différentielle.

Paire différentielle, avec des résistances.

Précisons que la résistance mentionnée précédemment peut être remplacée par n'importe quel autre circuit, l'essentiel étant qu'il fournisse un courant pour alimenter l'émetteur. Il en est de même que le générateur de courant. Dans le cas le plus simple, une simple résistance suffit pour les deux. Mais ce n'est pas cette solution qui est utilisée dans les mémoires actuelles. En effet, intégrer des résistances est compliqué dans les circuits à semi-conducteurs modernes, et les mémoires RAM en sont. Aussi, les résistances sont généralement remplacées par des circuits équivalents, qui ont le même rôle ou qui peuvent remplacer une résistance dans le montage voulu.

Les deux résistances du haut sont remplacées chacunes par un miroir de courant, à savoir un circuit qui crée un courant constant sur une sortie et le recopie sur une seconde sortie. Il existe plusieurs manières de créer un tel miroir de courant avec des transistors MOS/CMOS, la plus simple étant illustrée ci-dessous (le miroir de courant est dans l'encadré bleu). On pourrait aborder le fonctionnement d'un tel circuit, pourquoi il fonctionne, mais nous n'en parlerons pas ici. Cela relèverait plus d'un cours d'électronique analogique, et demanderait de connaître en détail le fonctionnement d'un transistor, les équations associées, etc. L'avantage est que le miroir de courant fournit le même courant aux deux bitlines, il égalise les courants dans les deux bitlines.

Paire différentielle. Le générateur de courant est en jaune, le miroir de courant est en bleu.

L'amplificateur de lecture à verrou modifier

Le second type d'amplificateur de lecture est l'amplificateur à verrou. Il amplifie une différence de tension entre les deux lignes de bit d'une colonne différentielle. Les deux colonnes doivent être préchargées à Vdd/2, à savoir la moitié de la tension d'alimentation. La raison à cela deviendra évidente dans les explications qui vont suivre. Toujours est-il que ce circuit a besoin qu'un circuit dit de précharge s'occupe de placer la tension adéquate sur les lignes de bit, avant toute lecture ou écriture. Nous reparlerons de ce circuit de précharge dans les sections suivantes, vers la fin de ce chapitre. Cela peu paraître peu pédagogique, mais à notre décharge, sachez que le circuit de précharge et l'amplificateur de lecture sont intimement liés. Il est difficile de parler de l'un sans parler de l'autre et réciproquement. Pour le moment, tout ce que vous avez à retenir est qu'avant toute lecture, les lignes de bit sont chargées à Vdd/2, ce qui permet à l'amplificateur à verrou de fonctionner correctement.

Le circuit de l'amplificateur de lecture à verrou modifier

L'amplificateur à verrou est composé de deux portes NON reliées tête-bêche, comme dans une cellule de SRAM. Chaque ligne de bit est reliée à l'autre à travers une porte NON. Sauf que cette fois-ci, il n'y a pas toujours de transistors de sélection, ou alors ceux-ci sont placés autrement.

Amplificateur de lecture à bascule.

Le circuit complet est illustré ci-dessous, de même qu'une version plus détaillée avec des transistors CMOS. Du fait de son câblage, l'amplificateur à verrou a pour particularité d'avoir des broches d'entrées qui se confondent avec celles de sortie : l'entrée et la sortie pour une ligne de bit sont fusionnées en une seule broche. L'utilisation d'inverseurs explique intuitivement pourquoi il faut précharger les lignes de bit à Vdd/2 : cela place la tension dans la zone de sécurité des deux inverseurs, là où la tension ne correspond ni à un 0, ni à un 1. Le fonctionnement du circuit dépend donc du fonctionnement des transistors, qui servent alors d'amplificateurs.

Amplificateur de lecture à bascule, version détaillée.

On peut noter que cet amplificateur est parfois fabriqué avec des transistors bipolaires, qui consomment beaucoup de courant. Mais même avec des transistors MOS, il est préférable de réduire la consommation électrique du circuit, quand bien même ceux-ci consomment peu. Pour cela, on peut désactiver l’amplificateur quand on ne l'utilise pas. Pour cela, on entoure l'amplificateur avec des transistors qui le débranchent, le déconnectent si besoin.

Amplificateur de lecture à bascule, avec transistors d'activation.

Le fonctionnement de l'amplificateur à verrou modifier

Expliquer en détail le fonctionnement de l'amplificateur à verrou demanderait de faire de l'électronique assez poussée. Il nous faudrait détailler le fonctionnement d'un transistor quand il est utilisé en tant qu'amplificateur, donner des équations, et bien d'autres joyeusetés. À la place, je vais donner une explication très simplifiée, que certains pourraient considérer comme fausse (ce qui est vrai, dans une certaine mesure).

Avant toute chose, précisons que les seuils pour coder un 0 ou un 1 ne sont pas les mêmes entre l’entrée d'une porte NON et sa sortie. Ils sont beaucoup plus resserrés sur l'entrée, la marge de sécurité entre 1 et 0 étant plus faible. Un signal qui ne correspondrait pas à un 0 ou un 1 en sortie peut l'être en entrée.

Le fonctionnement du circuit ne peut s'expliquer correctement qu'à partir du rapport entre tension à l'entrée et tension de sortie d'une porte NON. Le schéma ci-dessous illustre cette relation. On voit que la porte logique amplifie le signal d'entrée en plus de l'inverser. Pour caricaturer, on peut décomposer cette caractéristique en trois parties : deux zones dites de saturation et une zone d'amplification. Dans la zone de saturation, la tension est approximativement égale à la tension maximale ou minimale, ce qui fait qu'elle code pour un 0 ou un 1. Entre ces deux zones extrêmes, la tension de sortie dépend linéairement de la tension d'entrée (si on omet l'inversion).

Caractéristique tension d'entrée-tension de sortie d'un inverseur CMOS.

Quand on place deux portes NON l'une à la suite de l'autre, le résultat est un circuit amplificateur, dont la caractéristique est illustrée dans le second schéma. On voit que l'amplificateur amplifie la différence de tension entre VDD/2 et la tension d'entrée (sur la ligne de bit).

Utilisation de deux portes NON comme amplificateur de tension.

Si on regarde le circuit complet, on s’aperçoit que chaque ligne de bit est bouclée sur elle-même, à travers cet amplificateur. Cela fait boucler la sortie de l'amplificateur sur son entrée : la tension de base est alors amplifiée une fois, puis encore amplifiée, et ainsi de suite. Au final, les seuls points stables du montage sont la tension maximale ou la tension minimale, soit un 0 ou un 1, ou la tension VDD/2.

Ceci étant dit, on peut enfin comprendre le fonctionnement complet du circuit d'amplification. Commençons l'explication par la situation initiale : la ligne de bit est préchargée à VDD/2, et la cellule mémoire est déconnectée des lignes de bit. La ligne de bit est préchargée à VDD/2, l'amplificateur a sa sortie comme son entrée égales à VDD/2 et le circuit est parfaitement stable. Ensuite, la cellule mémoire à lire est connectée à la ligne de bit et la tension va passer au-dessous ou au-dessus de VDD/2. Nous allons supposer que celle-ci contenait un 1, ce qui fait que sa connexion entraîne une montée de la tension de la ligne de bit. La tension ne va cependant pas monter de beaucoup, mais seulement de quelques millivolts. Cette différence de tension va être amplifiée par les deux portes logiques, ce qui amplifie la différence de tension. Et rebelote : cette différence amplifiée est ré-amplifiée par le montage, et ainsi de suite jusqu’à ce que le circuit se stabilise soit à 0 soit à 1.

Fonctionnement très simplifié de l'amplificateur à verrou.

L'optimisation du temps de charge/décharge des lignes de bit modifier

Si les lignes de bit sont de simples fils conducteurs passifs, cela ne veut pas dire qu'ils n'ont pas d'influence sur les lectures et écritures. En réalité, ils jouent un grand rôle dans la rapidité des accès mémoire, pour des raisons techniques. Selon leur longueur, la tension va prendre plus ou moins de temps pour s'établir dans la ligne de bit, ce qui impacte directement les performances de la mémoire. Diverses techniques ont étés inventées pour résoudre ce problème, la plus importante étant l'utilisation d'un circuit dit de pré-charge, que nous allons étudier maintenant.

Les lignes de bit ne sont pas des fils parfaits : non seulement ils ont une résistance électrique, mais ils se comportent aussi comme des condensateurs (dans une certaine mesure). Nous n'expliquerons pas dans la physique de ce phénomène, mais allons simplement admettre qu'un fil électrique se modélise bien en mettant une résistance R en série avec un condensateur C : le circuit obtenu est un circuit RC. Le condensateur, appelée capacité parasite, n’apparaît que lorsque la tension de la ligne de bit change en passant de 0 à 1 ou inversement. Ce qui n'arrive que lors d'une lecture ou écriture, cela va de soit. Lorsque l'on change la tension en entrée d'un tel montage, la tension de sortie met un certain temps avant d'atteindre la valeur d'entrée. Ce qui est illustré dans les deux schémas ci-dessous, pour la charge (passage de 0 à 1) et la décharge (passage de 1 à 0). La variation est d'ailleurs exponentielle. On estime qu'il faut un temps égal , avec R la valeur de la résistance et C celle du condensateur. En clair : la ligne de bit met un certain temps avant que la tension atteigne celle qui correspond au bit lu ou à écrire.

Circuit RC série. Tension aux bornes d'un circuit RC en charge. Tension aux bornes d'un circuit RC en cours de décharge.

L'organisation du plan mémoire modifier

Une première idée pour optimiser le temps RC est de diminuer la résistance de la ligne. Il se trouve que celle-ci est proportionnelle à la longueur de la ligne de bit : plus la ligne de bit est longue, plus la résistance R sera élevée. On voit donc une première solution pour réduire la résistance, et donc le temps RC : réduire la taille des lignes de bit. Les petites mémoires, avec peu de cellules sur une colonne, ont des lignes de bit plus petites et sont donc plus rapides. Cela explique en partie pourquoi les temps d'accès des mémoires varient selon la capacité, chose que nous avons abordé il y a quelques chapitres. De même, à capacité égale, il vaut mieux utiliser des bytes large, pour réduire la taille des colonnes.

L'agencement en colonne de donnée ouvertes modifier

Mais d'autres optimisations du plan mémoire permettent d'obtenir des lignes de bit plus petites, à capacité et largeur de byte inchangée. Par exemple, on peut placer l'amplificateur de lecture au milieu du plan mémoire, et non au bout. En faisant ainsi, on doit couper la ligne de bit en deux, chaque moitié étant placée d'un côté ou de l'autre de l’amplificateur. La colonne contient ainsi deux lignes de bits séparées, chacune ayant une longueur réduite de moitié. Cette organisation est dite en colonne de donnée ouvertes. Mais cette organisation a un défaut : il est difficile d'implémenter l'amplificateur au milieu de la mémoire. Le nombre de fils qui doivent passer par le milieu de la RAM est important, rendant le câblage compliqué. De plus, les perturbations électromagnétiques ne touchent pas de la même manière chaque côté de la mémoire et l'amplificateur peut donner des résultats problématiques à cause d'elles.

Optimisations du plan mémoire pour réduire la taille des bitlines.

Il est aussi possible de répartir les amplificateurs de tension autrement. On peut mélanger les organisations en colonne de données ouvertes et "normales", en mettant les amplificateurs à la fois au milieu de la RAM et sur les bords. Une moitié des amplificateurs est placée au milieu du plan mémoire, l'autre moitié est placée sur les bords. On alterne les lignes de bits connectée entre amplificateurs selon qu’ils sont sur les bords ou au milieu. L'organisation est illustrée ci-dessous.

Organisation en colonnes de données ouvertes, avec répartition des amplificateur sur les bords du plan mémoire.

La pré-charge des lignes de bit modifier

Aperçu d'une ligne de bit conçue pour être préchargée. On voit qu'il s'agit d'une ligne de bit "normale", à laquelle a été ajouté un circuit qui permet de charger la ligne à partir de la tension d'alimentation. L'amplificateur de tension est situé du côté opposé au circuit de charge.

Une autre solution, beaucoup plus ingénieuse, ne demande pas de modifier la longueur des lignes de bit. À la place, on rend leur charge plus rapide en les pré-chargeant. Sans pré-charge, la ligne de bit est à 0 Volts avant la lecture et la lecture altère cette tension, que ce soit pour la laisser à 0 (lecture d'un 0), ou pour la faire monter à la tension maximale Vdd (lecture d'un 1). Le temps de réaction de la ligne de bit dépend alors du temps qu'il faut pour la faire monter à Vdd. Avec la pré-charge, la ligne de bit est chargée avant la lecture, de manière à la mettre à la moitié de Vdd. La lecture du bit fera descendre celle-ci à 0 (lecture d'un 0) ou la faire grimper à Vdd (lecture d'un 1). Le temps de charge ou de décharge est alors beaucoup plus faible, vu qu'on part du milieu.

Il faut noter que la pré-charge à Vdd/2 est un cas certes simple à comprendre, mais qui n'a pas valeur de généralité. Certaines mémoires pré-chargent leurs lignes de bit à une autre valeur, qui peut être Vdd, à 60% de celui-ci, ou une autre valeur. En fait, tout dépend de la technologie utilisée. Par exemple, Les mémoires de type CMOS pré-chargent à Vdd/2, alors que les mémoires TTL, NMOS ou PMOS pré-chargent à une autre valeur (le plus souvent Vdd).

On peut penser qu'il faudra deux fois moins de temps, mais la réalité est plus complexe (regardez les graphes de charge/décharge situés plus haut). De plus, il faut ajouter le temps mis pour précharger la ligne de bit, qui est à ajouter au temps de lecture proprement dit. Sur la plupart des mémoires, la pré-charge n'est pas problématique. Il faut dire qu'il est rare que la mémoire soit accédé en permanence et il y a toujours quelques temps morts pour pré-charger la ligne de bit. On verra que c'est notamment le cas sur les mémoires DRAM synchrones modernes, comme les SDRAM et les mémoires DDR. Mais passons...

Les circuits de précharge modifier

La pré-charge d'une ligne de bit se fait assez facilement : il suffit de connecter la ligne de bit à une source de tension qui a la valeur adéquate. Par exemple, une mémoire qui se pré-charge à Vdd a juste à relier la ligne de bit à la tension d'alimentation. Mais attention : cette connexion doit disparaître quand on lit ou écrit un bit dans les cellules mémoire. Sans cela, le bit envoyé sur la ligne de bit sera perturbé par la tension ajoutée. Il faut donc déconnecter la ligne de bit de la source d'alimentation lors d'une lecture écriture. On devine rapidement que le circuit de pré-charge est composé d'un simple interrupteur commandable, placé entre la tension d'alimentation (Vdd ou Vdd/2) et la ligne de bit. Le contrôleur mémoire commande cet interrupteur pour précharger la ligne de bit ou stopper la pré-charge lors d'un accès mémoire. Si un seul transistor suffit pour les lignes de bit simples, deux sont nécessaires pour les lignes de bit différentielles ou croisées. Ils doivent être ouvert et fermés en même temps, ce qui fait qu'ils sont commandés par un même signal.

Circuits de précharge

L'égaliseur de tension modifier

Pour les lignes de bit différentielles et croisées, il se peut que les deux lignes de bit complémentaires n'aient pas tout à fait la même tension suite à la pré-charge. Pour éviter cela, il est préférable d'ajouter un circuit d'égalisation qui égalise la tension sur les deux lignes. Celui-ci est assez simple : c'est un interrupteur commandable qui connecte les deux lignes de bit lors de la pré-charge. Là encore, un simple transistor suffit. L'égalisation et la pré-charge ayant lieu en même temps, ce transistor est commandé par le même signal que celui qui active le circuit de précharge. Le circuit complet, qui fait à la fois pré-charge et égalisation des tensions, est représenté ci-dessous.

Circuits de précharge et d'égalisation pour des lignes de bit différentielles.


La gestion de l'adressage et de la communication avec le bus sont assurées par un circuit spécialisé : le contrôleur mémoire. Une mémoire adressable est ainsi composée :

  • d'un plan mémoire ;
  • du contrôleur mémoire, composé d'un décodeur et de circuits de contrôle ;
  • et des connexions avec le bus.
Organisation interne d'une mémoire adressable.

Nous avons vu le fonctionnement du plan mémoire dans les chapitres précédents. Les circuits qui font l'interface entre le bus et la mémoire ne sont pas différents des circuits qui relient n'importe quel composant électronique à un bus, aussi ceux-ci seront vus dans le chapitre sur les bus. Bref, il est maintenant temps de voir comment fonctionne un contrôleur mémoire. Je parlerai du fonctionnement des mémoires multiports dans le chapitre suivant.

Les mémoires à adressage linéaire modifier

Pour commencer, nous allons voir les mémoires à adressage linéaire. Sur ces mémoires, le plan mémoire est un tableau rectangulaire de cellules mémoires, et toutes les cellules mémoires d'une ligne appartiennent à une même case mémoire. Les cellules mémoire d'une même colonne sont connectées à la même bitline. Avec cette organisation, la cellule mémoire stockant le énième bit du contenu d'une case mémoire (le bit de poids i) est reliée au énième fil du bus. Rappelons que chaque ligne est reliée à un signal de sélection de ligne Row Line, qui permet de connecter les cellules mémoires du byte adressé à la bitline.

Principe d'un plan mémoire linéaire.

Le rôle du contrôleur mémoire est de déduire quelle entrée Row Line mettre à un à partir de l'adresse envoyée sur le bus d'adresse. Pour cela, le contrôleur mémoire doit répondre à plusieurs contraintes :

  • il reçoit des adresses codées sur n bits : ce contrôleur a donc n entrées ;
  • l'adresse de n bits peut adresser 2n bytes : notre contrôleur mémoire doit donc posséder 2^n sorties ;
  • la sortie numéro N est reliée au N-iéme signal Row Line (et donc à la N-iéme case mémoire) ;
  • on ne doit sélectionner qu'une seule case mémoire à la fois : une seule sortie devra être placée à 1 ;
  • et enfin, deux adresses différentes devront sélectionner des cases mémoires différentes : la sortie de notre contrôleur qui sera mise à 1 sera différente pour deux adresses différentes placées sur son entrée.

Le seul composant électronique qui répond à ce cahier des charges est le décodeur, ce qui fait que le contrôleur mémoire se résume à un simple décodeur, avec quelques circuits pour gérer le sens de transfert (lecture ou écriture), et la détection/correction d'erreur. Ce genre d'organisation s'appelle l'adressage linéaire.

Décodeur et plan mémoire d'une mémoire à accès aléatoire.

Les mémoires à adressage ligne-colonne modifier

Sur des mémoires ayant une grande capacité, l'adressage linéaire n'est plus vraiment pratique. Si le nombre de sorties est trop grand, utiliser un seul décodeur utilise trop de portes logiques et a un temps de propagation au fraises, ce qui le rend impraticable. Pour éviter cela, certaines mémoires organisent leur plan mémoire autrement, sous la forme d'un tableau découpé en lignes et en colonnes, avec une case mémoire à l'intersection entre une colonne et une ligne. Ce type de mémoire s'appelle des mémoires à adressage ligne-colonne, ou encore des mémoires à adressage bidimensionnel.

Adressage par coïncidence stricte.

Adresser la mémoire demande de sélectionner la ligne voulue, et de sélectionner la colonne à l'intérieur de la ligne. Pour cela, chaque ligne a un numéro, une adresse de ligne, et il en est de même pour les colonnes qui sont adressées par un numéro de colonne, une adresse de colonne. L'adresse mémoire complète n'est autre que la concaténation de l'adresse de ligne avec l'adresse de colonne. L'avantage est que l'adresse mémoire peut être envoyée en deux fois à la mémoire : on envoie d'abord la ligne, puis la colonne. Ce n'est cependant pas systématique, et on peut parfaitement envoyer adresse de ligne et de colonne en même temps, en une seule adresse. Envoyer l'adresse en deux fois permet d'économiser des fils sur le bus d'adresse, mais nécessite de mémoriser l'adresse de ligne dans un registre. Lorsque l'on envoie l'adresse de la colonne sur le bus d'adresse, la mémoire doit avoir mémorisé l'adresse de ligne envoyée au cycle précédent. Pour cela, la mémoire incorpore deux registres pour mémoriser la ligne et la colonne. Il faut aussi ajouter de quoi aiguiller le contenu du bus d'adresse vers le bon registre, en utilisant un démultiplexeur.

Mémoire avec double envoi.

Du point de vue du contrôleur mémoire, sélectionner une ligne est facile : on utilise un décodeur. Mais la méthode utilisée pour sélectionner la colonne dépend de la mémoire utilisée. On peut utiliser un multiplexeur ou un décodeur, suivant l'organisation interne de la mémoire. L'avantage est qu'utiliser deux décodeurs assez petits prend moins de circuits qu'un seul gros décodeur. Idem pour l'usage d'un décodeur assez petit avec un multiplexeur lui-même petit, comparé à un seul gros décodeur.

Les avantages de cette organisation sont nombreux : possibilité de décoder une ligne en même temps qu'une colonne, possibilité d'envoyer l'adresse en deux fois, consommation moindre de portes logiques, etc.

Les mémoires à tampon de ligne modifier

Les mémoires à ligne-colonne les plus simples à comprendre sont les mémoires à tampon de ligne, aussi appelées mémoires à Row Buffer. Sur celles-ci, l'accès à une ligne copie celle-ci dans un registre interne, appelé le tampon de ligne (en anglais, Row Buffer). Puis, un circuit, généralement un multiplexeur, sélectionne la colonne adéquate dans ce tampon de ligne.

Mémoire à tampon de ligne

Une telle mémoire est composée d'un multiplexeur couplé à une mémoire qui mémorise les lignes. Avant le tampon de ligne, se trouve une mémoire normale, à adressage linéaire, dont chaque case mémoire est une ligne. Dit autrement une mémoire à tampon de ligne émule une mémoire de N bytes à partir d'une mémoire contenant B fois moins de bytes, mais dont chacun des bytes seront B fois plus gros. Chaque accès lit ou écrit un « super-byte » de la mémoire interne, et sélectionne le bon byte dans celui-ci. Sur le schéma du dessous, on voit bien que la mémoire est composée de deux grands morceaux : une mémoire à adressage linéaire, et un circuit de sélection d'un mot mémoire parmi B (souvent un multiplexeur).

Mémoire à row buffer - principe.

Le tampon de ligne s'intercale entre la mémoire à adressage linéaire et la sélection des colonnes.

Mémoire à tampon de ligne à registre.

Les avantages de cette organisation sont nombreux. Le plus simple à comprendre est que le rafraichissement est beaucoup plus rapide et plus simple. Rafraichir la mémoire se fait ligne par ligne, et non byte par byte. Autre avantage : en concevant correctement la mémoire, il est possible d'améliorer les performances lors de l'accès à des données proches en mémoire. Par contre, cette organisation consomme beaucoup d'énergie. Il faut dire que pour chaque lecture d'un byte dans notre mémoire, on doit charger une ligne complète dans le tampon de ligne.

L'avantage principal est que l'accès à des données proches, localisées sur la même ligne, est fortement accéléré sur ces mémoires. Quand une ligne est chargée dans le tampon de ligne, elle reste dans ce tampon durant plusieurs accès consécutifs. Et les accès ultérieurs peuvent utiliser cette possibilité. Nous allons supposer qu'une ligne a été copiée dans le tampon de ligne, lors d'un accès précédent. Deux cas sont alors possibles.

  • Premier cas : on accède à une donnée située dans une ligne différente : c'est un défaut de tampon de ligne. Dans ce cas, il faut vider le tampon de ligne, en plus de sélectionner la ligne adéquate et la colonne. L'accès mémoire n'est alors pas différent de ce qu'on aurait sur une mémoire sans tampon de ligne, avec cependant un temps d'accès un peu plus élevé.
  • Second cas : la donnée à lire/écrire est dans la ligne chargée dans le tampon de ligne. Ce genre de situation s'appelle un succès de tampon de ligne. Dans ce cas, la ligne entière a été recopiée dans le tampon de ligne et on n'a pas à la sélectionner : on doit juste changer de colonne. Le temps nécessaire pour accéder à notre donnée est donc égal au temps nécessaire pour sélectionner une colonne, auquel il faut parfois ajouter le temps nécessaire entre deux sélections de deux colonnes différentes.

Les accès consécutifs à une même ligne sont assez fréquents. Ils surviennent lorsque l'on doit accéder à des données proches les unes des autres en mémoire, ce qui est très fréquent. La majorité des programmes informatiques accèdent à des données proches en mémoire : c'est le principe de localité spatiale vu dans les chapitres précédents. Les mémoires à tampon de ligne profitent au maximum de cet effet de localité spatiale, ce qui leur donne un boost de performance assez important. Dans les faits, la mémoire RAM de votre ordinateur personnel est une mémoire à tampon de ligne. Toutes les mémoires RAM des ordinateurs grand public sont des mémoires à tampon de ligne, et ce depuis plusieurs décennies.

L'adressage par coïncidence modifier

Avec l'adressage par coïncidence, la sélection de la ligne se fait comme pour toutes les autres mémoire : grâce à un signal row line qui est envoyé à toutes les cases mémoire d'une même ligne. Les mémoires à adressages par coïncidence font la même chose mais pour les colonnes. Toutes les cases mémoires d'une colonne sont reliées à un autre fil, le column line. Une case mémoire est sélectionnée quand ces row lines et la column line sont tous les deux mis à 1 : il suffit d'envoyer ces deux signaux aux entrées d'une porte ET pour obtenir le signal d'autorisation de lecture/écriture pour une cellule. On utilise donc deux décodeurs : un pour sélectionner la ligne et un autre pour sélectionner la colonne.

Adressage par coïncidence stricte - intérieur de la mémoire.

L'avantage de cette organisation est que l'on a pas à recopier une ligne complète dans un tampon de ligne pour faire un accès mémoire. Mais c'est aussi un défaut car cela ne permet pas de profiter des diverses optimisations pour des accès à une même ligne. Ce qui explique que ces mémoires ne sont presque pas utilisées de nos jours, du moins pas dans les ordinateurs personnels.

Les mémoires à adresse tridimensionnel : les mémoires par blocs modifier

l'adressage par coïncidence peut être amélioré en rajoutant un troisième niveau de subdivision : chaque ligne est découpée en sous-lignes, qui contiennent plusieurs colonnes. On obtient alors des mémoires par blocs, ou divided word line structures. Chaque ligne est donc découpée en N lignes, numérotées de 0 à N-1. Les sous-lignes qui ont le même numéro sont en quelque sorte alignées verticalement, et sont reliées aux mêmes bitlines : celles-ci forment ce qu'on appelle un bloc. Chacun de ces blocs contient un plan mémoire, un multiplexeur, et éventuellement des amplificateurs de lecture et des circuits d'écriture.

Divided word line.

Tous les blocs de la mémoire sont reliés au décodeur d'adresse de ligne. Mais malgré tout, on ne peut pas accéder à plusieurs blocs à la fois : seul un bloc est actif lors d'une lecture ou d'une écriture. Pour cela, un circuit de sélection du bloc se charge d'activer ou de désactiver les blocs inutilisés lors d'une lecture ou écriture. L'adresse d'une sous-ligne bien précise se fait par coïncidence entre une ligne, et un bloc.

Adressage par bloc.

La ligne complète est activée par un signal wordline, généré par un décodeur de ligne. Les blocs sont activés individuellement par un signal nommé blocline, produit par un décodeur de bloc : ce décodeur prend en entrée l'adresse de bloc, et génère le signal blocline pour chaque bloc. Ensuite, une fois la sous-ligne activée, il faut encore sélectionner la colonne à l'intérieur de la sous-ligne sélectionnée, ce qui demande un troisième décodeur. L'adresse mémoire est évidemment coupée en trois : une adresse de ligne, une adresse de sous-ligne, et une adresse de colonne.

Annexe : l'attaque rowhammer modifier

Vous connaissez maintenant comment fonctionnent les cellules mémoires et le plan mémoire, ce qui fait que vous avez les armes nécessaires pour aborder des sujets assez originaux. Profitons-en pour aborder une faille de sécurité présente dans la plupart des mémoires DRAM actuelles : l'attaque row hammer. Vous avez bien entendu : il s'agit d'une faille de sécurité matérielle, qui implique les mémoires RAM, qui plus est. Voilà qui est bien étrange. D'ordinaire, quand on parle de sécurité informatique, on parle surtout de failles logicielles ou de problèmes d'interface chaise-clavier. La plupart des attaques informatiques sont des attaques d’ingénierie sociale où on profite de failles humaines pour obtenir un mot de passe ou toute autre information confidentielle, suivies par les failles logicielles, les virus, malwares et autres méthodes purement logicielles. Mais certaines failles de sécurités sont purement matérielles et profitent de bugs présents dans le matériel pour fonctionner. Car oui, les processeurs, mémoires, bus et périphériques peuvent avoir des bugs matériels qui sont généralement bénins, mais que des virus, logiciels ou autres malware peuvent exploiter pour commettre leur méfaits.

Attaque Row hammer - les lignes voisines en jaune sont accédées un grand nombre de fois à la suite, la ligne violette est altérée.

L'attaque row hammer, aussi appelée attaque par martèlement de mémoire, utilise un bug de conception des mémoires DRAM. Le bug en question tient dans le fait que les cellules mémoires ne sont pas parfaites et que leur charge électrique tend à fuir. Ces fuites de courant se dispersent autour de la cellule mémoire et tendent à affecter les cellules mémoires voisines. En temps normal, cela ne pose aucun problème : les fuites sont petites et l'interaction électrique est limitée. Cependant, des hackers ont réussit à exploiter ce comportement pour modifier le contenu d'une cellule mémoire sans y accèder. En accédant d'une manière bien précise à une ligne de la mémoire, on peut garantir que les fuites de courant deviennent signifiantes, suffisamment pour recopier le contenu d'une ligne mémoire dans les lignes mémoires voisines. Pour cela, il faut accéder un très grand nombre de fois à la cellule mémoire en question, ce qui explique pourquoi cette attaque s'appelle le martèlement de mémoire. Une autre méthode, plus fiable, est d’accéder à deux lignes de mémoires, qui prennent en sandwich la ligne mémoire à altérer. On accède successivement à la première, puis la seconde, avant de reprendre au début, et cela un très grand nombre de fois par secondes.

Modifier plusieurs bytes sans y accéder, mais en accédant à leurs voisins est une faille exploitable par les pirates informatiques. L'intérêt est de contourner les protections mémoires liées au système d'exploitation. Sur les systèmes d'exploitation modernes, chaque programme se voit attribuer certaines portions de la mémoire, auxquelles il est le seul à pouvoir accéder. Des mécanismes de protection mémoire intégré dans le processeur permettent d'isoler la mémoire de chaque programme, comme nous le verrons dans le chapitre sur la mémoire virtuelle. Mais avec row hammer, les accès à un byte attribué à un programme peuvent déborder sur les bytes d'un autre programme, avec des conséquences assez variables. Par exemple, un virus présent en mémoire pourrait interagir avec le byte qui mémorise un mot de passe ou une clé de sécurité RSA, ou toute donnée confidentielle. Il pourrait récupérer cette information, ou alors la modifier pour la remplacer par une valeur connue et l'attaquant.

La faille row hammer est d'autant plus simple que la physique des cellules mémoire est médiocre. Les progrès de la miniaturisation rendent cette attaque de plus en plus facilement exploitable, les fuites étant d'autant plus importantes que les cellules mémoires sont petites. Mais exploiter cette attaque est compliqué, car il faut savoir à quelle adresse se situe a donnée à altérer, sans compter qu'il faut avoir des informations sur l'adresse des cellules voisines. Rappelons que la répartition physique des adresses/bytes dépend de comment la mémoire est organisée en interne, avec des banques, rangées et autres. Deux adresses consécutives ne sont pas forcément voisines sur la barrette de mémoire et l relation entre deux adresses de cellules mémoires voisines n'est pas connue avec certitude tant elle varie d'un système mémoire à l'autre.

Les solutions pour mitiger l'attaque row hammer sont assez limitées. Une première solution est d'utiliser les techniques de correction et de détection d'erreur comme l'ECC, mais là l'effet est limité. Une autre solution est de rafraichir la mémoire plus fréquemment, mais cela a un effet assez limité, sans compter que cela a un impact sur les performance et la consommation d'énergie de la RAM. Les concepteurs de matériel ont dû inventer des techniques spécialisées, comme le pseudo target row refresh d'Intel ou le target row refresh des mémoires LPDDR4. Ces techniques consistent, pour simplifier, à détecter quand une ligne mémoire est accédée très souvent, à rafraichir les lignes de mémoire voisines assez régulièrement. L'effet sur les performances est limité, mais cela demande d'intégrer cette technique dans le contrôleur mémoire externe/interne.


Les mémoires vues au chapitre précédent sont les mémoires les plus simples qui soient. Mais ces mémoires peuvent se voir ajouter quelques améliorations pas franchement négligeables, afin d'augmenter leur rapidité, ou de diminuer leur consommation énergétique. Dans ce chapitre, nous allons voir quelles sont ces améliorations les plus courantes.

Les mémoires synchrones modifier

Les toutes premières mémoires des mémoires asynchrones, non-synchronisées avec le processeur via une horloge. Avec elles, le processeur devait attendre que la mémoire réponde et devait maintenir adresse et données pendant ce temps. Pour éviter cela, les concepteurs de mémoire ont synchronisé les échanges entre processeur et mémoire avec un signal d'horloge : les mémoires synchrones sont nées. L'utilisation d'une horloge a l'avantage d'imposer des temps d'accès fixes. Un accès mémoire prend un nombre déterminé (2, 3, 5, etc) de cycles d'horloge et le processeur peut faire ce qu'il veut dans son coin durant ce temps.

Les types de mémoires synchrones en fonction de leurs registres modifier

Fabriquer une mémoire synchrone demande de rajouter des registres sur les entrées/sorties d'une mémoire asynchrone.

La première méthode ne mémorise que l'adresse d'entrée et les signaux de commande dans un registre synchronisé sur l'horloge.

Mémoire synchrone première génération.

Une seconde méthode mémorise l'adresse, les signaux de commande, ainsi que les données à écrire.

Mémoire synchrone seconde génération.

Et la dernière méthode mémorise toutes les entrées et sorties de la mémoire dans des registres.

Mémoire synchrone troisième génération.

La distinction entre mémoire à flot direct et mémoire pipelinée modifier

Outre la distinction faite dans la section précédente, il faut aussi faire une distinction entre les mémoires à flot direct, les mémoires registre à verrou et les mémoires pipelinées. Les mémoires à flot direct regroupent les deux premiers cas de la section précédente.

Avec les mémoires à flot direct, soit la donnée lue ne subit pas de mémorisation dans un registre de sortie.

Mémoire à flot direct.

Avec les mémoires synchrones pipelinées, la donnée sortante est mémorisée dans un registre commandé par un front d'horloge, afin d'augmenter la fréquence de la mémoire et son débit. Le temps de latence des lectures est plus long avec cette technique, car il faut ajouter un cycle supplémentaire pour lire la donnée, comme on l'expliquera plus bas. Cela ralentit un peu les lectures, mais a un avantage qu'on expliquera dans ce qui suit.

Mémoire synchrone registre à registre.

Avec les mémoires registre à verrou, il y a un registre pour la donnée lue, mais ce registre est un registre commandé par un signal Enable et non sur un front d'horloge. Le registre commandé par un signal Enable est en quelque sorte transparent. L'usage d'un registre de ce type fait qu'on n’a pas à rajouter un cycle d'horloge pour chaque lecture, mais cela fait qu'on se prive de l'avantage des mémoires pipelinées.

Mémoire synchrone registre à verrou

Les mémoires synchrones non-pipelinées modifier

Grâce à l'ajout du registre d'adresse, le processeur n'a pas à maintenir l'adresse en entrée de la mémoire durant toute la durée d'un accès mémoire : le registre s'en charge. Voici ce que cela donne pour une lecture avec l'ajout d'un registre sur l'adresse uniquement. Le diagramme montre ce qu'il se passe pendant un cycle d'horloge complet. On voit que sur la mémoire asynchrone, l'adresse doit être maintenue durant toute la durée du cycle d'horloge mémoire. Mais sur la mémoire synchrone, l'adresse est envoyée en début du cycle seulement. L'avantage est que le processeur est plus rapide que la mémoire. Sur une mémoire asynchrone, le processeur devait maintenir l'adresse durant une dizaine de cycles d'horloge du processeur. Mais sur les mémoires synchrones, il maintient l'adresse durant un cycle processeur, avant de faire autre chose et de revenir quelques cycles plus tard pour lire la donnée. Le fonctionnement des écritures est similaire.

Différence entre mémoire asynchrone et synchrone avec mémorisation d'adresse uniquement.

L'ajout d'un registre pour les écritures permet de faire la même chose, mais pour les écritures. Sans ce registre, le processeur doit maintenir la donnée pendant toute l'écriture. Il envoie la donnée à écrire juste après l'adresse, mais doit la maintenir durant un bon bout de temps, durant lequel le processeur ne peut pas faire autre chose. L'ajout d'un registre pour la donnée écrite permet d'éliminer ce problème. La donnée à écrire est envoyée en même temps que l'adresse et le processeur n'a plus rien à faire au cycle suivant. On dit que l'on réalise une écriture anticipée.

Différence entre mémoire asynchrone et synchrone avec mémorisation des écritures

Un défaut des écritures anticipées est qu'elles marchent assez mal quand on enchaine les lectures et écritures. Prenons le cas simple où une lecture est suivie d'une écriture, les deux étant consécutives, séparées par un seul cycle d'horloge. Au premier cycle d'horloge, le processeur va présenter l'adresse à lire à la mémoire. Au second cycle, la donnée lue sera disponible. Mais le processeur va aussi présenter l'adresse et la donnée à écrire. Autant présenter deux adresses à la suite n'est pas un problème, autant lire une donnée et écrire la suivante en même temps en est un. Le bus de donnée ne permet pas de gérer des accès simultanés. Pour éviter cela, on peut utiliser des écritures tardives, où la donnée à écrire est présentée un cycle d'horloge après la présentation de l'adresse d'écriture.

Écriture tardive.

Les mémoires synchrones gèrent à la fois les écritures anticipées et les écritures tardives, sauf pour quelques exceptions. Quelques bits de commande permettent de choisir quelle écriture réaliser, pour chaque écriture. Le choix entre écriture tardives et écritures est réalisé par le processeur, et/ou par le contrôleur de mémoire externe, celui qui est placé sur la carte mère ou dans le processeur. Le choix se fait suivant les accès mémoire à réaliser, suivant qu'il y ait ou non alternance entre lectures et écritures, et bien d'autres paramètres.

Un problème avec les écritures tardives survient quand une écriture est suivie par une lecture à la même adresse. La lecture lira une donnée qui n'a pas encore été mise à jour par l'écriture. Pour éviter cela, on doit mettre en attente l'écriture dans le cas où l'adresse est la même qu'une lecture précédente. Une autre méthode revient à renvoyer directement le contenu du registre d'écriture sur le bus de donnée. Dans les deux cas, il faut ajouter un comparateur qui vérifie les deux adresses consécutives.

Les mémoires synchrones pipelinées modifier

L'ajout d'un registre pour la donnée lue a pour conséquence de décaler les lectures d'un cycle d'horloge. Sur une mémoire à flot direct (non-pipelinée), la lecture ne prend qu'un cycle d'horloge. Si l'adresse de lecture est envoyée lors d'un cycle, la donnée lue est présentée au cycle suivant. Mais avec le fonctionnement en pipeline, on rajoute un cycle d'horloge de décalage, ce qui fait que la donnée lue est disponible deux cycles après la présentation de l'adresse. La raison est que le premier cycle lit la donnée dans la RAM asynchrone et le mémorise dans le registre de sortie, mais ce n'est qu'au cycle suivant, une fois l'écriture dans le registre finie, que la donnée est disponible sur le bus.

Comme on le voit, l'accès en lecture se fait en deux étapes : on envoie l'adresse lors d'un premier cycle, effectue la lecture durant le cycle suivant, et récupère la donnée sur le bus un cycle plus tard. On pourrait aussi tenir compte du fait que la mémoire est organisée en lignes et en colonnes : l'étape de lecture peut ainsi être scindée en une étape pour sélectionner la ligne, et une autre pour sélectionner la colonne. Cela rajouterait un cycle d'horloge, vu qu'il y a une étape en plus. Mais ce découpage d'un accès mémoire en étapes a un avantage certain, qui se comprend bien en comparant une mémoire non-pipelinée et une mémoire pipelinée.

Sur une mémoire pipelinée, on doit attendre que l'accès mémoire précédent soit terminé avant d'en lancer un autre. Dans le cas d'une mémoire à flot direct, voici ce que cela donne.

Accès mémoires sans pipeline.

Mais en théorie, il serait possible d'envoyer l'adresse de la prochaine lecture, pendant qu'on lit la donnée de la lecture précédente. En effet, la donnée lue au cycle précédent est dans le registre de sortie et n'est donc pas altérée si on lance une lecture dans la SRAM interne.

Accès mémoires avec pipeline.

On peut faire la même chose, mais pour une mémoire multiplexée. Sans pipelining, on doit effectuer une sélection de la ligne, puis de la colonne, puis lire/écrire la donnée, avant de passer à l'accès suivant.

Accès mémoire multiplexé sans pipelining

Mais avec l'usage d'un pipeline, les choses changent. On peut accéder à une nouvelle ligne par cycle. Pendant qu'on envoie une nouvelle adresse de ligne, le tampon de ligne mémorise la ligne précédente et accède aux colonnes, pendant que le registre de sortie contient encore la donnée d'il y a deux cycles.

Accès mémoire multiplexé avec pipelining

Pour résumer, avec une mémoire synchrone sans pipeline, on doit attendre qu'un accès mémoire soit fini avant d'en démarrer un autre. Avec les mémoires pipelinées, on peut réaliser un accès mémoire sans attendre que les précédents soient finis. En quelque sorte, ces mémoires ont la capacité de traiter plusieurs requêtes de lecture/écriture en même temps, tant que celles-ci sont chacune à des étapes différentes. Par exemple, on peut envoyer l'adresse de la prochaine requête pendant que l'on accède à la donnée de la requête courante. Les mémoires qui utilisent ce pipelining ont donc un débit supérieur aux mémoires qui ne l'utilisent pas : le nombre d'accès mémoire traités par seconde augmente. Cette technique demande d'enchainer les différentes étapes à chaque cycle : les adresses et ordres de commande doivent arriver au bon endroit au bon moment. Pour cela, on est obligé de modifier le contrôleur de mémoire interne et y ajouter un circuit séquentiel qui envoie des ordres aux différents composants de la mémoire dans un ordre déterminé. La complexité de ce circuit séquentiel dépend fortement du nombre d'étapes utilisé pour gérer l'accès mémoire.

Pipemining mémoire

Précisons que le temps d'accès mémoire ne change pas beaucoup avec le pipelining. Par contre, le nombre de cycles nécessaires pour traiter une requête de lecture/écriture augmente : d'un seul cycle, on passe à autant de cycles qu'il y a d'étapes. Mais la fréquence est supérieure pour les mémoires pipelinées, ce qui compense exactement l'augmentation du nombre d'étapes. En effet, un cycle sur une mémoire non-pipelinée correspond à un accès mémoire complet, tandis qu'un cycle est égal à la durée d'une seule étape sur une mémoire pipelinée. Une explication plus détaillée sera donnée dans le chapitre sur l'usage du pipeline dans les processeurs.

Les écritures doublement tardives modifier

Plus haut, pour les mémoires non-pipelinées, nous avions vu qu'il est possible qu'il y ait des conflits d'accès où une donnée lue est envoyée sur le bus en même temps qu'une donnée à écrire. Ces conflits, qui ont lieu lors d'alternances entre lectures et écritures, sont appelés des retournements de bus.

La solution pour les mémoires non-pipelinées était de retarder la présentation de la donnée à écrire, ce qui donne des écritures tardives. Mais sur les mémoires pipelinées, les lectures sont décalées d'un cycle d'horloge, ce qui fait que cette méthode ne marche plus parfaitement. On peut se retrouver avec de tels conflits si les écritures et lectures ont des mauvais timings. Le problème est que les écritures et lectures ne prennent pas le même nombre de cycles d'horloges pour d’exécuter, ce qui fait qu'elles peuvent se marcher sur les pieds. La solution des écritures tardives, à savoir retarder la présentation de la donnée à écrire, marche toujours. Mais il faut adapter le retard pour le faire correspondre au temps des lectures. Vu que la lecture prend deux cycles sur une mémoire pipelinée, il faut retarder l'écriture de deux cycles d'horloge, et non d'un seul. Cette technique s'appelle l'écriture doublement tardive.

Les écritures doublement tardives posent le même problème que les écritures tardives. Il se peut qu'une lecture accède à une adresse écrite au cycle précédent ou dans les deux cycles précédents. La lecture lira alors une donnée pas encore mise à jour par l'écriture. Pour éviter cela, il faut soit mettre en attente la lecture, soit renvoyer le contenu du registre d'écriture sur le bus de donnée au bon timing. Dans les deux cas, il faut ajouter deux comparateurs : un qui compare l'adresse à lire avec l'adresse précédente, et un qui compare avec l'adresse d'il y a deux cycles.

L'accès en rafale modifier

L'accès en rafale est un accès mémoire qui permet de lire ou écrire un bloc de mémoire de taille fixe en envoyant une seule adresse. Concrètement, cela permet de lire/écrire plusieurs adresses mémoires consécutives les unes après les autres, en envoyant une seule adresse, en un seul accès mémoire. On envoie la première adresse et la mémoire s'occupe de lire les adresses suivantes les unes après les autres, automatiquement. L'accès en rafale fait que l'on n'a pas à envoyer plusieurs adresses pour lire un bloc de mémoire, mais une seule. Cela libère le processeur durant quelques cycles, et lui économise du travail. Un accès de ce type est appelé un accès en rafale, ou encore une rafale.

Accès en mode rafale.

La taille d'un bloc lu/écrit en une fois dépend de la mémoire. Il est généralement fixé une fois pour toutes et toutes les rafales ont la même taille. Par exemple, sur les mémoires asynchrones EDO-RAM, les rafales lisent/écrivent 4 octets consécutifs automatiquement, au rythme d'un par cycle d’horloge. D'autres mémoires gèrent plusieurs tailles pré-fixées, que l'on peut choisir au besoin. Par exemple, on peut choisir entre une rafale de 4 octets consécutifs, 8 octets consécutifs, ou 16 octets consécutifs. Par exemple, sur les mémoires SDRAM, on peut choisie s'il faut lire 1, 2, 4, ou 8 octets en rafale.

L'accès en rafale séquentiel, linéaire et entrelacé modifier

Il existe plusieurs types d'accès en rafale : l'accès entrelacé, l'accès linéaire et l'accès séquentiel.

Le mode séquentiel est le mode rafale normal : on accède à des octets consécutifs les uns après les autres. Peu importe l'adresse à laquelle on commence, on lit les N adresses suivantes lors de l'accès en rafale.

Le mode linéaire est un petit peu plus compliqué. Il lit un bloc de taille fixe, qui est aligné en mémoire. Par exemple, prenons un bloc de rafale de 8 octets, dont les bytes ont les adresses 0, 1, 2, 3, 4, 5, 6, et 7. Un accès linéaire n'est pas obligé de commencer par lire ou écrire le byte 0 : on peut très bien commencer par lire ou écrire au byte 3, par exemple. Dans ce cas, on commence par lire le byte numéroté 3, puis le 4, le 5, le 6 et le 7. Puis, l'accès reprend au bloc 1 et on accède aux blocs 1, 2 et 3. En clair, la mémoire est découpée en bloc de 8 bytes consécutifs et l'accès lit un bloc complet. Si la première adresse lue commence à la première adresse du bloc, l'accès est identique à l'accès séquentiel. Mais si l'adresse de départ de la rafale est dans le bloc, la lecture commence à ce bloc, puis reprend au début du bloc une fois arrivé au bout du bloc. Un accès en rafale parcourt un mot (un bloc de mots mémoires de même taille que le bus).

Le mode entrelacé utilise un ordre différent. Avec ce mode de rafale, le contrôleur mémoire effectue un XOR bit à bit entre un compteur (incrémenté à chaque accès) et l'adresse de départ pour calculer la prochaine adresse de la rafale.

Pour comprendre un petit peu mieux ces notions, nous allons prendre l'exemple du mode rafale sur les processeurs x86 présents dans nos ordinateurs actuels. Sur ces processeurs, le mode rafale permet des rafales de 4 octets, alignés sur en mémoire. Les rafales peuvent se faire en mode linéaire ou entrelacé, mais il n'y a pas de mode séquentiel. Vu que les rafales se font en 4 octets dans ces deux modes, la rafale gère les deux derniers bits de l'adresse, qui sont modifiés automatiquement par la rafale. Dans ce qui suit, nous allons indiquer les deux bits de poids faible et montrer comment ils évoluent lors d'une rafale. Le reste de l'adresse ne sera pas montré, car il pourrait être n'importe quoi.

Voici ce que cela donne en mode linéaire :

Accès en mode rafale de type linéaire sur les processeurs x86.
1er accès 2nd accès 3ème accès 4ème accès
Exemple 1 00 01 10 11
Exemple 2 01 10 11 00
Exemple 3 10 11 00 01
Exemple 4 11 00 01 10

Voici ce que cela donne en mode entrelacé :

Accès en mode rafale de type entrelacé sur les processeurs x86.
1er accès 2nd accès 3ème accès 4ème accès
Exemple 1 00 01 10 11
Exemple 2 01 00 11 10
Exemple 3 10 11 00 01
Exemple 4 11 10 01 00

L'implémentation des accès en rafale modifier

Au niveau de la microarchitecture, l'accès en rafale s'implémente en ajoutant un compteur dans la mémoire. L'adresse de départ est mémorisée dans un registre en aval de la mémoire. Ce registre n'est autre que le registre qui permet de transformer une mémoire asynchrone en mémoire synchrone, qu'on a vu plus haut.

Pour gérer les accès en rafale séquentiels, il suffit que le registre qui stocke l'adresse mémoire à lire/écrire soit transformé en compteur.

Pour les accès en rafale linéaire, le compteur est séparé de ce registre. Ce compteur est initialisé à 0 lors de la transmission d'une adresse, mais est incrémenté à chaque cycle sinon. L'adresse à lire/écrire à chaque cycle se calcule en additionnant l'adresse de départ, mémorisée dans le registre, au contenu du compteur. Pour les accès en rafale entrelacés, c'est la même chose, sauf que l'opération effectuée entre l'adresse de départ et le compteur n'est pas une addition, mais une opération XOR bit à bit.

Microarchitecture d'une RAM avec accès en rafale linéaire.

Le préchargement et les mémoires de type Dual et quad data rate modifier

Les processeurs sont de plus en plus exigeants et la vitesse de la mémoire commence à être de plus en plus limitante pour leurs performances. Pour augmenter la vitesse de la mémoire, la solution la plus évidente est d'augmenter sa fréquence. Mais le seul problème, c'est que c'est plus facile à dire qu'à faire ! Il faut dire que le plan mémoire ne peut pas vraiment être rendu plus rapide, compte tenu des contraintes techniques actuelles. Une autre solution pourrait être d'augmenter le débit de la mémoire, ce qui revient à échanger plus de données par cycle d'horloge. Cette solution à l'avantage de profiter de la localité spatiale, le fait que les programmes ordinaires ont souvent besoin d’accéder à des données consécutives. Problème : il faudrait rajouter des broches sur la mémoire et câbler plus de fils. Le prix de la mémoire s'envolerait et elle serait bien plus difficile à concevoir, sans compter les difficultés pour faire fonctionner l'ensemble à haute fréquence.

Si accroître sa fréquence ou la largeur du bus a trop de désavantages, il existe une solution alternative, qui est une sorte de mélange des deux techniques. Cette technique s'appelle le préchargement, prefetching en anglais. Elle donne naissance aux mémoires mémoires Dual Data Rate, aussi appelées mémoires DDR. Il s'agit de mémoires SDRAM améliorées, avec une interface avec la mémoire légèrement bidouillée.

Les mémoires sans préchargement modifier

Les mémoires sans préchargement sont appelées des mémoires SDR (Single Data Rate). Avec elles, le plan mémoire et le bus vont à la même fréquence et ils ont la même largeur (le nombre de bits transmit en une fois). Par exemple, si le bus mémoire a une largeur de 64 bits et une fréquence de 100 MHz, alors le plan mémoire fait de même. Toute augmentation de la fréquence et/ou de la largeur du bus se répercute sur le plan mémoire et réciproquement. Problème, le plan mémoire est difficile à faire fonctionner à haute fréquence, mais peut avoir une largeur assez importante sans problèmes. Pour le bus, c'est l'inverse : le faire fonctionner à haute fréquence est possible, bien que cela requière un travail d'ingénierie assez conséquent, alors qu'en augmenter la largeur poserait de sérieux problèmes.

Mémoire SDR.

Les mémoires avec préchargement modifier

L'idée du préchargement est un compromis idéal entre les deux contraintes précédentes : on augmente la largeur du plan mémoire sans en augmenter la fréquence, mais on fait l'inverse pour le bus. En faisant cela, le plan mémoire a une fréquence inférieure à celle du bus, mais a une largeur plus importante : on peut y lire ou y écrire 2, 4, 8 fois plus de données d'un seul coup. Le contrôleur et le bus mémoire fonctionnent à une fréquence plus élevée de celle du plan mémoire, pour compenser. Si le plan mémoire a une largeur de N fois celle du bus, le bus a une fréquence N plus élevée pour compenser.

Sur les mémoires DDR (Double Data Rate), le plan mémoire est deux fois plus large que le bus, mais a une fréquence deux fois plus faible. Les données lues ou écrites dans le plan mémoire sont envoyées en deux fois sur le bus, ce qui est compensé par le fait qu'il soit deux fois plus rapide. Ceci dit, il faut trouver un moyen pour découper un mot mémoire de 128 bits en deux blocs de 64, à envoyer sur le bus dans le bon ordre. Cela se fait dans l'interface avec le bus, grâce à une sorte de mémoire tampon un peu spéciale, dans laquelle on accumule les 128 bits lus ou à écrire.

Mémoire DDR.
Sur les mémoires DDR dans les ordinateurs personnels, seul un signal d'horloge est utilisé, que ce soit pour le bus, le plan mémoire, ou le contrôleur. Seulement, le bus et les contrôleurs mémoire réagissent à la fois sur les fronts montants et sur les fronts descendants de l'horloge. Le plan mémoire, lui, ne réagit qu'aux fronts montants.

Il existe aussi des mémoires quad data rate, pour lesquelles la fréquence du bus est quatre fois celle du plan mémoire. Évidemment, la mémoire peut alors lire ou écrire 4 fois plus de données par cycle que ce que le bus peut supporter.

Mémoire QDR.

Vous remarquerez que le préchargement se marie extrêmement bien avec le mode rafale.

Le préchargement augmente donc le débit théorique maximal. Sur les mémoires sans préchargement, le débit théorique maximal se calcule en multipliant la largeur du bus de données par sa fréquence. Par exemple, une mémoire SDRAM fonctionnant à 133 Mhz et qui utilise un bus de 8 octets, aura un débit de 8 * 133 * 1024 * 1024 octets par seconde, ce qui fait environ du 1 giga-octets par secondes. Pour les mémoires DDR, il faut multiplier la largeur du bus mémoire par la fréquence, et multiplier le tout par deux pour obtenir le débit maximal théorique. En reprenant notre exemple d'une mémoire DDR fonctionnant à 200 Mhz et utilisée en simple channel utilisera un bus de 8 octets, ce qui donnera un débit de 8 * 200 * 1024 * 1024 octets par seconde, ce qui fait environ du 2.1 gigaoctets par secondes.

Les banques et rangées modifier

Sur certaines puces mémoires, un seul boitier peut contenir plusieurs mémoires indépendantes regroupées pour former une mémoire unique plus grosse. Chaque sous-mémoire indépendante est appelée une banque, ou encore un banc mémoire. La mémoire obtenue par combinaison de plusieurs banques est appelée une mémoire multi-banques. Cette technique peut servir à améliorer les performances, la consommation d'énergie, et j'en passe. Par exemple, cela permet de faciliter le rafraichissement d'une mémoire DRAM : on peut rafraichir chaque sous-mémoire en parallèle, indépendamment des autres. Mais cette technique est principalement utilisée pour doubler le nombre d'adresses, doubler la taille d'un mot mémoire, ou faire les deux.

Mémoire multi-banques.

L'arrangement horizontal modifier

L'arrangement horizontal utilise plusieurs banques pour augmenter la taille d'un mot mémoire sans changer le nombre d'adresses. Chaque banc mémoire contient une partie du mot mémoire final. Avec cette organisation, on accède à tous les bancs en parallèle à chaque accès, avec la même adresse.

Arrangement horizontal.

Pour l'exemple, les barrettes de mémoires SDRAM ou DDR-RAM des PC actuels possèdent un mot mémoire de 64 bits, mais sont en réalité composées de 8 sous-mémoires ayant un mot mémoire de 8 bits. Cela permet de répartir la production de chaleur sur la barrette : la production de chaleur est répartie entre plusieurs puces, au lieu d'être concentrée dans la puce en cours d'accès. La technologie dual-channel est basée sur le même principe. Sauf qu'au lieu de rassembler plusieurs puces mémoires sur une même barrette, on fait la même chose avec plusieurs barrettes de mémoires. Ainsi, on peut connecter deux barrettes avec un mot mémoire de 64 bits et on les relie à un bus de 128 bits.

L'arrangement vertical modifier

L'arrangement vertical rassemble plusieurs boitiers de mémoires pour augmenter la capacité sans changer la taille d'un mot mémoire. On utilisera un boitier pour une partie de la mémoire, un autre boitier pour une autre, et ainsi de suite. Toutes les banques sont reliées au bus de données, qui a la même largeur que les sorties des banques. Une partie de l'adresse est utilisée pour choisir à quelle banque envoyer les bits restants de l'adresse. Les autres banques sont désactivées. Mais un arrangement vertical peut se mettre en œuvre de plusieurs manières différentes.

La première méthode consiste à connecter la bonne banque et déconnecter toutes les autres. Pour cela, on utilise la broche CS, qui connecte ou déconnecte la mémoire du bus. Cette broche est commandée par un décodeur, qui prend les bits de poids forts de l'adresse en entrée.

Comparaison entre arrangement horizontal (à gauche) et arrangement vertical (à droite).

Une autre solution est d'ajouter un multiplexeur/démultiplexeur en sortie des banques et de commander celui-ci convenablement avec les bits de poids forts. Le multiplexeur sert pur les lectures, le démultiplexeur pour les écritures.

Circuits d'une mémoire interleaved par rafale.

Les mémoires non-entrelacées modifier

Sans la technique dite de l'entrelacement, qu'on verra dans la section suivante, on utilise les bits de poids forts pour sélectionner la banque, ce qui fait que les adresses sont réparties comme illustré dans le schéma ci-dessous. Un défaut de cette organisation est que, si on souhaite lire/écrire deux mots mémoires consécutifs, on devra attendre que l'accès au premier mot soit fini avant de pouvoir accéder au suivant (vu que ceux-ci sont dans la même banque).

Répartition des adresses sans entrelacement.

L'entrelacement basique modifier

Avec l'organisation précédente, les accès séquentiels se font dans la même banque, ce qui les rend assez lents. Mais il est possible d'accélérer les accès à des bytes consécutifs en rusant quelque peu. L'idée est que des accès consécutifs se fassent dans des banques différentes, et donc que des bytes consécutifs soient localisés dans des banques différentes. Les mémoires qui fonctionnent sur ce principe sont appelées des mémoires à entrelacement simple.

Répartition des adresses dans une mémoire interleaved.

Pour cela, il suffit de prendre une mémoire à arrangement vertical, avec un petit changement : il faut utiliser les bits de poids faible pour sélectionner la banque, et les bits de poids fort pour le Byte.

Adresse mémoire d'une mémoire entrelacée

En faisant cela, on peut accéder à un plusieurs bytes consécutifs assez rapidement. Cela rend les accès en rafale plus rapide. Pour cela, deux méthodes sont possibles.

  • La première méthode utilise un accès en parallèle aux banques, d'où son nom d'accès entrelacé parallèle. Sans entrelacement, on doit accéder à chaque banque l'une après l'autre, en lisant chaque byte l'un après l'autre. Avec l’entrelacement parallèle, on lit plusieurs bytes consécutifs en même temps, en accédant à toutes les banques en même temps, avant d'envoyer chaque byte l'un après l'autre sur le bus (ce qui demande juste de configurer le multiplexeur). Un tel accès est dit en rafale : on envoie une adresse, puis on récupère plusieurs bytes consécutifs à partir de cette adresse initiale.
  • Une autre méthode démarre un nouvel accès mémoire à chaque cycle d'horloge, pour lire des bytes consécutifs un par un, mais chaque accès se fera dans une banque différente. En faisant cela, on n’a pas à attendre que la première banque ait fini sa lecture/écriture avant de démarrer la lecture/écriture suivante. Il s'agit d'une forme de pipelining, qui fait que l'accès à des bytes consécutifs est rendu plus rapide.

Les mémoires à entrelacement par décalage modifier

Les mémoires à entrelacement simple ont un petit problème : sur une mémoire à N banques, des accès dont les adresses sont séparées par N mots mémoires vont tous tomber dans la même banque et seront donc impossibles à pipeliner. Pour résoudre ce problème, il faut répartir les mots mémoires dans la mémoire autrement. Dans les explications qui vont suivre, la variable N représente le nombre de banques, qui sont numérotées de 0 à N-1.

Pour obtenir cette organisation, on va découper notre mémoire en blocs de N adresses. On commence par organiser les N premières adresses comme une mémoire entrelacée simple : l'adresse 0 correspond à la banque 0, l'adresse 1 à la banque 1, etc. Pour le bloc suivant, nous allons décaler d'une adresse, et continuer à remplir le bloc comme avant. Une fois la fin du bloc atteinte, on finit de remplir le bloc en repartant du début du bloc. Et on poursuit l’assignation des adresses en décalant d'un cran en plus à chaque bloc. Ainsi, chaque bloc verra ses adresses décalées d'un cran en plus comparé au bloc précédent. Si jamais le décalage dépasse la fin d'un bloc, alors on reprend au début.

Mémoire entrelacée par décalage.

En faisant cela, on remarque que les banques situées à N adresses d'intervalle sont différentes. Dans l'exemple du dessus, nous avons ajouté un décalage de 1 à chaque nouveau bloc à remplir. Mais on aurait tout aussi bien pu prendre un décalage de 2, 3, etc. Dans tous les cas, on obtient un entrelacement par décalage. Ce décalage est appelé le pas d'entrelacement, noté P. Le calcul de l'adresse à envoyer à la banque, ainsi que la banque à sélectionner se fait en utilisant les formules suivantes :

  • adresse à envoyer à la banque = adresse totale / N ;
  • numéro de la banque = (adresse + décalage) modulo N, avec décalage = (adresse totale * P) mod N.

Avec cet entrelacement par décalage, on peut prouver que la bande passante maximale est atteinte si le nombre de banques est un nombre premier. Seulement, utiliser un nombre de banques premier peut créer des trous dans la mémoire, des mots mémoires inadressables. Pour éviter cela, il faut faire en sorte que N et la taille d'une banque soient premiers entre eux : ils ne doivent pas avoir de diviseur commun. Dans ce cas, les formules se simplifient :

  • adresse à envoyer à la banque = adresse totale / taille de la banque ;
  • numéro de la banque = adresse modulo N.

L'entrelacement pseudo-aléatoire modifier

Une dernière méthode de répartition consiste à répartir les adresses dans les banques de manière pseudo-aléatoire. La première solution consiste à permuter des bits entre ces champs : des bits qui étaient dans le champ de sélection de ligne vont être placés dans le champ pour la colonne, et vice-versa. Pour ce faire, on peut utiliser des permutations : il suffit d'échanger des bits de place avant de couper l'adresse en deux morceaux : un pour la sélection de la banque, et un autre pour la sélection de l'adresse dans la banque. Cette permutation est fixe, et ne change pas suivant l'adresse. D'autres inversent les bits dans les champs : le dernier bit devient le premier, l'avant-dernier devient le second, etc. Autre solution : couper l'adresse en morceaux, faire un XOR bit à bit entre certains morceaux, et les remplacer par le résultat du XOR bit à bit. Il existe aussi d'autres techniques qui donnent le numéro de banque à partir d'un polynôme modulo N, appliqué sur l'adresse.

Les rangées modifier

Si on mélange l'arrangement vertical et l'arrangement horizontal, on obtient ce que l'on appelle une rangée. Sur ces mémoires, les adresses sont découpées en trois morceaux, un pour sélectionner la rangée, un autre la banque, puis la ligne et la colonne.

Les mémoires multiports modifier

Les mémoires multiports sont reliées non pas à un, mais à plusieurs bus. Chaque bus est connecté sur la mémoire sur ce qu'on appelle un port. Ces mémoires permettent de transférer plusieurs données à la fois, une par port. Le débit est sont donc supérieur à celui des mémoires mono-port. De plus, chaque port peut être relié à des composants différents, ce qui permet de partager une mémoire entre plusieurs composants. Comme autre exemple, certaines mémoires multiports ont un bus sur lequel on ne peut que lire une donnée, et un autre sur lequel on ne peut qu'écrire.

Mémoire multiport.

Le multiports idéal modifier

Une première solution consiste à créer une mémoire qui soit vraiment multiports. Avec une mémoire multiports, tout est dupliqué sauf les cellules mémoire. Pour rappel, dans une mémoire normale, chaque cellule mémoire est relié à bitline via un transistor, lui-même commandé par le décodeur. Chaque port a sa propre bitline dédiée, ce qui donne N bitlines pour une mémoire à N ports. Évidemment, cela demande d'ajouter des transistors de sélection, pour la connexion et la déconnexion. De plus, ces transistors sont dorénavant commandés par des décodeurs différents : un par port. Et on a autant de duplications que l'on a de ports : N ports signifie tout multiplier par N. Autant dire que ce n'est pas l'idéal en termes de consommation énergétique !

Cette solution pose toutefois un problème : que se passe-t-il lorsque des ports différents écrivent simultanément dans la même cellule mémoire ? Eh bien tout dépend de la mémoire : certaines donnent des résultats plus ou moins aléatoires et ne sont pas conçues pour gérer de tels accès, d'autres mettent en attente un des ports lors de l'accès en écriture. Sur ces dernières, il faut évidemment rajouter des circuits pour détecter les accès concurrents et éviter que deux ports se marchent sur les pieds.

Le multiports à état partagé modifier

Certaines mémoires ont besoin d'avoir un très grand nombre de ports de lecture. Pour cela, on peut utiliser une mémoire multiports à état dupliqué. Au lieu d'utiliser une seule mémoire de 20 ports de lecture, le mieux est d'utiliser 4 mémoires qui ont chacune 5 ports de lecture. Toutefois, ces quatre mémoires possèdent exactement le même contenu, chacune d'entre elles étant une copie des autres : toute donnée écrite dans une des mémoires l'est aussi dans les autres. Comme cela, on est certain qu'une donnée écrite lors d'un cycle pourra être lue au cycle suivant, quel que soit le port, et quelles que soient les conditions.

Mémoire multiport à état partagé.

Le multiports externe modifier

D'autres mémoires multiports sont fabriquées à partir d'une mémoire à un seul port, couplée à des circuits pour faire l'interface avec chaque port.

Mémoire multiport à multiportage externe.

Une première méthode pour concevoir ainsi une mémoire multiports est d'augmenter la fréquence de la mémoire mono-port sans toucher à celle du bus. À chaque cycle d'horloge interne, un port a accès au plan mémoire.

La seconde méthode est basée sur des stream buffers. Elle fonctionne bien avec des accès à des adresses consécutives. Dans ces conditions, on peut tricher en lisant ou en écrivant plusieurs blocs à la fois dans la mémoire interne mono-port : la mémoire interne a un port très large, capable de lire ou d'écrire une grande quantité de données d'un seul coup. Mais ces données ne pourront pas être envoyées sur les ports de lecture ou reçues via les ports d'écritures, nettement moins larges. Pour la lecture, il faut obligatoirement utiliser un circuit qui découpe les mots mémoires lus depuis la mémoire interne en données de la taille des ports de lecture, et qui envoie ces données une par une. Et c'est la même chose pour les ports d'écriture, si ce n'est que les données doivent être fusionnées pour obtenir un mot mémoire complet de la RAM interne.

Pour cela, chaque port se voit attribuer une mémoire qui met en attente les données lues ou à écrire dans la mémoire interne : le stream buffer. Si le transfert de données entre RAM interne et stream buffer ne prend qu'un seul cycle, ce n'est pas le cas pour les échanges entre ports de lecture et écriture et stream buffer : si le mot mémoire de la RAM interne est n fois plus gros que la largeur d'un port de lecture/écriture, il faudra envoyer le mot mémoire en n fois, ce qui donne n^cycles. Ainsi, pendant qu'un port accèdera à la mémoire interne, les autres ports seront occupés à lire le contenu de leurs stream buffers. Ces stream buffers sont gérés par des circuits annexes, pour éviter que deux stream buffers accèdent en même temps dans la mémoire interne.

Mémoire multiport streamée.

La troisième méthode remplace les stream buffers par des caches, et utilise une mémoire interne qui ne permet pas de lire ou d'écrire plusieurs mots mémoires d'un coup. Ainsi, un port pourra lire le contenu de la mémoire interne pendant que les autres ports seront occupés à lire ou écrire dans leurs caches.

Mémoire à multiports caché.

La méthode précédente peut être améliorée, en utilisant non pas une seule mémoire monoport en interne, mais plusieurs banques monoports. Dans ce cas, il n'y a pas besoin d'utiliser de mémoires caches ou de stream buffers : chaque port peut accéder à une banque tant que les autres ports n'y touchent pas. Évidemment, si deux ports veulent lire ou écrire dans la même banque, un choix devra être fait et un des deux ports devra être mis en attente.

Mémoire à multiports par banques.


Les mémoires primaires modifier

Illustration de la micro-architecture globale d'une mémoire ROM dite à diode (voir plus bas).

Dans les chapitres précédents, nous avons vu ce qu'il y a à l'intérieur des diverses mémoires. Nous avons abordé des généralités qui valent aussi bien pour des mémoires ROM, RAM, de masse, ou autres. Et il va de soi qu'après avoir vu les généralités, nous allons passer sur les spécificités de chaque type de mémoire. Nous allons d'abord étudier les mémoires ROM, pour une raison simple : l'intérieur de ces mémoires est très simple. Il faut dire qu'il s'agit de mémoires de faible capacité, dont les besoins en termes de performance sont souvent assez frustres, les seules ROM à haute performance étant les mémoires Flash. En conséquence, elles intègrent peu d'optimisations qui complexifient leur micro-architecture.

Rappelons qu'il existe plusieurs types de mémoires ROM :

  • les mémoires ROM sont fournies déjà programmées et ne peuvent pas être reprogrammées ;
  • les mémoires PROM sont fournies intégralement vierges, et on peut les programmer une seule fois ;
  • les mémoires RPROM sont reprogrammables, ce qui signifie qu'on peut les effacer pour les programmer plusieurs fois ;
    • les mémoires EPROM s'effacent avec des rayons UV et peuvent être reprogrammées plusieurs fois de suite ;
    • certaines EPROM peuvent être effacées par des moyens électriques : ce sont les mémoires EEPROM et les mémoires Flash.

Toutes ces mémoires ROM ont un contrôleur mémoire limité à son plus simple appareil : un simple décodeur pour gérer les adresses. Les circuits d'interface avec la mémoire sont aussi très simples et se limitent le plus souvent à un petit circuit combinatoire. Le plan mémoire est des plus classiques et ce chapitre ne l'abordera pas, pour ne pas répéter ce qui a été vu dans les chapitres précédents. Seuls les cellules mémoires se démarquent un petit peu, celles-ci étant assez spécifiques sur les mémoires ROM. Les cellules mémoires ROM varient grandement selon le type de mémoire, ce qui explique les différences entre types de ROM.

Les mémoires ROM modifier

Les mémoires ROM les plus simples sont de loin les Mask ROM, qui sont fournies avec leur contenu directement intégré dans la ROM lors de sa fabrication. Ces mémoires ROM sont accessibles uniquement en lecture, mais pas en écriture, sans compter qu'elles ne sont pas reprogrammables. Il est possible de les fabriquer avec plusieurs méthodes différentes, que nous n'allons pas toutes présenter. La plus simple est de loin de prendre une mémoire FROM et de la programmer avec les données voulues. Et c'est d'ailleurs ainsi que procèdent certains fabricants. Mais cette méthode n'est pas très intéressante : le constructeur ne produit alors pas vraiment une "vraie" Mask ROM. À la place, nous allons vous parler des autres méthodes, plus intéressantes à étudier et aussi plus économes en circuits.

Les Mask ROM sont fabriquées en combinant un décodeur avec un OU câblé modifier

Les ROM sont techniquement des circuits combinatoires : leur sortie (la donnée lue) ne dépend que de l'entrée (l'adresse). Et en conséquence, pour chaque mémoire ROM, il existe un circuit combinatoire équivalent. Prenons un circuit qui, pour chaque entrée , renvoie le résultat  : celui-ci est équivalent à une ROM dont le byte d'adresse contient la donnée . Et réciproquement : une telle ROM est équivalente au circuit précédent. En clair, on peut créer un circuit combinatoire quelconque en utilisant une ROM, ce qui est très utilisé dans certains circuits que nous verrons dans quelques chapitres. Cependant, cela ne signifie pas que chaque circuit combinatoire soit une mémoire ROM : une vraie ROM contient un plan mémoire, un décodeur, de même que les circuits d'interface avec le bus, des bitlines, et j'en passe.

On peut créer une simili-ROM avec un décodeur et des portes OU modifier

Il existe une sorte d'intermédiaire entre une ROM véritable et un circuit combinatoire optimisé. Rappelons que tout circuit combinatoire est composé de trois couches de portes logiques : une couche de portes NON, une autre de portes ET, et une dernière couche de portes OU. Les deux couches de portes NON et ET calculent des minterms, et la couche de porte OU effectue un OU entre ces minterms. On peut remplacer les deux premières couches par un décodeur, comme nous l'avions vu dans le chapitre sur les circuits de sélection. En effet, par définition, un décodeur est un circuit qui fournit tous les minterms que l'on peut obtenir à partir de l'entrée. Il reste à faire un OU entre les sorties adéquates du décodeur pour obtenir le circuit voulu.

Un tel circuit commence à ressembler à une mémoire, bien que ce soit encore imparfait. On trouve bien un décodeur, comme dans toute mémoire, mais le plan mémoire n'existe pas vraiment car les portes OU ne sont pas des cellules mémoires en elles-mêmes. Néanmoins, ce circuit sert de base aux véritables mémoires ROM, qui s'obtient à partir du circuit précédent.

Conception d'un circuit combinatoire quelconque à partir d'un décodeur.

Le OU câblé modifier

OU câblé.

Les mémoires ROM sont conçues en remplaçant les portes OU par un OU câblé (wired OR). Pour rappel, le OU câblé est une technique qui permet d'obtenir un équivalent d'une porte OU juste en reliant les entrées à un même fil. Mais elle demande que le décodeur utilise des sorties à /drain/collecteur ouvert, c'est-à-dire des sorties qui peuvent prendre deux états, dont un qui déconnecte la sortie. Et plus précisément, les sorties sortent un 1 ou sont déconnectées.

On peut alors faire un OU câblé en connectant les sorties sur lesquelles on souhaite faire un OU entre elles. Si toutes ces sorties sont à 0, alors tous les circuits sont déconnectés et la sortie est connectée à la masse à travers la résistance : elle est à 0. Mais si une seule entrée est à 1, alors le 1 d'entrée est recopié sur le fil, ce qui met la sortie à 1.

Certains décodeurs fabriqués avec des portes TTL sont dans ce cas, ce qui est rendu possible par le fait que certaines portes logiques TTL ont des sorties à collecteur ouvert de ce type. Mais avec les transistors CMOS, il faut ruser. L'idée est de transformer les sorties d'un décodeur CMOS normal, en sortie à collecteur ouvert adéquate, en ajoutant un petit circuit en avant d'une sortie normale. Il existe deux manières pour cela : la première utilise une diode, la seconde utilise un transistor. La première méthode donne les ROM à diode, alors que la seconde donne des ROM à transistors MOS.

Les ROM à diodes modifier

Les ROM à diodes sont des mémoires ROM fabriquées en combinant un décodeur avec des diodes pour obtenir un décodeur à sorties à collecteur ouvert. Le OU câblé ressemble donc à ceci, en tenant compte des diodes. Les entrées A et B sont les sorties normales du décodeur, ce ne sont pas des sorties à collecteur ouvert !

Ou câblé fabriqué avec des diodes.
Wired OR avec des diodes.

L'intérieur d'une mémoire à diode ressemble à ceci :

ROM à diodes.

Les ROM à transistors MOS modifier

Les ROM de type MOS fonctionnent comme les ROM à diodes, si ce n'est que les diodes sont remplacées par des transistors MOS. On peut utiliser aussi bien des transistors NMOS que PMOS, ce qui donne des circuits très différents. Avec des transistors PMOS, les diodes sont simplement remplacées par des transistors, et le reste du circuit ne change pas. Le transistor PMOS se ferme quand on met un 1 sur sa base, et s'ouvre si on lui envoie un 0. Il se comporte alors un peu comme une diode, d'où le fait que le remplacement se fait à l'identique.

Regardons ce qui se passe quand on veut lire dans une ROM à transistors PMOS. Les cellules mémoire qui contiennent un 1 sont représentées par un simple fil, les autres ont un transistor. Si la cellule mémoire n'est pas sélectionnée, le décodeur envoie un 0 sur la grille du transistor, qui reste fermé. Il se comporte comme un fil.

ROM MOS - sélection d'une cellule contenant un 1.

Si la cellule est sélectionnée, le décodeur envoie un 1 sur la grille du transistor, qui s'ouvre. La bitline est déconnectée de la tension d'alimentation, et est reliée à la masse à travers une résistance : la bitline est mise à 0.

ROM MOS - sélection d'une cellule contenant un 0.

Si un transistor NMOS qui est utilisé, le circuit est en quelque sorte inversé. Rappelons qu'un transistor NMOS se ferme quand on met un 0 sur sa base, et s'ouvre si on lui envoie un 1. En conséquence, les sorties du décodeur sont ici réellement à collecteur ouvert, à savoir qu'elles sont à 0 ou déconnectées. C'est l'inverse de ce qu'on a avec une diode. Les conséquences sont multiples. Déjà, la résistance de rappel est connectée à la tension d'alimentation et non à la masse. De plus, là où on aurait mis une diode dans une ROM à diode, on ne met pas de transistor MOS. Et inversement, on place un transistor MOS là où on ne met pas de diodes.

Les mémoires PROM modifier

Mémoire PROM fabriquée avec des transistors bipolaires.

L'intérieur d'une mémoire FROM est similaire à celui d'une mémoire ROM simple, sauf que les diodes/transistors (ou leur absence) sont remplacées par un autre dispositif. Précisément, chaque cellule mémoire est composé d'une sorte d'interrupteur qu'on ne peut configurer qu'une seule fois. Celui est localisé à l'intersection d'une bitline et d'un signal row line, et connecte ces deux fils. Lors de la programmation, ce connecteur est soit grillé (ce qui déconnecte les deux fils), soit laissé intact. Pour faire un parallèle avec une ROM à diode, ce connecteur fonctionne comme une diode quand il est laissé intact, mais comme l'absence de diode quand il est grillé.

Suivant la mémoire, ce connecteur peut être un transistor, ou un fusible. Dans le premier cas, chaque transistor fonctionne soit comme un interrupteur ouvert, soit comme un interrupteur fermé. Un 1 correspond à un transistor laissé intact, qui fonctionne comme un interrupteur ouvert. Par contre, un 0 correspond à un transistor grillé, qui se comporte comme un interrupteur fermé. Dans le cas avec les fusibles, chaque bit est stocké en utilisant un fusible : un 1 est codé par un fusible intact, et le zéro par un fusible grillé. Une fois le fusible claqué, on ne peut pas revenir en arrière : la mémoire est programmée définitivement.

Programmer une PROM consiste à faire claquer certains fusibles/transistors en les soumettant à une tension très élevée. Pour cela, le contrôleur mémoire balaye chaque ligne une par une, ce qui permet de programmer la ROM ligne par ligne, byte par byte. Lorsqu'une ligne est sélectionnée, on place une tension très importante sur les bitlines voulues. Les fusibles de la ligne connectés à ces bitlines sont alors grillés, ce qui les met à 0. Les autres bitlines sont soumises à une tension normale, ce qui est insuffisant pour griller les fusibles. En choisissant bien les bitlines en surtension pour chaque ligne, on arrive à programmer la mémoire FROM comme souhaité.

Les mémoires EPROM et EEPROM modifier

Les mémoires EPROM et EEPROM, y compris la mémoire Flash, sont fabriquées avec des transistors à grille flottante, que nous avons déjà abordés il y a quelques chapitres. Je vous renvoie au chapitre sur les cellules mémoire pour plus d'informations à ce sujet. La grille de ces transistors est connectée à la row line, ce qui permet de commander leur ouverture, le drain et la source sont connectés à une bitline.

Les mémoires EPROM modifier

EPROM de ST Microelectronics M27C160, capacité de 16 Mbits.

Avant de pouvoir (re-)programmer une mémoire EPROM ou EEPROM, il faut effacer son contenu. Sur les EPROM, l'effacement se fait en exposant les transistors à grille flottante à des ultraviolets. Divers phénomènes physiques vont alors décharger les transistors, mettant l'ensemble de la mémoire à 0. Pour pouvoir exposer le plan mémoire aux UV, toutes les mémoires EPROM ont une petite fenêtre transparente, qui expose le plan mémoire. Il suffit d'éclairer cette fenêtre aux UV pour effacer la mémoire. Pour éviter un effacement accidentel de la mémoire, cette fenêtre est d'ordinaire recouverte par un film plastique qui ne laisse pas passer les UV.

Les mémoires EEPROM et Flash modifier

Les mémoires Flash et les mémoires EEPROM se ressemblent beaucoup, suffisamment pour que la différence entre les deux est assez subtile. Pour simplifier, on peut dire que les EEPROM effectuent les effacements/écritures byte par byte, alors que les Flash les font par paquets de plusieurs bytes. Là où on peut effacer/programmer un byte individuel sur une EEPROM, ce n'est pas possible sur une mémoire Flash. Sur une mémoire Flash, on est obligé d'effacer/programmer un bloc entier de la mémoire, le bloc faisant plus de 512 bytes. C’est une simplification, qui cache le fait que la distinction entre EEPROM et Flash n'est pas très claire. Dans les faits, on considère que le terme EEPROM est à réserver aux mémoires dont les unités d'effacement/programmation sont petites (elles ne font que quelques bytes, pas plus), alors que les Flash ont des unités beaucoup plus larges.

Les différences entre EEPROM, Flash NOR et Flash NAND modifier

Dans ce cours, nous ferons la distinction entre EEPROM et Flash sur le critère suivant : l'effacement peut se faire byte par byte sur une EEPROM, alors qu'il se fait par blocs entiers sur une Flash. Quant à la reprogrammation, tout dépend du type de mémoire. Sur les EEPROM, elle a forcément lieu byte par byte, comme l'effacement. Mais sur les mémoires Flash, elle peut se faire soit byte par byte, soit par paquets de plusieurs centaines de bytes. Cela permet de distinguer deux sous-types de mémoires Flash : les mémoires Flash de type NOR et les Flash de type NAND. Nous verrons ci-dessous d'où proviennent ces termes, mais laissons cela de côté pour le moment. Sur les Flash de type NOR, on doit effacer la mémoire par blocs, mais on peut reprogrammer les bytes uns par uns, indépendamment les uns des autres. Par contre, sur les Flash de type NAND, effacement et reprogrammation se font par paquets de plusieurs centaines de bytes. Pire : les blocs pour l'effacement n'ont pas la même taille que pour la reprogrammation : environ 512 à 8192 octets pour la reprogrammation, plus de 64 kibioctets pour l'effacement. Par exemple, il est possible de lire un octet individuel, d'écrire par paquets de 512 octets et d'effacer des paquets de 4096 octets. Sur les Flash NAND, l'unité d'effacement s'appelle un bloc (comme pour les Flash NOR), alors que l'unité de reprogrammation s'appelle une page mémoire.

Différences entre EEPROM, Flash NAND et Flash NOR
Reprogrammation Byte par byte Reprogrammation par blocs entiers
Effacement Byte par byte EEPROM N'existe pas
Effacement par blocs entiers Flash de type NOR Flash de type NAND

Les avantages et inconvénients de chaque type d'EEPROM modifier

En termes d’avantages et d'inconvénients, les différents types de Flash sont assez distincts. Les Flash NOR ont meilleur un temps de lecture que les Flash NAND, alors que c'est l'inverse pour la reprogrammation et l'effacement. Pour faire simple, l'écriture est assez lente sur les Flash NOR. En termes de capacité mémoire, les Flash NAND ont l'avantage, ce qui les rend mieux adaptées pour du stockage de masse. Leur conception réduit de loin le nombre d'interconnexions internes, ce qui augmente fortement la densité de ces mémoires.

Les différents types de Flash/EEPROM sont utilisées dans des scénarios très différents. Les Flash NAND sont idéales pour des accès séquentiels, comme on en trouve dans des accès à des fichiers. Par contre, les EEPROM et les Flash de type NOR sont idéales pour des accès aléatoires. En conséquence, les Flash NAND sont idéales comme mémoire de masse, alors que les Flash NOR/EEPROM sont idéales pour stocker des programmes de petite taille, comme des Firmware ou des BIOS. C'est la raison pour laquelle les Flash NAND sont utilisées dans les disques de type SSD, alors que les autres sont utilisées comme de petites mémoires mortes.

La micro-architecture des mémoires FLASH simples modifier

Les mémoires Flash sont fabriquées avec des transistors à grille flottante, comme pour les mémoires EEPROM. Du point de vue de la micro-architecture, il n'y a pas de différence notable entre EEPROM et mémoire Flash. La seule exception tient dans le plan mémoire et notamment dans la manière dont les cellules mémoires sont reliées aux bitlines (les fils sur lesquels on connecte les cellules mémoires pour lire et écrire dedans). Mais la manière utilisée n'est pas la même entre les Flash NAND et les Flash NOR.

Le tout est illustré dans le schéma qui suit, dans lequel on voit que chaque cellule d'une Flash NOR est connectée à la bitline directement, alors que les Flash NAND placent les cellules en série. De ce fait, les Flash NAND ont beaucoup moins de fils et de connexions, ce qui dégage de la place. Pas étonnant que ces dernières aient une densité mémoire plus importante que pour les Flash NOR (on peut mettre plus de cellules mémoire par unité de surface). Cette différence n'a strictement rien à voir avec ce qui a été dit plus haut. Peu importe que chaque cellule soit connectée à la bitline ou que les transistors soient en série, on peut toujours lire et reprogrammer chaque cellule indépendamment des autres.

FLASH NOR.
FLASH NAND.


Avant l'invention des mémoires SDRAM et DDR, il exista un grand nombre de mémoires différentes, les plus connues étant les mémoires fast page mode et EDO-RAM. Ces mémoires n'étaient pas synchronisées avec le processeur via une horloge. Quand ces mémoires ont été créées, cela ne posait aucun problème : les accès mémoire étaient très rapides et le processeur était certain que la mémoire aurait déjà fini sa lecture ou écriture au cycle suivant.

Les mémoires asynchrones à RAS/CAS : FPM et EDO-RAM modifier

Les mémoires asynchrones les plus connues étaient les mémoires FPM et mémoires EDO. Il s'agissait de mémoires à adressage par coïncidence ou à tampon de ligne, avec une adresse découpée en deux : une adresse haute pour sélectionner la ligne, et une adresse basse qui sélectionne la colonne. L'adresse est envoyée en deux fois : la ligne, puis la colonne. Pour rappel, l'avantage de cette méthode est qu'elle permet de limiter le nombre de fils du bus d'adresse, ce qui très intéressant sur les mémoires de grande capacité. Les mémoires FPM et EDO-RAM étant utilisées comme mémoire principale d'un ordinateur, elles devaient avoir une grande capacité. Cependant, avoir un petit nombre de broches sur les barrettes de mémoire est clairement important, ce qui impose d'utiliser des stratagèmes. Envoyer l'adresse en deux fois répond parfaitement à ce problème : cela permet d'avoir des adresses larges et donc des mémoires de forte capacité, avec une performance acceptable et peu de fils sur le bus d'adresse.

Pour savoir si une donnée envoyée sur le bus d'adresse est une adresse de ligne ou de colonne, le bus de commande de ces mémoires contenait deux fils bien particuliers : les RAS et le CAS. Pour simplifier, le signal RAS permettait de sélectionner une ligne, et le signal CAS permettait de sélectionner une colonne.

Signaux RAS et CAS.

Si on a deux bits RAS et CAS, c'est parce que la mémoire prend en compte les signaux RAS et CAS quand ils passent de 1 à 0. C'est à ce moment là que la ligne ou colonne dont l'adresse est sur le bus sera sélectionnée. Tant que des signaux sont à zéro, la ligne ou colonne reste sélectionnée : on peut changer l'adresse sur le bus, cela ne désélectionnera pas la ligne ou la colonne et la valeur présente lors du front descendant est conservée.

L'intérieur d'une FPM.

Les mémoires FPM modifier

Les mémoires FPM (Fast Page Mode) possédaient une petite amélioration, qui rendait l'adressage plus simple. Avec elles, il n'y a pas besoin de préciser deux fois la ligne si celle-ci ne changeait pas lors de deux accès consécutifs : on pouvait garder la ligne sélectionnée durant plusieurs accès. Par contre, il faut quand même préciser les adresses de colonnes à chaque changement d'adresse. Il existe une petite différence entre les mémoire FPM proprement dit et les mémoires Fast-Page Mode. Sur les premières, le signal CAS est censé passer à 0 avant qu'on fournisse l'adresse de colonne. Avec les Fast-Page Mode, l'adresse de colonne pouvait être fournie avant que l'on configure le signal CAS. Cela faisait gagner un petit peu de temps, en réduisant quelque peu le temps d'accès total.

Sélection d'une ligne sur une mémoire FPM ou EDO.

Avec les mémoires en mode quartet, il est possible de lire quatre octets consécutifs sans avoir à préciser la ligne ou la colonne à chaque accès. On envoie l'adresse de ligne et l'adresse de colonne pour le premier accès, mais les accès suivants sont fait automatiquement. La seule contrainte est que l'on doit générer un front descendant sur le signal CAS pour passer à l'adresse suivante. Vous aurez noté la ressemblance avec le mode rafale vu il y a quelques chapitres, mais il y a une différence notable : le mode rafale vrai n'aurait pas besoin qu'on précise quand passer à l'adresse suivante avec le signal CAS.

Mode quartet.

Les mémoires FPM à colonne statique se passent même du signal CAS. Le changement de l'adresse de colonne est détecté automatiquement par la mémoire et suffit pour passer à la colonne suivante. Dans ces conditions, un délai supplémentaire a fait son apparition : le temps minimum entre deux sélections de deux colonnes différentes, appelé tCAS-to-CAS.

Accès en colonne statique.

Les mémoires EDO-RAM modifier

L'EDO-RAM a été inventée quelques années après la mémoire FPM. Elle a été déclinée en deux versions : la EDO simple, et la EDO en rafale.

L'EDO simple ajoutait une capacité de pipelining limitée aux mémoires FPM. On pouvait démarrer un nouvel accès alors que la donnée de l'accès précédent était encore présent sur le bus de données. L'implémentation n'est pas différente des mémoires FPM, si ce n'est qu'il y a un registre ajouté sur la sortie de donnée pour les lectures.

Les EDO en rafale effectuent les accès à 4 octets consécutifs automatiquement : il suffit d'adresser le premier octet à lire. Les 4 octets étaient envoyés sur le bus les uns après les autres, au rythme d'un par cycle d’horloge : ce genre d'accès mémoire s'appelle un accès en rafale.

Accès en rafale.

Implémenter cette technique nécessite d'ajouter un compteur, capable de faire passer d'une colonne à une autre quand on lui demande, et quelques circuits annexes pour commander le tout.

Modifications du contrôleur mémoire liées aux accès en rafale.

Le rafraichissement mémoire modifier

Les mémoires FPM et EDO doivent être rafraichies régulièrement. Selon les mémoires, le rafraichissement était géré par le processeur ou de manière totalement automatique. Dans le premier cas, le rafraichissement était déclenché par une commande, provenant du processeur ou du contrôleur mémoire. Celui-ci envoyait une commande précise, qui ordonnait de rafraichir une adresse, parfois une ligne complète. Par la suite, le rafraichissement mémoire est devenu totalement automatique, ni le processeur, ni le contrôleur mémoire ne devant s'en charger. Le rafraichissement est purement le fait des circuits de la mémoire RAM et devient une simple opération de maintenance interne, gérée par la RAM elle-même.

Le rafraichissement manuel modifier

Au début, le rafraichissement se faisait ligne par ligne. Le rafraichissement avait lieu quand le RAS passait à l'état haut, alors que le CAS restait à l'état bas. Le processeur, ou le contrôleur mémoire, sélectionnait la ligne à rafraichir en fournissant son adresse mémoire. D'où le nom de rafraichissement par adresse qui est donné à cette méthode de commande du rafraichissement mémoire.

Rafraichissement mémoire manuel.

Par la suite, certaines mémoires ont implémenté un compteur interne d'adresse, pour déterminer la prochaine adresse à rafraichir sans la préciser sur le bus d'adresse. Le rafraichissement était devenu automatique, dans une certaine mesure. Mais le déclenchement du rafraichissement se faisait par une commande externe, provenant du contrôleur mémoire ou du processeur. Cette commande faisait passer le CAS à 0 avant le RAS. Cette méthode de rafraichissement se nomme rafraichissement interne.

Rafraichissement sur CAS précoce.

On peut noter qu'il est possible de déclencher plusieurs rafraichissements à la suite en laissant le signal CAS dans le même état. Ce genre de choses pouvait avoir lieu après une lecture : on pouvait profiter du fait que le CAS soit mis à zéro par la lecture ou l'écriture pour ensuite effectuer des rafraichissements en touchant au signal RAS. Dans cette situation, la donnée lue était maintenue sur la sortie durant les différents rafraichissements.

Rafraichissements multiples sur CAS précoce.

Le rafraichissement automatique modifier

Rapidement, les constructeurs de mémoire se sont dits qu'il valait mieux gérer ce rafraichissement de façon automatique, sans faire intervenir le contrôleur mémoire intégré à la carte mère. Ce rafraichissement a alors été délégué au contrôleur mémoire intégré à la barrette de mémoire, et est maintenant géré par des circuits spécialisés. Ce circuit de rafraichissement automatique n'est rien d'autre qu'un compteur, qui contient un numéro de ligne (celle à rafraichir).

Rafraichissement mémoire automatique.


Les mémoires actuelles sont un plus complexes que les mémoires vues dans les chapitres précédents. Ce chapitre va vous expliquer dans les grandes lignes en quoi nos mémoires actuelles se démarquent des autres. On verra que les mémoires modernes ne sont que des améliorations des mémoires vues précédemment.

Les mémoires SDRAM modifier

Les mémoires asynchrones ont laissé la place aux mémoires SDRAM, qui sont synchronisées avec le bus par une horloge. L'utilisation d'une horloge a comme avantage des temps d'accès fixes : le processeur sait qu'un accès mémoire prendra un nombre déterminé de cycles d'horloge et peut faire ce qu'il veut dans son coin durant ce temps. Avec les mémoires asynchrones, le processeur ne pouvait pas prévoir quand la donnée serait disponible et ne faisait rien tant que la mémoire n'avait pas répondu : il exécutait ce qu'on appelle des wait states en attendant que la mémoire ait fini.

Le mode rafale modifier

Qui plus est, les SDRAM gèrent à la fois l'accès entrelacé et l'accès linéaire. Nous avions vu ces deux types d'accès dans le chapitre sur les mémoires évoluées, mais faisons un bref rappel. Le mode linéaire est le mode rafale normal : un compteur est incrémenté à chaque cycle et son contenu est additionné à l'adresse de départ. Le mode entrelacé utilise un ordre différent. Avec ce mode de rafale, le contrôleur mémoire effectue un XOR bit à bit entre un compteur (incrémenté à chaque accès) et l'adresse de départ pour calculer la prochaine adresse de la rafale.

Sur les SDRAM, les paramètres qui ont trait au mode rafale sont modifiables, programmables. Déjà, on peut configurer la mémoire pour effectuer au choix des accès sans rafale ou des accès en rafale. Ensuite, on peut décider s'il faut faire un accès en mode linéaire ou entrelacé. Il y a aussi la possibilité de configurer le nombre d'octets consécutifs à lire ou écrire en mode rafale. On peut ainsi accéder à 1, 2, 4, ou 8 octets en une seule fois, alors que les EDO ne permettaient que des accès à 4 octets consécutifs.

Les commandes SDRAM modifier

Le bus de commandes d'une SDRAM contient évidemment un signal d'horloge, pour cadencer la mémoire, mais pas que. En tout, 18 fils permettent d'envoyer des commandes à la mémoire, commandes qui vont effectuer une lecture, une écriture, ou autre chose dans le genre. Les commandes en question sont des demandes de lecture, d'écriture, de préchargement et autres. Elles sont codées par une valeur bien précise qui est envoyée sur les 18 fils du bus de commande. Ces commandes sont nommées READ, READA, WRITE, WRITEA, PRECHARGE, ACT, ...

Bit CS Bit RAS Bit CAS Bit WE Bits de sélection de banque (2 bits) Bit du bas d'adresse A10 Reste du bus d'adresse Nom de la commande : Description
1 X Absence de commandes.
0 1 1 1 X No Operation : Pas d'opération
0 1 1 0 X Burst Terminante : Arrêt d'un accès en rafale en cours.
0 1 0 1 Adresse de la banque 0 Adresse de la colonne READ : lire une donnée depuis la ligne active.
0 1 0 1 Adresse de la banque 1 Adresse de la colonne READA : lire une donnée depuis la ligne active, avec rafraichissement automatique de la ligne.
0 1 0 0 Adresse de la banque 0 Adresse de la colonne WRITE : écrire une donnée depuis la ligne active.
0 1 0 0 Adresse de la banque 1 Adresse de la colonne WRITEA : écrire une donnée depuis la ligne active, avec rafraichissement automatique de la ligne.
0 0 1 1 Adresse de la banque Adresse de la ligne ACT : charge une ligne dans le tampon de ligne.
0 0 1 0 Adresse de la banque 0 X PRECHARGE : précharge le tampon de ligne dans la banque voulue.
0 0 1 0 Adresse de la X 1 X PRECHARGE ALL : précharge le tampon de ligne dans toutes les banques.
0 0 0 1 X Auto refresh : Demande de rafraichissement, gérée par la SDRAM.
0 0 0 0 00 Nouveau contenu du registre de mode LOAD MODE REGISTER : configure le registre de mode.

Les commandes READ et WRITE ne peuvent se faire qu'une fois que la banque a été activée par une commande ACT. Une fois la banque activée par une commande ACT, il est possible d'envoyer plusieurs commandes READ ou WRITE successives. Ces lectures ou écritures accèderont à la même ligne, mais à des colonnes différentes. Le commandes ACT se font à partir de l'état de repos, l'état où toutes les banques sont préchargées. Par contre, les commandes MODE REGISTER SET et AUTO REFRESH ne peuvent se faire que si toutes les banques sont désactivées.

Le fonctionnement simplifié d'une SDRAM peut se résumer dans ce diagramme :

Fonctionnement simplifié d'une SDRAM.

Les délais mémoires modifier

Il faut un certain temps pour sélectionner une ligne ou une colonne, sans compter qu'une SDRAM doit gérer d'autres temps d'attente plus ou moins bien connus : ces temps d'attente sont appelés des délais mémoires. La façon de mesurer ces délais varie : sur les mémoires FPM et EDO, on les mesure en unités de temps (secondes, millisecondes, micro-secondes, etc.), tandis qu'on les mesure en cycles d'horloge sur les mémoires SDRAM.

Timing Description
tRAS Temps mis pour sélectionner une ligne.
tCAS Temps mis pour sélectionner une colonne.
tRP Temps mis pour réinitialiser le tampon de ligne et décharger la ligne.
tRCD Temps entre la fin de la sélection d'une ligne, et le moment où l'on peut commencer à sélectionner la colonne.
tWTR Temps entre une lecture et une écriture consécutives.
tCAS-to-CAS Temps minimum entre deux sélections de deux colonnes différentes.

Les délais/timings mémoire ne sont pas les mêmes suivant la barrette de mémoire que vous achetez. Certaines mémoires sont ainsi conçues pour avoir des timings assez bas et sont donc plus rapides, et surtout : beaucoup plus chères que les autres. Le gain en performances dépend beaucoup du processeur utilisé et est assez minime comparé au prix de ces barrettes. Les circuits de notre ordinateur chargés de communiquer avec la mémoire (ceux placés soit sur la carte mère, soit dans le processeur), doivent connaitre ces timings et ne pas se tromper : sans ça, l’ordinateur ne fonctionne pas.

Le registre de mode modifier

Les mémoires SDRAM permettent de configurer divers paramètres de la mémoire, comme la longueur du mode rafale. Le contrôleur mémoire interne de la SDRAM mémorise ces informations dans un registre de 10 bits, le registre de mode. Il contient un bit qui permet de préciser s'il faut effectuer des accès normaux ou des accès en rafale. Il mémorise aussi le nombre d'octets consécutifs à lire ou écrire. Voici à quoi correspondent les 10 bits de ce registre :

Signification des bits du registre de mode des SDRAM
Bit n°9 Type d'accès : en rafale ou normal
Bit n°8 et 7 Doivent valoir 00, sont réservés pour une utilisation ultérieur dans de futurs standards.
Bit n°6, 5, et 4 Latence CAS
Bit n°3 Type de rafale : linéaire ou entrelacée
Bit n°2, 3, et 0 Longueur de la rafale : indique le nombre d'octets à lire/écrire lors d'une rafale.

Les SDRAM standards modifier

Les mémoires SDRAM sont standardisées par un organisme international, le JEDEC, et ont été déclinées en versions de performances différentes. Les voici :

Nom standard Fréquence Bande passante
PC66 66 mhz 528 Mio/s
PC66 100 mhz 800 Mio/s
PC66 133 mhz 1064 Mio/s
PC66 150 mhz 1200 Mio/s

Les mémoires DDR modifier

Les mémoires SDRAM récentes sont des mémoires de type dual data rate, voire quad data rate (voir le chapitre précédent pour ceux qui ont oublié) : elles portent ainsi le nom de mémoires DDR. Plus précisément, le plan mémoire des DDR est deux fois plus large que le bus mémoire, même si les deux sont commandés par un même signal d'horloge : là où les transferts avec le plan mémoire ont lieu sur front montant, les transferts de données sur le bus ont lieu sur les fronts montants et descendants de l'horloge. Il y a donc deux transferts de données sur le bus pour chaque cycle d'horloge, ce qui permet de doubler le débit sans toucher à la fréquence. D'autres différences mineures existent entre les SDRAM et les mémoires DDR. Par exemple, la tension d'alimentation des mémoires DDR est plus faible que pour les SDRAM.

Les commandes des mémoires DDR modifier

Les commandes des mémoires DDR sont globalement les mêmes que celles des mémoires SDRAM, vues plus haut. Les modifications entre SDRAM, DDR1, DDR2, DDR3, DDR4, et DDR5 sont assez mineures. Les seules différences sont l'addition de bits pour la transmission des adresses, des bits en plus pour la sélection des banques, un registre de mode un peu plus grand (13 bits sur la DDR 2, au lieu de 10 sur les SDRAM). En clair, une simple augmentation quantitative.

Avant la DDR4, les modifications des commandes sont mineures. La DDR2 supprime la commande Burst Terminate, la DDR3 et la DDR4 utilisent le bit A12 pour préciser s'il faut faire une rafale complète, ou une rafale de moitié moins de données. Mais avec la DDR4, les choses changent, notamment au niveau de la commande ACT. Avec l'augmentation de la capacité des barrettes mémoires, la taille des adresses est devenue trop importante. Il a donc fallu rajouter des bits d'adresses. Mais pour éviter d'avoir à rajouter des broches sur des barrettes déjà bien fournies, les concepteurs du standard DDR4 ont décidé de ruser. Lors d'une commande ACT, les bits RAS, CAS et WE sont utilisés comme bits d'adresse, alors qu'ils ont leur signification normale pour les autres commandes. Pour éviter toute confusion, un nouveau bit ACT est ajouté pour indiquer la présence d'une commande ACT : il est à 1 pour une commande ACT, 0 pour les autres commandes.

Commandes d'une mémoire DDR4, seule la commande colorée change par rapport aux SDRAM
Bit CS Bit ACT Bit RAS Bit CAS Bit WE Bits de sélection de banque (2 bits) Bit du bas d'adresse A10 Reste du bus d'adresse Nom de la commande : Description
1 X Absence de commandes.
0 0 1 1 1 X No Operation : Pas d'opération
0 0 1 1 0 X Burst Terminante : Arrêt d'un accès en rafale en cours.
0 0 1 0 1 Adresse de la banque 0 Adresse de la colonne READ : lire une donnée depuis la ligne active.
0 0 1 0 1 Adresse de la banque 1 Adresse de la colonne READA : lire une donnée depuis la ligne active, avec rafraichissement automatique de la ligne.
0 0 1 0 0 Adresse de la banque 0 Adresse de la colonne WRITE : écrire une donnée depuis la ligne active.
0 0 1 0 0 Adresse de la banque 1 Adresse de la colonne WRITEA : écrire une donnée depuis la ligne active, avec rafraichissement automatique de la ligne.
0 1 Adresse de la ligne (bits de poids forts) Adresse de la banque Adresse de la ligne (bits de poids faible) ACT : charge une ligne dans le tampon de ligne.
0 0 0 1 0 Adresse de la banque 0 X PRECHARGE : précharge le tampon de ligne dans la banque voulue.
0 0 0 1 0 Adresse de la X 1 X PRECHARGE ALL : précharge le tampon de ligne' dans toutes les banques.
0 0 0 0 1 X Auto refresh : Demande de rafraichissement, gérée par la SDRAM.
0 0 0 0 0 00 Nouveau contenu du registre de mode LOAD MODE REGISTER : configure le registre de mode.

Les types de mémoires DDR modifier

Les mémoires DDR sont standardisées par un organisme international, le JEDEC, et ont été déclinées en versions DDR1, DDR2, DDR3, et DDR4. Il existe enfin d'autres types de mémoires DDR, non-standardisées par le JEDEC : les mémoires GDDR, pour graphics double data rate, utilisées presque exclusivement sur les cartes graphiques. Il en existe plusieurs types pendant que j'écris ce tutoriel : GDDR, GDDR2, GDDR3, GDDR4, et GDDR5. Mais attention, il y a des différences avec les DDR normales : les GDDR sont des mémoires multiports et elles ont une fréquence plus élevée que les DDR normales, avec des temps d'accès plus élevés (sauf pour le tCAS).

Il existe quatre types de mémoires DDR1 officialisés par le JEDEC.

Nom standard Nom des modules Fréquence du bus Débit Tension d'alimentation
DDR 200 PC-1600 100 Mhz 1,6 gibioctets seconde 2,5 Volts
DDR 266 PC-2100 133 Mhz 2,1 gibioctets seconde 2,5 Volts
DDR 333 PC-2700 166 Mhz 2,7 gibioctets seconde 2,5 Volts
DDR 400 PC-3200 200 Mhz 3,2 gibioctets seconde 2,6 Volts

Avec les mémoires DDR2, 5 types de mémoires sont officialisées par le JEDEC. Diverses améliorations ont été apportées sur les mémoires DDR2 : la tension d'alimentation est notamment passée de 2,5/2,6 Volts à 1,8 Volts.

Nom standard Nom des modules Fréquence du bus Débit
DDR2 400 PC2-3200 100 Mhz 3,2 gibioctets par seconde
DDR2 533 PC2-4200 133 Mhz 4,2 gibioctets par seconde
DDR2 667 PC2-5300 166 Mhz 5,3 gibioctets par seconde
DDR2 800 PC2-6400 200 Mhz 6,4 gibioctets par seconde
DDR2 1066 PC2-8500 266 Mhz 8,5 gibioctets par seconde

Avec les mémoires DDR3, 6 types de mémoires sont officialisées par le JEDEC. Diverses améliorations ont été apportées sur les mémoires DDR3 : la tension d'alimentation est notamment passée à 1,5 Volts.

Nom standard Nom des modules Fréquence du bus Débit
DDR3 800 PC3-6400 100 Mhz 6,4 gibioctets par seconde
DDR3 1066 PC3-8500 133 Mhz 8,5 gibioctets par seconde
DDR3 1333 PC3-10600 166 Mhz 10,6 gibioctets par seconde
DDR3 1600 PC3-12800 200 Mhz 12,8 gibioctets par seconde
DDR3 1866 PC3-14900 233 Mhz 14,9 gibioctets par seconde
DDR3 2133 PC3-17000 266 Mhz 17 gibioctets par seconde


Les mémoires ROM ou SRAM ont généralement une interface simple, à laquelle le processeur peut s'interfacer directement. Mais pour d'autres mémoires, notamment les DRAM, ce n'est pas le cas. C'est le cas sur les mémoires où les adresses sont multiplexées, sur les DRAM qui nécessitent un rafraichissement, et bien d'autres. Pour les mémoires multiplexées, connecter le processeur directement sur ces mémoires n'est pas possible : le bus d'adresse du processeur et celui de la mémoire ne collent pas. Pour le rafraichissement, on pourrait le déléguer au processeur, mais cela imposerait des contraintes assez fortes qui sont loin d'être idéales. Et il y a bien d'autres raisons qui font que le processeur ne peut pas s'interfacer facilement avec certaines mémoires. Imaginez par exemple, les mémoires à bus de donnée série, où les données sont communiquées bit par bit.

Bref, pour gérer ces problèmes intrinsèques aux mémoires DRAM et à quelques autres modèles, les mémoires ne sont pas connectées directement au processeur. À la place, on ajoute un composant entre le processeur et la mémoire : le contrôleur mémoire externe. Celui-ci est placé sur la carte mère ou dans le processeur, et ne doit pas être confondu avec le contrôleur mémoire intégré dans la mémoire. Ce chapitre va voir quels sont les rôles du contrôleur mémoire, son interface et ce qu'il y a à l'intérieur.

Les rôles et l'interface du contrôleur mémoire modifier

L'interface du contrôleur mémoire, à savoir ses broches d'entrées/sorties et leur signification, est généralement très simple. Il se connecte au processeur et à la mémoire, ce qui fait qu'il a deux ports : un qui a la même interface mémoire que le processeur, un autre qui a la même interface que la mémoire. Cela trahit d'ailleurs son rôle principal, qui est de transformer les requêtes de lecture/écriture provenant du processeur en une suite de commandes acceptée par la mémoire. En effet, les requêtes du processeur ne sont pas forcément compatibles avec les entrées de la mémoire. Un accès mémoire typique venant du processeur contient juste une adresse à lire/écrire, le bit R/W qui indique s'il faut faire une lecture ou une écriture, et éventuellement une donnée à écrire. Mais, nous avons vu que les accès mémoires sur une DRAM sont multiplexés : on envoie l'adresse en deux fois : la ligne d'abord, puis la colonne. De plus, il faut générer les signaux RAS, CAS et bien d'autres. Le tout est illustré ci-dessous.

Contrôleur mémoire externe.

La traduction d'adresse modifier

Notons que cette fonction d’interfaçage implique beaucoup de choses, la première étant que les adresses du processeur sont traduites en adresses compatibles avec la mémoire. Sur les mémoires DRAM, cela signifie que l'adresse est découpée en une adresse de ligne et une adresse de colonne, envoyées l'une après l'autre. Mais ce n'est pas la seule opération de conversion possible. Il y a aussi le cas où le bus d'adresse et le bus de données sont fusionnés. Nous avions vu cela dans le chapitre sur l'interface des mémoires. Dans ce cas, on peut envoyer soit une adresse, soit lire/écrire une donnée sur le bus, mais on ne peut pas faire les deux en même temps. Un bit ALE indique si le bus est utilisé en tant que bus d'adresse ou bus de données. Le contrôleur mémoire gère cette situation, en fixant le bit ALE et en envoyant séparément adresse et donnée pour les écritures.

Une autre possibilité est la gestion de l'entrelacement, qui intervertit certains bits de l'adresse lors des accès mémoires. Rappelons qu'avec l'entrelacement, des adresses consécutives sont placées dans des mémoires séparées, ce qui demande de jouer avec les bits d'adresse, chose qui est dévolue à l'étape de traduction d'adresse du contrôleur mémoire.

Une autre possibilité est le cas où les adresses du processeur n'ont pas la même taille que les adresses du bus mémoire, le contrôleur peut se charger de tronquer les adresses mémoires pour les faire rentrer dans le bus d'adresse. Cela arrive quand la mémoire a des mots mémoires plus longs que le byte du processeur. Prenons l'exemple où le processeur gère des bytes de 1 octet, alors que la mémoire a des mots mémoires de 4 octets. Lors d'une lecture, le contrôleur mémoire va lire des blocs de 4 octets et récupérera l'octet demandé par le processeur. En conséquence, la lecture dans la mémoire utilise une adresse différente, plus courte que celle du processeur : il faut tronquer les bits de poids faible lors de la lecture, mais les utiliser lors de la sélection de l'octet.

Les séquencement des commandes mémoires modifier

Une demande de lecture/écriture faite par le processeur se fait en plusieurs étapes sur une mémoire SDRAM. Il faut d'abord précharger le tampon de ligne avec une commande PRECHARGE, puis envoyer une commande ACT qui fixe l'adresse de ligne, et enfin envoyer une commande READ/WRITE. Et encore, ce cas est simple : il y a des opérations mémoires qui sont beaucoup plus compliquées. Et outre l'ordre d'envoi des commandes pour chaque requête, il faut aussi tenir compte des timings mémoire, à savoir le fait que ces commandes doivent être séparées par des temps d'attentes bien précis. Par exemple, sur certaines mémoires, il faut attendre 2 cycles entre une commande ACT et une commande READ, il faut attendre 6 cycles avant deux commandes WRITE consécutives, etc. Chaque requête du processeur correspond donc à une séquence de commandes envoyées à des timings bien précis. Le contrôleur mémoire s'occupe de faire cette traduction des requêtes en commandes si besoin. Notons que cette traduction demande deux choses : traduire une requête processeur en une série de commandes à faire dans un ordre bien précis, et la gestion des timings. Les deux sont parfois effectués par des circuits séparés, comme nous le verrons plus bas.

Le rafraichissement mémoire modifier

N'oublions pas non plus la gestion du rafraichissement mémoire, qui est dévolue au contrôleur mémoire ! Il pourrait être réalisé par le processeur, mais ce ne serait pas pratique. Il faudrait que le processeur lui-même incorpore un compteur dédié pour le rafraichissement des lignes, et qu'il dispose de la circuiterie pour envoyer un signal de rafraichissement à intervalles réguliers. Et le processeur devrait régulièrement s'interrompre pour s'occuper du rafraichissement, ce qui perturberait l’exécution des programmes en cours d'une manière assez subtile, mais pas assez pour ne pas poser de problèmes. Pour éviter cela, on préfère déléguer le rafraichissement au contrôleur mémoire externe.

La traduction des signaux et l’horloge modifier

A cela, il faut ajouter l'interface électrique et la gestion de l'horloge.

Rappelons que la mémoire ne va pas à la même fréquence que le processeur et qu'il y a donc une adaptation à faire. Soit le contrôleur mémoire génère la fréquence qui commande la mémoire, soit il prend en entrée une fréquence de base qu'il multiplie pour obtenir la fréquence désirée. Les deux solutions sont presque équivalentes, si ce n'est que les circuits impliqués ne sont pas les mêmes. Dans le premier cas, le contrôleur doit embarquer un circuit oscillateur, qui génère la fréquence demandée. Dans l'autre cas, un simple multiplieur/diviseur de fréquence suffit et c'est généralement une PLL qui est utilisée pour cela. Il va de soi qu'un générateur de fréquence est beaucoup plus complexe qu'une simple PLL.

Un autre point est que la mémoire peut avoir une interface série, à savoir que les données sont transmises bit par bit. Dans ce cas, les mots mémoire sont transférés bit par bit à la mémoire ou vers le processeur. La traduction d'un mot mémoire de N bits en une transmission bit par bit est réalisée par cette interface électrique. Un simple registre à décalage suffit dans les cas les plus simples.

Enfin, n'oublions pas l’interfaçage électrique, qui traduit les signaux du processeur en signaux compatibles avec la mémoire. Il est en effet très fréquent que la mémoire et le processeur n'utilisent pas les mêmes tensions pour coder un bit, ce qui fait qu'elles ne sont pas compatibles. Dans ce cas, le contrôleur mémoire fait la conversion.

Les autres fonctions (résumé) modifier

Pour résumer, le contrôleur mémoire externe gère au minimum la traduction des accès mémoires en suite de commandes (ACT, sélection de ligne, etc.), le rafraîchissement mémoire, ainsi que l’interfaçage électrique. Mais il peut aussi incorporer diverses optimisations pour rendre la mémoire plus rapide. Par exemple, c'est lui qui s'occupe de l'entrelacement. Il gère aussi le séquencement des accès mémoires et peut parfois réorganiser les accès mémoires pour mieux utiliser les capacités de pipelining d'une mémoire synchrone, ou pour mieux utiliser les accès en rafale. Évidemment, cette réorganisation ne se voit pas du côté du processeur, car le contrôleur remet les accès dans l'ordre. Si les commandes mémoires sont envoyées dans un ordre différent de celui du processeur, le contrôleur mémoire fait en sorte que cela ne se voit pas. Notamment, il reçoit les données lues depuis la mémoire et les remet dans l'ordre de lecture demandé par le processeur. Mais nous reparlerons de ces capacités d'ordonnancement plus bas.

L'architecture du contrôleur modifier

Dans les grandes lignes, on peut découper le contrôleur mémoire externe en deux grands ensembles : un gestionnaire mémoire et une interface physique. Cette dernière s'occupe, comme dit haut, de la traduction des tensions entre processeur et mémoire, ainsi que de la génération de l'horloge. Si la mémoire est une mémoire série, elle contient un registre à décalage pour transformer un mot mémoire de N bits en signal série transmis bit par bit. Elle s'occupe aussi de la correction et de la détection d'erreur si la mémoire gère cette fonctionnalité. En clair, elle gère tout ce qui a trait à la transmission des bits, au niveau électronique voire électrique. Le séquenceur mémoire gère tout le reste. L'interface électrique est presque toujours présente, alors que le séquenceur peut parfois être réduit à peu de chagrin sur certaines mémoires. le gestionnaire mémoire est découpé en deux : un circuit qui s'occupe de la gestion des commandes mémoires proprement dit, et un circuit qui s'occupe des échanges de données avec le processeur. Ce dernier prévient le processeur quand une donnée lue est disponible et lui fournit la donnée avec, il prévient le processeur quand une écriture est terminée, etc.

Contrôleur mémoire, intérieur simplifié.

Le séquenceur mémoire modifier

Le rôle principal du contrôleur est la traduction des requêtes processeurs en une suite de commandes mémoire. Pour faire cette traduction, il y a plusieurs méthodes. Dans le cas le plus simple, le contrôleur mémoire contient un circuit séquentiel appelé une machine à état fini, aussi appelée séquenceur, qui s'occupe de cette traduction. Il s'occupe à la fois de la traduction des requêtes en suite de commande et des timings d'envoi des commandes à la mémoire. Mais cette organisation marche assez mal avec la gestion du rafraichissement. Aussi, il est parfois préférable de séparer la traduction des requêtes en suite de commandes, et la gestion des timings d'envoi de ces commandes à la mémoire. S'il y a séparation, le séquenceur est alors séparé en deux : un circuit de traduction et un circuit d’ordonnancement des commandes. Ce dernier reçoit les commandes du circuit de traduction, les mets en attente et les envoie à la mémoire quand les timings le permettent.

Notons que cela implique une fonction de mise en attente des commandes. Les raisons à cela sont multiples. Le cas le plus simple est celui des requêtes processeur qui correspondent à plusieurs commandes. Prenons l'exemple d'une requête de lecture se traduit en une série de deux commandes : ACT et READ. La commande ACT peut être envoyée directement à la mémoire si elle est libre, mais la commande READ doit être envoyée deux cycles plus tard. Cette dernière doit donc être mise en attente durant deux cycles. Et on pourrait aussi citer le cas où plusieurs requêtes processeur arrivent très vite, plus vite que la mémoire ne peut les traiter. Si des requêtes arrivent avant que la mémoire n'ait pu terminer la précédente, elles doivent être mises en attente.

Notons que pour les mémoires SDRAM et DDR, ce circuit décide s'il faut ajouter ou non des commandes PRECHARGE. Certains accès demandent des commandes PRECHARGE alors que d'autres peuvent s'en passer. C'est aussi lui qui détecte les accès en rafale et envoie les commandes adaptées. Notons que quand on accède à des données consécutives, on a juste à changer l'adresse de la colonne : pas besoin d'envoyer de commande ACT pour changer de ligne. C'est le séquenceur mémoire qui se charge de cela, et qui détecte les accès en rafale et/ou les accès à des données consécutives. Nous en parlerons plus en détail dans la suite du chapitre, dans la section sur la politique de gestion du tampon de ligne.

Le circuit de gestion du rafraichissement et le circuit d'arbitrage modifier

La gestion du rafraichissement est souvent séparée de la gestion des commandes de lecture/écriture, et est effectuée dans un circuit dédié, même si ce n'est pas une obligation. Si les deux sont séparés, le circuit de gestion des commandes et le circuit de rafraichissement sont secondés par un circuit d'arbitrage, qui décide qui a la priorité. Ainsi, cela permet que les commandes de rafraichissement et les commandes mémoires ne se marchent pas sur les pieds. Notamment quand une commande mémoire et une commande de rafraichissement sont envoyées en même temps, la commande de rafraichissement a la priorité.

Le circuit d'arbitrage est aussi utilisé quand la mémoire est connectée à plusieurs composants, plusieurs processeurs notamment. Dans ce cas, les commandes des deux processeurs ont tendance à se marcher sur les pieds et le circuit d'arbitrage doit répartir le bus mémoire entre les deux processeurs. Il doit gérer de manière la plus neutre possible les commandes des deux processeurs, de manière à empêcher qu'un processeur monopolise le bus pour lui tout seul.

L'architecture complète du contrôleur mémoire externe modifier

En clair, le contrôleur mémoire contient notamment un circuit de traduction des requêtes processeur en commandes mémoire, suivi par une FIFO pour mettre en attente les commandes mémoires, un module de rafraichissement, ainsi qu'un circuit d'arbitrage (qui décide quelle requête envoyer à la mémoire). Ajoutons à cela des FIFO pour mettre en attente les données lues ou à écrire, afin qu'elles soient synchronisées par les commandes mémoires correspondantes. Si une commande mémoire est mise en attente, alors les données qui vont avec le sont aussi. Ces FIFOS sont couplées à quelques circuits annexes, le tout formant le circuit d'échange de données avec le processeur, dans le gestionnaire mémoire, vu dans les schémas plus haut.

Module d'interface avec la mémoire.

Le contrôleur mémoire externe est schématiquement composé de quatre modules séparés.

  • Le module d'interface avec le processeur gère la traduction d’adresse ;
  • Le module de génération des commandes traduit les accès mémoires en une suite de commandes ACT, READ, PRECHARGE, etc.
  • Le module d’ordonnancement des commandes contrôle le rafraîchissement, l'arbitrage entre commandes/rafraichissement et les timings des commandes mémoires.
  • Le module d'interface physique se charge de la conversion de tension, de la génération d’horloge et de la détection et la correction des erreurs.

Si la mémoire (et donc le contrôleur) est partagée entre plusieurs processeurs, certains circuits sont dupliqués. Dans le pire des cas, tout ce qui précède le circuit d'arbitrage, circuit de rafraichissement mis à part, est dupliqué en autant d’exemplaires qu'il y a de processeurs.

La politique de gestion du tampon de ligne modifier

Le séquenceur mémoire décide quand envoyer les commandes PRECHARGE, qui pré-chargent les bitlines et vident le tampon de ligne. Il peut gérer cet envoi des commandes PRECHARGE de diverses manières nommées respectivement politique de la page fermée, politique de la page ouverte et politique hybride.

La politique de la page fermée modifier

Dans le premier cas, le contrôleur ferme systématiquement toute ligne ouverte par un accès mémoire : chaque accès est suivi d'une commande PRECHARGE. Cette méthode est adaptée à des accès aléatoires, mais est peu performante pour les accès à des adresses consécutives. On appelle cette méthode la close page autoprecharge, ou encore politique de la page fermée.

La politique de la page ouverte modifier

Avec des accès consécutifs, les données ont de fortes chances d'être placées sur la même ligne. Fermer celle-ci pour la réactiver au cycle suivant est évident contreproductif. Il vaut mieux garder la ligne active et ne la fermer que lors d'un accès à une autre ligne. Cette politique porte le nom d'open page autoprecharge, ou encore politique de la page ouverte.

Lors d'un accès, la commande à envoyer peut faire face à deux situations : soit la nouvelle requête accède à la ligne ouverte, soit elle accède à une autre ligne.

  • Dans le premier cas, on doit juste changer de colonne : c'est un succès de tampon de ligne. Le temps nécessaire pour accéder à la donnée est donc égal au temps nécessaire pour sélectionner une colonne avec une commande READ, WRITE, WRITA, READA. On observe donc un gain signifiant comparé à la politique de la page fermée dans ce cas précis.
  • Dans le second cas, c'est un défaut de tampon de ligne et il faut procéder comme avec la politique de la page fermée, à savoir vider le tampon de ligne avec une commande PRECHARGE, sélectionner la ligne avec une commande ACT, avant de sélectionner la colonne avec une commande READ, WRITE, WRITA, READA.

Détecter un succès/défaut de tampon de ligne n'est pas très compliqué. Le séquenceur mémoire a juste à se souvenir des lignes et banques actives avec une petite mémoire : la table des banques. Pour détecter un succès ou un défaut, le contrôleur doit simplement extraire la ligne de l'adresse à lire ou écrire, et vérifier si celle-ci est ouverte : c'est un succès si c'est le cas, un défaut sinon.

Les politiques dynamiques modifier

Pour gagner en performances et diminuer la consommation énergétique de la mémoire, il existe des techniques hybrides qui alternent entre la politique de la page fermée et la politique de la page ouverte en fonction des besoins.

La plus simple décide s'il faut maintenir ouverte la ligne en regardant les accès mémoire en attente dans le contrôleur. La politique de ce type la plus simple laisse la ligne ouverte si au moins un accès en attente y accède. Une autre politique laisse la ligne ouverte, sauf si un accès en attente accède à une ligne différente de la même banque. Avec l'algorithme FR-FCFS (First Ready, First-Come First-Service), les accès mémoires qui accèdent à une ligne ouverte sont exécutés en priorité, et les autres sont mis en attente. Dans le cas où aucun accès mémoire ne lit une ligne ouverte, le contrôleur prend l'accès le plus ancien, celui qui attend depuis le plus longtemps comparé aux autres.

Pour implémenter ces techniques, le contrôleur compare la ligne ouverte dans le tampon de ligne et les lignes des accès en attente. Ces techniques demandent de mémoriser la ligne ouverte, pour chaque banque, dans une mémoire RAM : la table des banques, aussi appelée bank status memory. Le contrôleur extrait les numéros de banque et de ligne des accès en attente pour adresser la table des banques, le résultat étant comparé avec le numéro de ligne de l'accès.

Architecture d'un module de gestion des commandes à politique dynamique.

Les politiques prédictives modifier

Les techniques prédictives prédisent s'il faut fermer ou laisser ouvertes les pages ouvertes.

La méthode la plus simple consiste à laisser ouverte chaque ligne durant un temps prédéterminé avant de la fermer.

Une autre solution consiste à effectuer ou non la pré-charge en fonction du type d'accès mémoire effectué par le dernier accès. On peut très bien décider de laisser la ligne ouverte si l'accès mémoire précédent était une rafale, et fermer sinon.

Une autre solution consiste à mémoriser les N derniers accès et en déduire s'il faut fermer ou non la prochaine ligne. On peut mémoriser si l'accès en question a causé la fermeture d'une ligne avec un bit. Mémoriser les N derniers accès demande d'utiliser un simple registre à décalage, un registre couplé à un décaleur par 1. Pour chaque valeur de ce registre, il faut prédire si le prochain accès demandera une ouverture ou une fermeture.

  • Une première solution consiste à faire la moyenne des bits à 1 dans ce registre : si plus de la moitié des bits est à 1, on laisse la ligne ouverte et on ferme sinon.
  • Pour améliorer l'algorithme, on peut faire en sorte que les bits des accès mémoires les plus récents aient plus de poids dans le calcul de la moyenne. Mais rien de transcendant.
  • Une autre technique consiste à détecter les cycles d'ouverture et de fermeture de page potentiels. Pour cela, le contrôleur mémoire associe un compteur pour chaque valeur du registre. En cas de fermeture du tampon de ligne, ce compteur est décrémenté, alors qu'une non-fermeture va l'incrémenter : suivant la valeur de ce compteur, on sait si l'accès a plus de chance de fermer une page ou non.

Le ré-ordonnancement des commandes mémoires modifier

Le contrôleur mémoire peut optimiser l'utilisation de la mémoire en changeant l'ordre des requêtes mémoires pour regrouper les accès à la même ligne/banque. Ce ré-ordonnancement marche très bien avec la politique à page ouverte ou les techniques assimilées. Elles ne servent à rien si le séquenceur utilise une politique de la page fermée. L'idée est de profiter du fait qu'une page est restée ouverte pour effectuer un maximum d'accès dans cette ligne avant de la fermer. Les commandes sont réorganisés de manière à regrouper les accès dans la même ligne, afin qu'ils soient consécutifs s'ils ne l'étaient pas avant ré-ordonnancement.

Pour réordonnancer les accès mémoire, le séquenceur vérifie si il y a des dépendances entre les accès mémoire. Les dépendances en question n'apparaissent que si les accès se font à une même adresse. Si tous les accès mémoire se font à des adresses différentes, il n'y a pas de dépendances et ont peut, en théorie, faire les accès dans n'importe quel ordre. Le tout est juste que le séquenceur remette les données lues dans l'ordre demandé par le processeur et communique ces données lues dans cet ordre au processeur. Les dépendances apparaissent quand des accès mémoire se font à une même adresse. le cas réellement bloquant, qui empêche toute ré-ordonnancement, est le cas où une lecture lit une donnée écrite par une écriture précédente. Dans ce cas, la lecture doit avoir lieu après l'écriture.

Une première solution consiste à regrouper plusieurs accès à des données successives en un seul accès en rafale. Le séquenceur analyse les commandes mises en attente et détecte si plusieurs commandes consécutives se font à des adresses consécutives. Si c'est le cas, il fusionne ces commandes en une lecture/écriture en rafale. Une telle optimisation est appelée la combinaison de lecture pour les lectures, et la combinaison d'écriture pour les écritures. Cette méthode d'optimisation ne fait que fusionner des lectures consécutives ou des écritures consécutives, mais ne fait pas de ré-ordonnancement proprement dit. Une variante améliorée combine cette fusion avec du ré-ordonnancement.

Une seconde solution consiste à effectuer les lectures en priorité, quitte à mettre en attente les écritures. Il suffit d'utiliser des files d'attente séparées pour les lectures et écritures. Si une lecture accède à une donnée pas encore écrite dans la mémoire (car mise en attente), la donnée est lue directement dans la file d'attente des écritures. Cela demande de comparer toute adresse à lire avec celles des écritures en attente : la file d'attente est donc une mémoire associative. Cette solution a pour avantage de faciliter l'implémentation de la technique précédente. Séparer les lectures et écritures facilite la fusion des lectures en mode rafale, idem pour les écritures.

Double file d'attente pour les lectures et écritures.

Une autre solution répartit les accès mémoire sur plusieurs banques en parallèle. Ainsi, on commence par servir la première requête de la banque 0, puis la première requête de la banque 1, et ainsi de suite. Cela demande de trier les requêtes de lecture ou d'écriture suivant la banque de destination, ce qui demande une file d'attente pour chaque banque.

Gestion parallèle des banques.


Barrette de mémoire RAM.

Dans nos ordinateurs, les mémoires prennent la forme de barrettes mémoires. Il s'agit de circuits imprimés auxquels on a ajouté des broches. Ces broches sont les trucs dorés situés en bas des barrettes de mémoire. Elles servent à connecter les circuits de la barrette de mémoire sur le bus.

Slots mémoires.

Celles-ci se fixent à la carte mère sur un connecteur standardisé, appelé slot mémoire.

Les formats de barrettes modifier

La classification des barrettes pour PC est assez compliquée, les différences entre barrettes étant nombreuses. Dans les grandes lignes, trois distinctions sont importantes.

  • La distinction la plus visible à l’œil nu est le fait que certaines barrettes ont des puces mémoire d'un seul côté alors que d'autres en ont sur les deux faces. Cela permet de distinguer les barrettes SIMM et DIMM.
  • Une autre distinction est le fait que les barrettes n'ont pas le même nombre de broches. Pour des raisons de compatibilité, les barrettes de mémoires ont un nombre de broches différent suivant la version de DDR utilisée. Le nombre de broches dépend du format utilisé pour la barrette de mémoire (il existe trois formats différents), ainsi que du type de mémoire. Certaines mémoires obsolètes (les mémoires FPM-RAM et EDO-RAM) se contentaient de 30 broches, tandis que la mémoire DDR2 utilise entre 204 et 244 broches.
  • Enfin, les barrettes n'ont pas la même taille, pas le même format. Par exemple, les barrettes de PC portable utilisent un format spécialisé incompatible avec le format des barrettes pour PC de bureau. L'existence de ce format provient des contraintes spécifiques aux PC portables : il n'y a pas beaucoup de place à l'intérieur d'un PC portable, ce qui demande de diminuer la taille des barrettes.

Barrettes SIMM (FPM et EDO) modifier

Les barrettes pour PC sont appelées des barrettes SIMM ou DIMM. Les barrettes SIMM ont des puces sur une seule face de la barrette. Les barrettes de mémoires FPM et EDO-RAM étaient des barrettes SIMM. Elles existaient en deux versions : une version 72 broches, et une version 30 broches. Pour information, la tension d'alimentation des mémoires FPM était de 5 volts. Pour les mémoires EDO, cela variait entre 5 et 3.3 volts.

SIMM recto.
SIMM verso.

SIMM 30 broches modifier

Barrette SIMM 30 broches, pour PC de bureau.

Pour les curieux, voici en détail à quoi servent les broches de la SIMM 30 broches. Si vous vous amusez à compter les nombre de bits pour le bus de donnée et pour le bus d'adresse, vous remarquerez que le bus d'adresse contient 12 bits et que le bus de données en fait 8. L'adresse étant envoyée en deux fois, cela fait des adresses de maximum 24 bits, soit 16 mébioctets.

Détail des broches Utilité Détail des broches Utilité
1 Tension d'alimentation 2 Signal CAS
3 Bit 0 du bus de donnée 4 Bit 0 du bus d'adresse
5 Bit 1 du bus d'adresse 6 Bit 1 du bus de données
7 Bit 2 du bus d'adresse 8 Bit 3 du bus d'adresse
9 Masse : zéro volt 10 Bit 2 du bus de données
11 Bit 4 du bus d'adresse 12 Bit 5 du bus d'adresse
13 Bit 3 du bus de données 14 Bit 6 du bus d'adresse
15 Bit 7 du bus d'adresse 16 Bit 4 du bus de données
17 Bit 8 du bus d'adresse 18 Bit 9 du bus d'adresse
19 Bit 10 du bus d'adresse 20 Bit 5 du bus de données
21 Bit R/W 22 Zéro volt : masse
23 Bit 6 du bus de données 24 Bit 11 du bus d'adresse
25 Bit 7 du bus de données 26 Bit de parité pour les données écrites
27 Signal RAS 28 Bit de parité du signal CAS
29 Bit de parité pour les données lues 30 Tension d'alimentation (en double)

SIMM 72 broches modifier

Les mémoires 72 broches contiennent plus de bits pour le bus de données : 32 pour être précis. Par contre le bus d'adresse ne change pas : il reste de 12 bits. D'autres bits pour ou moins importants ont été rajoutés : les bits RAS et CAS sont en plusieurs exemplaires et on trouve 4 fois plus de bits de parité (un par octet transférable sur le bus de données).

Barrettes DIMM (SDRAM et DDR) modifier

Barrette DIMM de type SDRAM, pour PC de bureau.
Barrette DIMM de type DDR, pour PC de bureau.

Contrairement aux barrettes SIMM, les barrettes DIMM ont des puces sur les deux côtés. Les barrettes de DDR ou SDR sont toutes des barrettes DIMM, les SIMM étant l'apanage des FPM et EDO-RAM. Le nombre de broches d'une barrette au format DIMM peut varier suivant le type de mémoire, entre 168 et 244. Je suppose que vous comprendrez le fait que je ne souhaite pas vraiment en faire la liste, comme je l'ai pour les mémoires FPM 30 broches, mais je vais quand même vous donner le nombre de broches par barrette en fonction du type de mémoire. Outre le nombre de broches, la position des encoches est différentes entre les barrettes de SDR, de DDR1, DDR2, etc.

Type de DDR Nombre de broches
SDRAM 168
DDR 184
DDR2 214, 240 ou 244, suivant la barrette ou la carte mère.
DDR3 204 ou 240, suivant la barrette ou la carte mère.

DDR-DIMM modifier

Les barrettes de mémoires DDR sont toutes des mémoires de type DIMM, tout comme les SDRAM. Il faut signaler que les barrettes de DDR ont une forme similaire, mais que la position de l'encoche est différente, pour des raisons de compatibilité.

Barrettes de DDR pour PC de bureau.

DDR SO-DIMM modifier

Les barrettes SO-DIMM, pour ordinateurs portables, sont différentes selon que la mémoire est une DDR1, une DDR2 ou une DDR3.

Barrettes de DDR pour PC portables.

RD-DIMM modifier

Il faut noter qu'outre les mémoires SDR et DDR, certaines mémoires alternatives conçues par Rambus ont utilisé le format DIMM. Cependant, la position des broches n'était pas la même que celle des formats DIMM normaux, sans compter que le connecteur Rambus n'était pas compatible avec les connecteurs SDR/DDR normaux. Cela fait que les barrettes de Rambus ont parfois étés appelées des mémoires RB-DIMM, mais ce sont en réalité des DIMM comme les autres, les différences étant assez spécifiques.

L'intérieur d'une barrette de mémoire modifier

Après avoir vu le format des barrettes mémoire, il est temps de voir ce qu'il y a l'intérieur. On peut voir à l’œil nu qu'elles sont composées de boîtiers noirs soudées sur un circuit imprimé. Ces boîtiers noirs, appelés des puces mémoires, contiennent une mémoire RAM. Chaque barrette combine ces puces de manière à additionner leurs capacités : on peut ainsi créer une mémoire de 8 gibioctets à partir de 8 puces d'un gibioctet, par exemple. Mais outre ces puces, les barrettes mémoires contiennent aussi des fils électriques qui connectent les puces mémoires aux bus, ainsi que d'autres circuits électroniques. Et ce sont eux que nous allons maintenant aborder.

Les bus internes à la barrette mémoire modifier

Comme dit précédemment, chaque puce est reliée aux bus de commande, d'adresse et de données. Toutes les puces sont connectées aux bus d'adresse et de commande, ce qui permet d'envoyer la même adresse/commande à toutes les puces en même temps. La manière dont ces puces sont reliées au bus de commande dépend selon la mémoire utilisée.

Les DDR1 et 2 utilisent ce qu'on appelle une topologie en T, illustrée ci-dessous. On voit que le bus de commande forme une sorte d'arbre, dont chaque extrémité est connectée à une puce. La topologie en T permet d'égaliser le délai de transmission des commandes à travers le bus : la commande transmise arrive en même temps sur toutes les puces. Mais elle a de nombreux défauts, à savoir qu'elle fonctionne mal à haute fréquence et qu'elle est aussi difficile à router parce que les nombreuses connexions posent problèmes.

Organisation des bus de commandes sur les DDR1-2, nommée topologie en T.

En comparaison, les DDR3 utilisent une topologie fly-by, où les puces sont connectées en série sur le bus de commande/adresse. La topologie fly-by n'a pas les problèmes de la topologie en T : elle est simple à router et fonctionne très bien à haute fréquence.

Organisation des bus de commandes sur les DDR3 - topologie fly-by

Le Serial Presence Detect modifier

Localisation du SPD sur une barrette de SDRAM.

Toute barrette de mémoire assez récente contient une petite mémoire ROM qui stocke les différentes informations sur la mémoire : délais mémoire, capacité, marque, etc. Cette mémoire s'appelle le Serial Presence Detect, aussi communément appelé le SPD. Ce SPD contient non seulement les timings de la mémoire RAM, mais aussi diverses informations, comme le numéro de série de la barrette, sa marque, et diverses informations. Cette mémoire ROM est lue au démarrage de l'ordinateur par le BIOS, afin de pourvoir configurer ce qu'il faut. Le contenu de ce fameux SPD est standardisé par un organisme nommé le JEDEC, qui s'est chargé de standardiser le contenu de cette mémoire, ainsi que les fréquences, timings, tensions et autres paramètres des mémoires SDRAM et DDR. Pour les curieux, vous pouvez lire la page wikipédia sur le SPD, qui donne son contenu pour les mémoires SDR et DDR : Serial Presence Detect.

La connexion au bus mémoire modifier

Dans le cas le plus fréquent, toutes les barrettes d'un PC sont reliées au même bus mémoire, comme indiqué dans le schéma ci-dessous. Cela pose quelques problèmes lors de l'utilisation d'un grand nombre de barrettes. Les puces mémoires sont reliées directement au bus mémoire, sans autres intermédiaires que des fils électriques. Mais cela limite le nombre de barrettes qui peuvent être placées sur la carte mère, pour diverses raisons techniques. Disons simplement que plus on connecte de barrettes sur un même bus, plus la qualité du signal électrique transmis est mauvaise. Mais diverses solutions ont été trouvées pour limiter ce désagrément, et il est intéressant de les connaître.

Bus mémoire

Les barrettes tamponnées (à registres) modifier

Pour résoudre le problème précédent, certaines barrettes intègrent un registre, qui fait l'interface entre le bus et la barrette de RAM. Le registre découple les barrettes du bus mémoire, ce dernier ne communiquant qu'avec un registre et non avec les puces mémoires. Le fait est que le registre ne perturbe pas le signal transmis sur le bus, ou du moins pas autant qu'en son absence. Ces barrettes ont un temps de latence est plus important que celui des barrettes normales, du fait de la latence du registre. Les barrettes de ce genre sont appelées des barrettes RIMM. Il en existe deux types :

  • Avec les barrettes RDIMM, le registre fait l'interface pour le bus d'adresse et le bus de commande, mais pas pour le bus de données.
  • Avec les barrettes LRDIMM (Load Reduced DIMMs), le registre fait tampon pour tous les bus, y compris le bus de données.
Organisation des bus de commandes sur les RDIMM.

Les mémoires FB-DIMM modifier

À l'opposé, les barrettes FB-DIMM ne sont pas connectées avec un bus. À la place, les barrettes FB-DIMM sont reliées entre elles par une sorte de guirlande, dont chaque point est une barrette. Les données/adresses circulent d'une barrette à l'autre, jusqu’à atteindre la barrette de destination. Chaque barrette est reliée au bus par une sorte de mémoire tampon couplée à des circuits de contrôle : l'Advanced Memory Buffer.

Bus mémoire pour les barrettes FB-DIMM, schéma détaillé.


Les mémoires associatives ont un fonctionnement totalement opposé aux mémoires adressables normales : au lieu d'envoyer l'adresse pour accéder à la donnée, on envoie la donnée pour récupérer son adresse. On les appelle aussi des mémoires adressables par contenu, ou encore Content-adressed Memory (CAM) en anglais. Dans ce qui suit, nous utiliserons parfois l'abréviation CAM pour désigner les mémoires associatives.

Les mémoires associatives semblent assez bizarres au premier abord. Quand on y réfléchit, à quoi cela peut-il bien servir de récupérer l'adresse d'une donnée ? La réponse est que les mémoires associatives ont été conçues pour répondre à une problématique assez connue des programmeurs : la recherche d'une donnée dans un ensemble. Généralement, les données manipulées par un programme sont regroupées dans des structures de données organisées : tableaux, listes, graphes, sets, tables de hachage, arbres, etc. Et il arrive fréquemment que l'on recherche une donnée bien précise dedans. Certaines structures de données sont conçues pour accélérer cette recherche, ce qui donne des gains de performance bienvenus, mais au prix d'une complexité de programmation non-négligeable. Les mémoires associatives sont une solution matérielle alternative bien plus rapide. Le processeur envoie la donnée recherchée à la mémoire associative, et celle-ci répond avec son adresse en quelques cycles d'horloge de la mémoire.

Les mémoires associatives dépassent rarement quelques mébioctets et nous n'avons pas encore de mémoire associative capable de mémoriser un gibioctet de mémoire. Leur faible capacité fait qu'elles sont utilisées dans certaines applications bien précises, où les structures de données sont petites. La contrainte de la taille des données fait que ces situations sont très rares. Un cas d'utilisation basique est celui des routeurs, des équipements réseaux qui servent d'intermédiaire de transmission. Chaque routeur contient une table CAM et une table de routage, qui sont souvent implémentées avec des mémoires associatives. Nous en reparlerons dans le chapitre sur le matériel réseau. Elles sont normalement utilisées en supplément d'une mémoire RAM principale. Il arrive plus rarement que la mémoire associative soit utilisée comme mémoire principale. Mais cette situation est assez rare car les mémoires associatives ont souvent une faible capacité.

L'interface des mémoires associatives modifier

Une mémoire associative prend en entrée une donnée et renvoie son adresse. Le bus de donnée est donc accessible en lecture/écriture, comme sur une mémoire normale. Par contre, son bus d'adresse est accessible en lecture et en écriture, c'est une entrée/sortie. L'usage du bus d'adresse comme entrée est similaire à celui d'une mémoire normale : il faut bien écrire des données dans la mémoire associative, ce qui demande de l'adresser comme une mémoire normale. Par contre, le fonctionnement normal de la mémoire associative, à savoir récupérer l'adresse d'une donnée, demande d'utiliser le bus d'adresse comme une sortie, de lire l'adresse dessus.

Principe de fonctionnement d'une mémoire associative

Lorsque l'on recherche une donnée dans une mémoire CAM, il se peut que la donnée demandée ne soit pas présente en mémoire. La mémoire CAM doit donc préciser si elle a trouvé la donnée recherchée avec un signal dédié. Un autre problème survient quand la donnée est présente en plusieurs exemplaires en mémoire : la mémoire renvoie alors le premier exemplaire rencontré (celui dont l'adresse est la plus petite). D'autres mémoires renvoient aussi les autres exemplaires, qui sont envoyées une par une au processeur.

Signal trouvé sur une mémoire associative

Notons que tout ce qui a été dit dans les deux paragraphes précédent ne vaut que pour le port de lecture de la mémoire. Les mémoires associatives ont généralement un autre port, qui est un port d'écriture, qui fonctionne comme pour une mémoire normale. Après tout, les donnée présentes dans la mémoire associatives viennent bien de quelque part. Elles sont écrites dans la mémoire associative par le processeur, en passant par ce port d'écriture. Sur ce port, on envoie l'adresse et la donnée à écrire, en même temps ou l'une après l'autre, comme pour une mémoire RAM. Le port d'écriture est géré comme pour une mémoire RAM : la mémoire associative contient un décodeur d'adresse, connecté aux cellules mémoires, et tout ce qu'il faut pour fabriquer une mémoire RAM normale, excepté que le port de lecture est retiré. Cela fait partie des raisons pour lesquelles les mémoires associatives sont plus chères et ont une capacité moindre : elles contiennent plus de circuits à capacité égale. On doit ajouter les circuits qui rendent la mémoire associative, en plus des circuits d'une RAM quasi-normale pour le port d'écriture.

La microarchitecture des mémoires associatives modifier

Si on omet tout ce qui a rapport au port d'écriture, l'intérieur d'une mémoire associative est organisée autour de deux fonctions : vérifier la présence d'une donnée dans chaque case mémoire (effectuer une comparaison, donc), et déterminer l'adresse d'une case mémoire sélectionnée.

La donnée à rechercher dans la mémoire est envoyée à toutes les cases mémoires simultanément et toutes les comparaisons sont effectuées en parallèle.

Plan mémoire d'une mémoire associative.

Une fois la case mémoire qui contient la donnée identifiée, il faut déduire son adresse. Si vous regardez bien, le problème qui est posé est l'exact inverse de celui qu'on trouve dans une mémoire adressable : on n'a pas l'adresse, mais les signaux sont là. La traduction va donc devoir se faire par un circuit assez semblable au décodeur : l'encodeur. Dans le cas où la donnée est présente en plusieurs exemplaires dans la mémoire, la solution la plus simple est d'en sélectionner un seul, généralement celui qui a l'adresse la plus petite (ou la plus grande). Pour cela, l'encodeur doit subir quelques modifications et devenir un encodeur à priorité.

Intérieur d'une mémoire associative.

Les cellules CAM des mémoires associatives modifier

Avec une mémoire associative normale, la comparaison réalisée par chaque case mémoire est une comparaison d'égalité stricte : on vérifie que la donnée en entrée correspond exactement à la donnée dans la case mémoire. Cela demande de comparer chaque bit d'entrée avec le bit correspondant mémorisé, avant de combiner les résultats de ces comparaisons. Pour faciliter la compréhension, nous allons considérer que la comparaison est intégrée directement dans la cellule mémoire. En clair, chaque cellule d'une CAM compare le bit d'entrée avec son contenu et fournit un résultat de 1 bit : il vaut 1 si les deux sont identiques, 0 sinon. L'interface d'une cellule mémoire associative contient donc deux entrées et une sortie : une sortie pour le résultat de la comparaison, une entrée pour le bit d'entrée, et une entrée write enable qui dit s'il faut faire la comparaison avec le bit d'entrée ou écrire le bit d'entrée dans la cellule. Une cellule mémoire de ce genre sera appelée une cellule CAM dans ce qui suit.

Interface d'une cellule mémoire associative.

Les cellules CAM avec une porte NXOR/XOR modifier

La comparaison de deux bits est un problème des plus simples. Rappelons que nous avions vu dans le chapitre sur les comparateurs qu'un comparateur qui vérifie l"égalité de deux bits n'est autre qu'une porte NXOR. Ce faisant, dans leur implémentation la plus naïve, chaque cellule CAM incorpore une porte NXOR pour comparer le bit d'entrée avec le bit mémorisé. Une cellule CAM ressemble donc à ceci :

Cellule mémoire associative naïve.

Pour limiter le nombre de portes logiques utilisées pour le comparateur, les circuits de comparaison sont fusionnés avec les cellules mémoires. Concrètement, la porte NXOR est fusionnée avec la cellule mémoire, en bidouillant le circuit directement au niveau des transistors. Et cela peut se faire de deux manières, la première donnant les cellules CAM de type NOR et la seconde les cellules mémoire de type NAND. Les cellules CAM de type NOR sont de loin les plus performantes mais consomment beaucoup d'énergie, alors que c'est l'inverse pour les cellules CAM de type NAND.

Précisons que cette distinction entre cellules mémoire de type NOR et NAND n'a rien à voir avec les mémoires FLASH de type NOR et NAND.
Comparateur séparé à la case mémoire
Comparateur intégré à la case mémoire

Les cellules CAM de type NAND modifier

Avec les cellules CAM de type NAND, le câblage est le suivant :

Cellule CAM de type NAND.

Voici comment elle fonctionne :

Cellule CAM de type NAND - fonctionnement

Les cellules CAM de type NOR modifier

Une cellule CAM de type NOR ressemble à ceci :

Cellule CAM de type NOR.

Pour comprendre son fonctionnement, il suffit de se rappeler que les transistors se ferment quand on place un 1 sur la grille. De plus, le signal trouvé est mis à 1 par un stratagème technique si les transistors du circuit ne le connectent pas à la masse. Ce qui donne ceci :

Fonctionnement d'une cellule CAM de type NOR.

Les performances des cellules de mémoire CAM modifier

Comme vous pouvez le constater plus haut, les cellules CAM contiennent beaucoup de transistors, surtout quand on les compare avec des cellules de SRAM, voire de DRAM. Une cellule de CAM NOR contient 4 transistors, plus une cellule de SRAM (la bascule D), ce qui fait 10 transistors au total. Une cellule de CAM NAND en contient 2, plus la bascule D/cellule de SRAM, ce qui fait 8 au total. Quelques optimisations diverses permettent d'économiser 1 ou 2 transistors, mais guère plus, ce qui donne entre 6 et 9 transistors dans le meilleur des cas.

Cellule CAM de type NOR.

En comparaison, une cellule de mémoire SRAM contient 6 transistors en tout, pour une cellule double port, transistor de sélection inclut. Cela ne fait que 1 à 3 transistors de plus, mais cela réduit la densité des mémoires CAM par rapport aux mémoires SRAM d'un facteur qui va de 10 à 50%. Les mémoires CAM ont donc une capacité généralement assez faible, plus faible que celle des SRAM qui n'est déjà pas terrible. Ne faisons même pas la comparaison avec les mémoires DRAM, qui elles ont une densité près de 10 fois meilleures dans le pire des cas. Autant dire que les mémoires CAM sont chères et que l'on ne peut pas en mettre beaucoup dans un ordinateur. C'est sans doute la raison pour laquelle leur utilisation est des plus réduites, limitée à quelques routeurs/switchs et quelques autres circuits assez rares.

La combinaison des résultats des comparaisons pour un mot mémoire modifier

Les cellules CAM comparent le bit stocké avec le bit d'entrée, ce qui donne un résultat de comparaison pour un bit. Les résultats des comparaisons sont alors combinés ensemble, pour donner un signal "trouve/non-trouvé" qui indique si le mot mémoire correspond au mot envoyé en entrée. Dans le cas le plus simple, on utilise une porte ET à plusieurs entrées pour combiner les résultats. Mais il s'agit là d'un circuit assez naïf et diverses techniques permettent de combiner les résultats comparaisons de 1 bit sans elle. Cela permet d'économiser des circuits, mais aussi de gagner en performances et/ou en consommation d'énergie. Il existe deux grandes méthodes pour cela.

Avec la première méthode, le signal en question est déterminé avec le circuit donné ci-dessous. Le transistor du dessus s'ouvre durant un moment afin de précharger le fil qui transportera le signal. Le signal sera relié au zéro volt si une seule cellule CAM a une sortie à zéro. Dans le cas contraire, le fil aura été préchargé avec une tension correspondant à un 1 sur la sortie et y restera durant tout le temps nécessaires : on aura un 1 en sortie.

Gestion de la précharge des cases mémoires associatives à base de NOR.

La seconde méthode relie les cellules CAM d'un mot mémoire ensemble comme indiqué dans le schéma ci-dessous. Chaque cellule mémoire a un résultat de comparaison inversé : il vaut 0 quand le bit de la cellule CAM est identique au bit d'entrée, et vaut 1 sinon. Si toutes les cellules CAM correspondent aux bits d'entrée, le signal final sera mis à la masse (à zéro). Et inversement, il suffit qu'une seule cellule CAM ouvre son transistor pour que le signal soit relié à la tension d'alimentation. En somme, le signal obtenu vaut zéro si jamais la donnée d'entrée et le mot mémoire sont identiques, et 1 sinon.

Gestion de la précharge des cases mémoires associatives à base de NAND.

En théorie, les deux méthodes peuvent s'utiliser indifféremment avec les cellules NOR ou NAND, mais elles ont des avantages et inconvénients similaires à celles des cellules NOR et NAND. La première méthode a de bonnes performances et une consommation d'énergie importante, comme les cellules NOR, ce qui fait qu'elle est utilisée avec elles. L'autre a une bonne consommation d'énergie mais de mauvaises performances, au même titre que les cellules NAND, ce qui fait qu'elle est utilisée avec elles. Il existe cependant des CAM hybrides, pour lesquelles on a des cellules NOR avec un agencement NAND, ou l'inverse.

Les mémoires associatives avec opérations de masquage intégrées modifier

Les mémoires associatives vues précédemment sont les plus simples possibles, dans le sens où elles ne font que vérifier l'égalité de la donnée d'entrée pour chaque case mémoire. Mais d'autres mémoires associatives sont plus complexes et permettent d'effectuer facilement des opérations de masquage facilement. Nous avons vu les opérations de masquage dans le chapitre sur les opérations bit à bit, mais un petit rappel ne fait pas de mal. Le masquage permet d'ignorer certains bits lors de la comparaison, de sélectionner seulement certains bits sur lesquels la comparaison sera effectuée. L'utilité du masquage sur les mémoires associatives n'est pas évidente, mais on peut cependant dire qu'il est très utilisé sur les routeurs, notamment pour leur table de routage.

Pour implémenter le masquage, il existe trois méthodes distinctes : celle des CAM avec un masque fournit en entrée, les CAM avec masquage intégré, et les CAM ternaires. Les deux méthodes se ressemblent beaucoup, mais il y a quelques différences qui permettent de séparer les deux types.

Les CAM avec masquage fournit en entrée modifier

Sur les CAM avec masquage fournit en entrée, les bits sélectionnés pour la comparaison sont précisés par un masque, un nombre de la même taille que les cases mémoires. Le masque est envoyé à la mémoire via un second bus, séparé du bus de données. Le masque est alors appliqué à toutes les cases mémoires. Notons que si le contenu d'une cellule n'est pas à prendre en compte, alors le résultat de l'application du masque doit être un 1 : sans cela, la porte ET à plusieurs entrées (ou son remplacement) ne donnerait pas le bon résultat. Pour implémenter ce comportement, il suffit de rajouter un circuit qui prend en entrée : le bit du masque et le résultat de la comparaison. On peut établir la table de vérité du circuit en question et appliquer les méthodes vues dans le chapitre sur les circuits combinatoires, mais le résultat sera dépend du format du masque. En effet, deux méthodes sont possibles pour gérer un masque.

  • Avec la première, les bits à ignorer sont indiqués par un 0 alors que les bits sélectionnés sont indiqués par des 1.
  • Avec la seconde méthode, c'est l'inverse : les bits à ignorer sont indiqués par des 1 et les autres par des 0.

La seconde méthode donne un circuit légèrement plus simple, où l'application du masque demande de faire un OU logique entre le bit du masque et la cellule mémoire associée. Il suffit donc de rajouter une porte OU à chaque cellule mémoire, entre la bascule D et la porte NXOR. Une autre manière de voir les choses est de rajouter un circuit de masquage à chaque case mémoire, qui s'intercale entre les cellules mémoires et le circuit qui combine les résultats de comparaison de 1 bit. Ce circuit est constitué d'une couche de portes OU, ou de tout autre circuit compatible avec le format du masque utilisé.

Cellule CAM avec masque

Les CAM avec masquage intégré modifier

Sur d'autres mémoires associatives plus complexes, chaque case mémoire mémorise son propre masque attitré. Il n'y a alors pas de masque envoyé à toutes les cases mémoires en même temps, mais un masque par case mémoire. L'interface de la mémoire CAM change alors quelque peu, car il faut pouvoir indiquer si l'on souhaite écrire soit une donnée, soit un masque dans une case mémoire. Cette méthode peut s'implémenter de deux manière équivalentes. La première est que chaque case mémoire contient en réalité deux registres, deux cases mémoires : un pour la case mémoire proprement dit, et un autre pour le masque. Ces deux registres sont connectés au circuit de masquage, puis au circuit qui combine les résultats de comparaison de 1 bit. Une autre manière équivalente demande d'utiliser deux bits de SRAM par cellules mémoires : un qui code la valeur 0 ou 1, et un pour le bit du masque associé. Le bit qui indique si la valeur stockée est X est prioritaire sur l'autre. En clair, chaque case mémoire stocke son propre masque.

Cellule CAM avec masque intégré à la cellule mémoire

Il est cependant possible d'optimiser le tout pour réduire les circuits de comparaison, en travaillant directement au niveau des transistors. On peut notamment s'inspirer des méthodes vues pour les cellules CAM normales, avec une organisation de type NAND ou NOR. Une telle cellule demande 2 bascules SRAM, ce qui fait 2 × 6 = 12 transistors. Il faut aussi ajouter les circuits de comparaison, ce qui rajoute 4 à 6 transistors. Un défaut de cette approche est qu'elle augmente la taille de la cellule mémoire, ce qui réduit donc la densité de la mémoire. La capacité de la mémoire CAM s'en ressent, car les cellules CAM sont très grosses et prennent beaucoup de transistors. Les cellules CAM de ce type les plus optimisées utilisent entre 16 et 20 transistors par cellules, ce qui est énorme comparé aux 6 transistors d'une cellule SRAM, déjà assez imposante. Autant dire que les mémoires de ce type sont des plus rares.

Cellule CAM d'une CAM avec masque intégré dans la cellule mémoire.

Les CAM ternaires modifier

Une alternative aux opérations de masquage proprement dit est d'utiliser des mémoires associatives ternaires. Pour bien les comprendre, il faut les comparer avec les mémoires associatives normales. Avec une mémoire associative normale, chaque cellule mémoire stocke un bit, qui vaut 0 et 1, mais ne peut pas prendre d'autres valeurs. Avec une mémoire associative ternaire, chaque cellule mémoire stocke non pas un bit, mais un trit, c’est-à-dire une valeur qui peut prendre trois valeurs : 0, 1 ou une troisième valeur nommée X. Sur les mémoires associatives ternaires, cette valeur est utilisée pour signaler que l'on se moque de la valeur réelle du trit, ce qui lui vaut le nom de valeur "don't care". Et c'est cette valeur X qui permet de faciliter l'implémentation des opérations de masquage. Lorsque l'on compare un bit avec cette valeur X, le résultat sera toujours 1 (vrai). Par exemple, si on compare la valeur 0101 0010 1010 avec la valeur 01XX XXX0 1010, le résultat sera vrai : les deux sont considérés comme identiques dans la comparaison. Pareil si on compare la valeur 0100 0000 1010 avec la valeur 01XX XXX0 1010 ou 0111 1110 1010 avec la valeur 01XX XXX0 1010.

Formellement, ces mémoires associatives ternaires ne sont pas différentes des mémoires associatives avec un masque intégré dans chaque case mémoire. La différence est au niveau de l'interface : l'interface d'une CAM ternaire est plus complexe et ne fait pas la distinction entre donnée stockée et masque. À l'opposé, les CAM avec masque intégré permettent de spécifier séparément le masque de la donnée pour chaque case mémoire. Mais au niveau du fonctionnement interne, les deux types de CAM se ressemblent beaucoup. D'ailleurs, les CAM ternaires sont souvent implémentées par une CAM avec masque intégré, avec un bit de masque pour chaque cellule mémoire, à laquelle on rajoute quelques circuits annexes pour l'interface. Mais il existe des mémoires associatives ternaires pour lesquelles le stockage du bit de donnée et du masque se font sans utiliser deux cellules mémoires. Elles sont plus rares, plus chères, mais elles existent.

Les mémoires associatives bit-sérielles modifier

Les circuits comparateurs des mémoires associatives sont gourmands et utilisent beaucoup de circuits. Les optimisations vues plus haut limitent quelque peu la casse, sans trop nuire aux performances, mais elles ne sont pas une panacée. Mais il existe une solution qui permet de drastiquement économiser sur les circuits de comparaison, qu détriment des performances. L'idée est que la comparaison entre donnée d'entrée et case mémoire se fasse un bit après l'autre, plutôt que tous les bits en même temps. Pour prendre un exemple, prenons une mémoire dont les cases mémoires font 16 bits. Les techniques précédentes comparaient les 16 bits de la case mémoire avec les 16 bits de l'entrée en même temps, en parallèle, avant de combiner les comparaisons pour obtenir un résultat. L'optimisation est de faire les 16 comparaisons bit à bit non pas en même temps, mais l'une après l'autre, et de combiner les résultats autrement.

Avec cette méthode, la comparaison bit à bit est effectuée par un comparateur d'égalité sériel, un circuit que nous avions vu dans le chapitre sur les comparateurs, qui vérifie l'égalité de deux nombres bits par bit. Rappelons que ce circuit ne fait pas que comparer deux bits : il mémorise le résultat des comparaisons précédentes et le combine avec la comparaison en cours. Il fait donc à la fois la comparaison bit à bit et la combinaison des résultats. Pour cela, il intègre une bascule qui mémorise le résultat de la comparaison des bits précédents. L'avantage est que les circuits de comparaison sont beaucoup plus simples. Le comparateur parallèle utilise beaucoup de transistors, alors qu'un comparateur sériel utilise au maximum une bascule et deux portes logiques, moins s'il est optimisé. Au final, l'économie en portes logiques et en consommation électrique est drastique et peut être utilisée pour augmenter la capacité de la mémoire associative. Par contre, le traitement bit par bit fait qu'on met plus de temps pour faire la comparaison, ce qui réduit les performances.

Un tel traitement des bits en série s'oppose à une comparaison des bits en parallèle des mémoires précédentes. Ce qui fait que les mémoires de ce type sont appelées des mémoires bit-sérielles. L'idée est simple, mais il reste à l'implémenter. Pour cela, il existe deux grandes solutions, la première utilisant des registres à décalage, l'autre avec un système qui balaye les colonnes de la mémoire une par une.

Les mémoires bit-sérielles basées sur des registres à décalage modifier

L'implémentation la plus simple à comprendre est celle qui utilise des registres à décalage, mais elle a le défaut d'utiliser beaucoup de circuits, comparé aux alternatives. Avec cette implémentation, chaque byte, chaque case mémoire, est stockée dans un registre à décalage, et il en est de même pour le mot d'entrée à comparer. Ainsi, à chaque cycle, les deux bits à comparer sortent de ces deux registres à décalage. La comparaison bit à bit est effectuée par un comparateur d'égalité sériel. Le bit qui sort de la case mémoire est envoyé au comparateur sériel, mais il est aussi réinjecté dans le registre à décalage, afin que la case mémoire ne perde pas son contenu. L'idée est que le contenu de la case mémoire fasse une boucle avant de revenir à son état initial : on applique une opération de rotation dessus.

Case mémoire d'un processeur associatif bit serial avec une bascule.

Pour donner un exemple, je vais prendre l'exemple d'un Byte qui contient la valeur 1100, et une donnée d'entrée qui vaut 0100. À chaque cycle d’horloge, le processeur associatif compare un bit de l'entrée avec le bit de même poids dans la case mémoire. La comparaison est le fait d'un circuit comparateur sériel (vu dans le chapitre sur les circuits comparateurs). Le résultat de la comparaison est disponible dans la bascule de 1 bit une fois la comparaison terminée. La bascule est reliée à la sortie sur laquelle on envoie le signal Trouvé / Non-trouvé.

Illustration d'une opération sur un processeur associatif sériel.

Les mémoires bit-sérielles basées sur un système de sélection des colonnes modifier

L’implémentation précédente a quelques défauts, liés à l'usage de registres à décalage. Le fait de devoir déplacer des bits d'une bascule à l'autre n'est pas anodin : cela n'est pas immédiat, sans compter que ça consomme du courant. Autant ce n'est pas du tout un problème d'utiliser un registre à décalage pour le mot d'entrée, autant en utiliser un par case mémoire est déjà plus problématique. Aussi, d'autres mémoires associatives ont corrigé ce problème avec un subterfuge assez intéressant, inspiré du fonctionnement des mémoires RAM.

Vous vous rappelez que les mémoires RAM les plus simples sont organisées en lignes et en colonnes : chaque ligne est une case mémoire, un bit se trouve à l'intersection d'une ligne et d'une colonne. Les mémoires CAM ne sont pas différentes de ce point de vue. L'idée est de n'activer qu'une seule colonne à la fois, du moins pour ce qui est des comparaisons. L'activation de la colonne connecte toutes les cellules CAM de la colonne au comparateur sériel, mais déconnecte les autres. En conséquence, les bits situés sur une même colonne sont envoyés au comparateur en même temps, alors que les autres sont ignorés. Un système de balayage des colonnes active chaque colonne l'une après l'autre, il passe d'une colonne à l'autre de manière cyclique. Ce faisant, le résultat est le même qu'avec l'implémentation avec des registres à décalage, mais sans avoir à déplacer les bits. La consommation d'énergie est donc réduite, sans compter que l'implémentation est légèrement plus rapide.

Les mémoires associatives word-sérielles modifier

les mémoires associatives word-sérielles sont des mémoires associatives qui sont conçues totalement différemment des précédentes. Elles sont conçues à partir d'une mémoire RAM, à laquelle on rajoute des circuits pour simuler une mémoire réellement associative. Là où les mémoires réellement associatives comparent la donnée d'entrée avec toutes les cases mémoires en parallèle, les mémoires associatives word-sérielles comparent avec chaque case mémoire une par une. Les circuits qui entourent la mémoire balayent la mémoire, en partant de l'adresse la plus basse, puis en passant d'une adresse à l'autre. Les circuits en question sont composés d'un comparateur, d'un compteur, et de quelques circuits annexes. Le compteur calcule les adresses à lire à chaque cycle d'horloge, la mémoire RAM est adressée par ce compteur, le comparateur récupère la donnée lue et effectue la comparaison. Dès qu'un match est trouvé, la mémoire associative renvoie le contenu du compteur, qui contient l'adresse du byte adéquat.

Mémoire associative word-sérielle

Les avantages et inconvénients des mémoires associatives word-sérielles modifier

Il est possible que la mémoire s'arrête au premier match trouvé, mais elle peut aussi continuer et balayer toute la mémoire afin de trouver toutes les cases mémoires qui correspondent à l'entrée, tout dépend de comment la logique de contrôle est conçue. L'avantage de balayer toute la mémoire est que l'on peut savoir si une donnée est présente en plusieurs exemplaires dans la mémoire, et localiser chaque exemplaire. À chaque cycle, la mémoire indique si elle a trouvé un match et quel est l'adresse associée. Le processeur a juste à récupérer les adresses transmises quand un match est trouvé, il récupérera les adresses de chaque exemplaire au fur et à mesure que la mémoire est balayée. En comparaison, les autres mémoires associatives utilisent un encodeur à priorité qui choisit un seule exemplaire de la donnée recherchée.

Un autre avantage de telles architectures est qu'elles utilisent peu de circuits, sans compter qu'elles sont simples à fabriquer. Elles sont économes en circuits essentiellement en raison du faible nombre de comparateurs utilisés. Elles n'utilisent qu'un seul comparateur non-sériel, là où les autres mémoires associatives utilisent un comparateur par case mémoire (comparateur parallèle ou sériel, là n'est pas le propos). Vu le grand nombre de cases mémoires que contient une mémoire réellement associative, l'avantage est aux mémoires associatives word-sérielles.

Par contre, cette économie de circuits se fait au détriment des performances. Le fait que chaque case mémoire soit comparée l’une après l'autre, en série, est clairement un désavantage. Le temps mis pour balayer la mémoire est très long et cette solution n'est praticable que pour des mémoires très petites, dont le temps de balayage est faible. De plus, balayer une mémoire est quelque chose que les ordinateurs normaux savent faire avec un morceau de code adapté. N'importe quel programmeur du dimanche peut coder une boucle qui traverse un tableau ou une autre structure de donnée pour y vérifier la présence d'un élément. Implémenter cette boucle en matériel n'a pas grand intérêt et le gain en performance est généralement mineur. Autant dire que les mémoires associatives word-sérielles sont très rares et n'ont pas vraiment d'utilité.

Les mémoires associatives sérielles par blocs modifier

Il est cependant possible de faire un compromis entre performance et économie de circuit/énergie avec les mémoires associatives word-sérielles. L'idée est d'utiliser plusieurs mémoires RAM internes, au lieu d'une seule, qui sont accédées en parallèle. Ainsi, au lieu de comparer chaque case mémoire une par une, on peut en comparer plusieurs à la fois : autant qu'il y a de mémoires internes. L'idée n'est pas sans rappeler l'organisation en banques et rangées des mémoires RAM modernes. En faisant cela, la comparaison sera plus rapide, mais au prix d'une consommation d'énergie et de circuits plus importante. Les mémoires ainsi créées sont appelées des mémoires associatives sérielles par blocs.

Mémoire associative word-sérielle optimisée

Les mémoires associatives sont assez peu utilisées, surtout les mémoires associatives word-sérielles. Cependant, les mémoires caches leur ressemblent beaucoup et une bonne partie de ce que nous venons de voir sera très utile quand nous verrons les mémoires caches. Si les informations de ce chapitre ne pourrons pas être réutilisées à l'identique, il s'agit cependant d'une introduction particulièrement propédeutique. Vous verrez que les mémoires réellement associative ressemblent beaucoup aux mémoires caches de type totalement associative, que les mémoires associatives word-sérielles ressemblent beaucoup aux caches directement adressés, et que les mémoires associatives word-sérielles optimisées avec plusieurs banques ressemblent beaucoup aux mémoires caches à plusieurs voies. Mais laissons cela de côté pour le moment, les mémoires caches étant abordées dans un chapitre à la fin de ce cours, et passons au prochain chapitre.


Les mémoires FIFO et LIFO conservent les données triées dans l'ordre d'écriture (l'ordre d'arrivée). La différence est qu'une lecture dans une mémoire FIFO renvoie la donnée la plus ancienne, alors que pour une mémoire LIFO, elle renverra la donnée la plus récente, celle ajoutée en dernier dans la mémoire. Dans les deux cas, la lecture sera destructrice : la donnée lue est effacée.

On peut voir les mémoires FIFO comme des files d'attente, des mémoires qui permettent de mettre en attente des données tant qu'un composant n'est pas prêt. Seules deux opérations sont possibles sur de telles mémoires : mettre en attente une donnée (enqueue, en anglais) et lire la donnée la plus ancienne (dequeue, en anglais).

Fonctionnement d'une file (mémoire FIFO).

De même, on peut voir les mémoires LIFO comme des piles de données : toute écriture empilera une donnée au sommet de cette mémoire LIFO (on dit qu'on push la donnée), alors qu'une lecture enlèvera la donnée au sommet de la pile (on dit qu'on pop la donnée).

Fonctionnement d'une pile (mémoire LIFO).

L'interface d'une mémoire FIFO/LIFO modifier

Les mémoires FIFO/LIFO possèdent un bus de commande assez simple, sans bus d'adresse vu que ces mémoires ne sont pas adressables. Il contient les bits CS et OE, le signal d'horloge et quelques bits de commande annexes comme un bit R/W.

La mémoire FIFO/LIFO doit indiquer quand celle-ci est pleine, à savoir qu'elle ne peut plus accepter de nouvelle donnée en écriture. Elle doit aussi indiquer quand elle est vide, ce qui signifie qu'il n'y a pas possibilité de lire son contenu. Pour cela, la mémoire possède deux sorties nommées FULL et EMPTY. Ces deux bits indiquent respectivement si la mémoire est pleine ou vide, quand ils sont à 1.

Les mémoires FIFO/LIFO les plus simples n'ont qu'un seul port qui sert à la fois pour la lecture et l'écriture, mais elles sont cependant rares. Dans les faits, les mémoires FIFO sont presque toujours des mémoires double ports, avec un port pour la lecture et un autre pour les écritures. Du fait de la présence de deux ports dédiés, le bit R/W est souvent séparé en deux bits distincts : un bit Write Enable sur le port d'écriture, qui indique qu'une écriture est demandée, et un bit Read Enable sur le port de lecture qui indique qu'une lecture est demandée. La séparation du bit R/W en deux bits séparés pour chaque port s'expliquera dans les paragraphes suivants.

Les mémoires FIFO/LIFO sont souvent des mémoires synchrones, les implémentations asynchrones étant plus rares. Elles ont donc au moins une entrée pour le signal d'horloge. Point important, de nombreuses mémoires FIFO ont une fréquence pour la lecture qui est distincte de la fréquence pour l'écriture. En conséquence, elles reçoivent deux signaux d'horloge sur deux entrées séparées : un pour la lecture et un pour l'écriture. La présence de deux fréquences s'explique par le fait que les mémoires FIFO servent à interfacer deux composants qui ont des vitesses différentes, comme un disque dur et un processeur, un périphérique et un chipset de carte mère, etc. D'ailleurs, c'est ce qui explique que le bit R/W soit scindé en deux bits, un par port : les deux ports n'allant pas à la même vitesse, ils ne peuvent pas partager un même bit d'entrée sans que cela ne pose problème.

L'interface la plus classique pour une mémoire FIFO/LIFO est illustrée ci-dessous. On voit la présence d'un port d'écriture séparé du port de lecture, et la présence de deux signaux d'horloge distincts pour chaque port.

Interface des mémoires FIFO et LIFO

Les mémoires LIFO modifier

Les mémoires LIFO peuvent se concevoir en utilisant une mémoire RAM, couplée à un registre. Les données y sont écrites à des adresses successives : on commence par remplir la RAM à l'adresse 0, puis on poursuit adresse après adresse, ce qui garantit que la donnée la plus récente soit au sommet de la pile. Tout ce qu'il y a à faire est de mémoriser l'adresse de la donnée la plus récente, dans un registre appelé le pointeur de pile. Cette adresse donne directement la position de la donnée au sommet de la pile, celle à lire lors d'une lecture. Le pointeur de pile est incrémenté à chaque écriture, pour pointer sur l'adresse de la nouvelle donnée. De même, il est décrémenté à chaque lecture, vu que les lectures sont destructrices (elles effacent la donnée lue). La gestion des bits EMPTY et FULL est relativement simple : il suffit de comparer le pointeur de pile avec l'adresse minimale et maximale. Si le pointeur de pile et l'adresse maximale sont égaux, cela signifie que toutes les cases mémoires sont remplies : la mémoire est pleine. Quand le pointeur de pile pointe sur l'adresse minimale (0), la mémoire est vide.

Microarchitecture d'une mémoire LIFO

Il est aussi possible, bien que plus compliqué, de créer des LIFO à partir de registres. Pour cela, il suffit d'enchainer des registres les uns à la suite des autres. Les données peuvent passer d'un registre à son suivant, ou d'un registre aux précédents. Toutes les lectures ou écritures ont lieu dans le même registre, celui qui contient le sommet de la pile. Quand on écrit une donnée, celle-ci est placée dans ce registre de sommet de pile. Pour éviter de perdre des données, celles-ci sont déplacées de leur registre actuel au précédent. Toutes les données sont donc décalées d'un registres, avant l'écriture de la donnée au sommet de pile. Lors d'une lecture, le sommet de la pile est effacé. Pour cela, toutes les données sont avancées d'un registre, en passant du registre actuel au suivant. Les échanges de données entre registres sont gérés par divers circuits d’interfaçage, commandés par un gigantesque circuit combinatoire (le contrôleur mémoire).

Mémoire LIFO fabriquée à partir de registres

Les mémoires FIFO modifier

Les mémoires FIFO sont surtout utilisées pour mettre en attente des données ou commandes, tout en conservant leur ordre d'arrivée. Si on utilise une mémoire FIFO dans cet optique, elle prend le nom de mémoire tampon. L'utilisation principale des mémoires tampons est l’interfaçage de deux composants de vitesse différentes qui doivent communiquer entre eux. Le composant rapide émet des commandes à destination du composant lent, mais le fait à un rythme discontinu, par rafales entrecoupées de "moments de silence". Lors d’une rafale, le composant lent met en attente les commandes dans la mémoire FIFO et il les consomme progressivement lors des moments de silence.

On retrouve des mémoires tampons dans beaucoup de matériel électronique : dans les disques durs, des les lecteurs de CD/DVD, dans les processeurs, dans les cartes réseau, etc. Prenons par exemple le cas d'un disque dur : il reçoit régulièrement, de la part du processeur, des commandes de lecture écriture et les données associées. Mais le disque dur étant un périphérique assez lent, il doit mettre en attente les commandes/données réceptionnées avant de pouvoir les traiter. Et cette mise en attente doit conserver l'ordre d'arrivée des commandes, sans quoi on ne lirait pas les données demandés dans le bon ordre. Pour cela, les commandes sont stockées dans une mémoire FIFO et sont consommées au fur et à mesure. On trouve le même genre de logique dans les cartes réseau, qui reçoivent des paquets de données à un rythme discontinu, qu'elles doivent parfois mettre en attente tant que la carte réseau n'a pas terminé de gérer le paquet précédent.

Les FIFO de taille fixe modifier

Les mémoires FIFO les plus simples ont une taille fixe, à savoir qu'elles contiennent toujours le même nombre de données mises en attente. Elles peuvent se fabriquer en enchainant des registres.

Register based parallel SAM

Il est aussi possible de fabriquer une FIFO en utilisant plusieurs registres à décalages. Chaque registre à décalage contient un bit pour chaque byte mis en attente. Le circuit en question est décrit dans le schéma ci-dessous.

FIFO de m bytes de n bits fabriquées avec des registres à décalages

Les FIFO de taille variable modifier

La plupart des applications requièrent des FIFOs qui mémorisent un nombre variable de données. De telles FIFO de taille variable peuvent se fabriquer à partir d'une mémoire RAM en y ajoutant deux compteurs/registres et quelques circuits annexes. Les deux compteurs mémorisent l'adresse de la donnée la plus ancienne, ainsi que l'adresse de la plus récente, à savoir l'adresse à laquelle écrire la prochaine donnée à enqueue, et l'adresse de lecture pour la prochaine donnée à dequeue. Quand une donnée est retirée, l'adresse la plus récente est décrémentée, pour pointer sur la prochaine donnée. Quand une donnée est ajoutée, l'adresse la plus ancienne est incrémentée pour pointer sur la bonne donnée.

Mémoire FIFO construite avec une RAM.

Petit détail : quand on ajoute des instructions dans la mémoire, il se peut que l'on arrive au bout, à l'adresse maximale, même s'il reste de la place à cause des retraits de données. La prochaine entrée à être remplie sera celle numérotée 0, et on poursuivra ainsi de suite. Une telle situation est illustrée ci-dessous.

Débordement de FIFO.

La gestion des bits EMPTY et FULL se fait par comparaison des deux compteurs. S'ils sont égaux, c'est que la pile est soit vide, soit pleine. On peut faire la différence selon la dernière opération : la pile est vide si c'est une lecture et pleine si c'est une écriture. Une simple bascule suffit pour mémoriser le type de la dernière opération. Un simple circuit combinatoire contenant un comparateur permet alors de gérer les flags simplement.

FIFO pleine ou vide.

L'implémentation des deux compteurs modifier

L'implémentation d'une mémoire FIFO demande donc d'utiliser deux compteurs. Vous vous attendez à ce que ces deux compteurs soient des compteurs binaires normaux, pas quelque chose d'exotique. Mais dans les faits, il est parfaitement possible d'utiliser des compteurs en anneau, ou des registres à décalage à rétroaction linéaire, ou des compteurs modulo comme compteurs dans une FIFO.

Premièrement, rien n'impose que les compteurs comptent de 1 en 1. Dans les faits, la séquence comptée peut être arbitraire : tout ce qui compte est qu'elle est la même entre les deux compteurs, pour vérifier si la RAM est pleine ou vide. Et cela amène à une optimisation assez simple : on peut utiliser des registres à décalage à rétroaction linéaire (LFSR ), au lieu de compteurs binaires usuels. Le résultat fonctionnera de la même façon vu de l'extérieur : on aura une FIFO qui fonctionne normalement, sans problèmes particuliers. Certes, la mémoire RAM ne sera pas remplie en partant de l'adresse 0, puis en remplissant la mémoire en sautant d'une adresse à sa voisine. A la place, les données seront ajoutées dans un ordre différent, mais qui ne change globalement pas grand chose. Les raisons de faire ainsi sont que les compteurs à base de LFSR est qu'ils sont plus simples à concevoir, moins gourmands en circuits et plus rapides. Le seul défaut est que les LFSR ne peuvent en effet pas contenir la valeur 0, ce qui fait qu'une adresse est gâchée. Mais sur les FIFOs assez grosses, le gain en vitesse et l'économie de circuits liée au compteur vaut la peine de gâcher une adresse.

Deuxièmement, rien n'impose que le compteur contienne une valeur codée en binaire. L'encodage de la valeur en binaire est cependant nécessaire pour adresser la mémoire RAM, mais il suffit d'ajouter un circuit pour traduire le contenu du compteur en binaire usuel pour cela. Sur les FIFO avec un port de lecture et un port d'écriture cadencés à des fréquences différentes, on utilise deux compteurs qui encodent des entiers en code Gray. La raison à cela est que les deux compteurs sont respectivement dans le port de lecture et dans le port d'écriture, et sont donc cadencés à deux fréquences différentes. Et la différence de fréquence entre compteurs peut causer des soucis pour calculer les bits EMPTY et FULL. Pour le dire autrement, la FIFO contient deux domaines d'horloge qui doivent communiquer entre eux pour calculer les bits EMPTY et FULL. C'est un cas de clock domain crossing tel que vu dans le chapitre sur le signal d'horloge, et la solution est alors celle évoquée dans ce chapitre : utiliser le code Gray.

Lors de l'incrémentation d'un compteur, tous les bits ne sont pas modifiés en même temps : les bits de poids faible sont typiquement modifiés avant les autres. Évidemment, à la fin de l'incrémentation, on obtient le résultat final, correct. Mais pendant le temps de calcul, le compteur peut se retrouver dans un état transitoire, où certains bits ont été modifiés mais pas les autres. Or, le comparateur qui s'occupe de déterminer les bits EMPTY et FULL est cadencé de manière à être au moins aussi rapide que le plus rapide des deux compteurs. Le comparateur est donc plus rapide que le compteur le plus lent et peut "voir" cet état transitoire. Il se peut que le comparateur compare le contenu du compteur le plus rapide avec l'état transitoire de l'autre, ce qui donnera un résultat temporairement faux. L'usage de compteurs en code Gray permet d'éviter ce problème : vu que seul un bit est modifié lors d'une incrémentation/décrémentation, les états transitoires n'existent tout simplement pas.


Le processeur modifier

L'architecture externe modifier

Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boite noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des instructions du processeur. Pour simplifier, on peut classer les instructions en quatre grands types :

  • les échanges de données entre mémoires ;
  • les calculs et autres opérations arithmétiques ;
  • les instructions de comparaison ;
  • les instructions de branchement.

À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc.

Les instructions arithmétiques modifier

Tout ordinateur gère des instructions qui font des calculs arithmétiques simples. Elles dépendent de la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres signés, des nombres codés en complément à 1, des flottants simple précision, des flottants double précision, etc. Tout dépend du type de la donnée, à savoir est-ce un flottant, un entier codé en BCD, etc.

Dans le cas le plus simple, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. Sur d'autres machines assez anciennes, on stockait le type de la donnée dans la mémoire. Chaque nombre manipulé par le processeur incorporait un tag, une petite suite de bits qui permettait de préciser son type. Le processeur ne possédait pas d'instructions en plusieurs exemplaires pour faire la même chose, et utilisait le tag pour déduire comment faire ses calculs. Par exemple, ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Le traitement effectué par cette instruction dépendait du tag incorporé dans la donnée. Des processeurs de ce type s'appellent des architectures à tags, ou tagged architectures.

Les tous premiers ordinateurs pouvaient manipuler des données de taille arbitraire. Alors certes, ces processeurs n'utilisaient pas vraiment les encodages de nombres qu'on a vus au premier chapitre. À la place, ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. De nos jours, les ordinateurs utilisent des entiers de taille fixe. La taille des données à manipuler peut dépendre de l'instruction. Ainsi, un processeur peut avoir des instructions pour traiter des nombres entiers de 8 bits, et d'autres instructions pour traiter des nombres entiers de 32 bits, par exemple. On peut aussi citer le cas des flottants : il faut bien faire la différence entre flottants simple précision et double précision !

Les instructions entières modifier

Les instructions arithmétiques sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, éventuellement la division, parfois les opérations plus complexes comme la racine carrée. La division est une opération très complexe et particulièrement lente, bien plus qu'une addition ou une multiplication. Pour information, sur les processeurs actuels, la division est 20 à 80 fois plus lente qu'une addition/soustraction, et presque 7 à 26 fois plus lente qu'une multiplication. Mais on a de la chance : c'est aussi une opération assez rare.

La ou les instructions d'addition entière modifier

Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences.

La première différence est la gestion des débordements, qui est parfois gérée en ayant deux instructions d'addition : une pour les additions de nombres signés et une autre pour les nombres non-signés. L’additionneur utilisé pour ces deux instructions est le même, l'addition elle-même ne change pas, mais les débordements d'entiers ne sont pas gérés de la même manière. Comme on le verra plus tard, certains processeurs disposent d'un registre de débordement, de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Ce bit de débordement est mis à jour à chaque addition/multiplication/soustraction. Et les débordements d'entiers sont différents selon que l'addition est signée ou non, d'où l'existence des deux instructions dédiées. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard.

Une autre possibilité, présente sur les vieux processeurs, tient dans la gestion de la retenue. A l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le bit de retenue. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (ADD with Carry). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares.

Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue.

La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (Substract with Carry). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant :

Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, et les 8 bits de poids forts de ces opérandes, et et leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1.

  • Si la retenue de l'addition est de 1, on a , ce qui veut dire que la seconde soustraction doit calculer .
  • A l'inverse, si la retenue de l'addition est de 0, on a . Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer .

Récapitulons dans un tableau.

Retenue Opération à effectuer pour la seconde soustraction
0
1

On voit que ce qu'il faut ajouter à est égal à la retenue. L'opération SUBC fait donc le calcul suivant :

La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple.

Les instructions de multiplication et de division entière modifier

Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Heureusement, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce.

Par contre, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. En effet, la multiplication de deux opérandes de bits donne un résultat de , ce qui ne tient donc pas dans un registre. Pour résoudre ce problème, il existe plusieurs solutions :

  • Dans sa version la plus simple, l'instruction de multiplication se contente de garder les bits de poids faible, ce qui peut entrainer l'apparition de débordements d'entiers.
  • Une autre version mémorise le résultat dans deux registres : un pour les bits de poids faible et un autre pour les bits de poids fort.
  • D'autres processeurs disposent de deux instructions de multiplication : une instruction pour calculer les bits de poids fort du résultat et une autre pour les bits de poids faible.
  • Enfin, certains processeurs disposent d'instructions de multiplication configurables, qui permettent de choisir quels bits conserver : poids fort ou poids faible.

Les instructions sur les entiers BCD modifier

Enfin, il faut citer les instructions qui agissent sur des entiers codés en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Sauf que la plupart faisaient autrement. A la place, ils traitaient les nombres BCD comme des nombres binaires normaux. Concrètement, il n'y avait pas d'instruction d'addition ou de soustraction spécifique pour le BCD. Les programmeurs utilisaient une instruction d'addition ou de soustraction normale, celle utilisée sur les nombres binaires normaux. Le résultat de l'opération n'était évidemment pas le bon, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour, souvent appelée Decimal Adjust After Addition.

Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le format Packed, où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé Unpacked, ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD packed et unpacked.

Pour commencer, étudions le cas des nombres BCD unpacked, plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD unpacked. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD unpacked dépasse 9, la limite de débordement en BCD unpacked. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le half carry flag, indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD unpacked. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ASCII adjust after addition.

Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ASCII adjust after substraction.

Pour les nombres BCD packed, la procédure est globalement la même, sauf qu'il faut traiter les deux nibbles : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le nibble de poids fort. Le bit half carry flag n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le nibble de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux nibles au lieu d'un, et que le bit half-carry est ignoré au profit du bit de retenue normal'. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD packed, appelée Décimal adjust after addition sur les processeurs x86.

Les instructions flottantes modifier

Les processeurs peuvent aussi gérer les calculs sur des nombres flottants. IEEE 754 standardise aussi quelques instructions sur les flottants qui doivent impérativement être supportées : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont même plus loin et implémentent non seulement les instructions de la norme, mais aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres, sont supportées par certains processeurs. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments.

Le support matériel et logiciel des flottants modifier

Autrefois, à savoir il y a une quarantaine d'années, les processeurs n'étaient capables d'utiliser que des nombres entiers et aucune instruction machine ne pouvait manipuler de nombres flottants. On devait alors émuler les calculs flottants par une suite d'instructions machine effectuées sur des entiers. Cette émulation était effectuée par une bibliothèque logicielle, fournie par un programmeur, voire par le système d'exploitation. Quand le système d'exploitation fournissait de quoi émuler les flottants, les instructions flottantes étaient alors émulées par le biais d'exceptions matérielles (nous verrons ce concept dans quelques chapitres). Pour simplifier, le processeur interrompt temporairement l'exécution du programme en cours et exécute un sous-programme capable de traiter l'exception. Dans tous les cas, les calculs sur des nombres flottants étaient alors vraiment lents et la manière de stocker des flottants en mémoire dépendait de l'application ou du système d'exploitation.

Pour améliorer les performances, les concepteurs de processeurs ont conçus des processeurs spécialisés dans les calculs flottants. À une époque, un ordinateur de type PC pouvait contenir un processeur normal, secondé par un processeur annexe dédié aux flottants. le processeur secondaire dédié aux flottants était appelé le coprocesseur arithmétique, ou encore le coprocesseur. Ils étaient très chers et relativement peu utilisés. Seules certaines applications assez rares étaient capables d'en tirer profit : des logiciels de conception assistée par ordinateur, par exemple. Un emplacement dans la carte mère était réservé au coprocesseur.

Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants dans les jeux d'instruction du processeur principal, rendant le coprocesseur inutile. Les premiers processeurs de ce type n'incorporaient néanmoins pas le moindre circuit capable d'effectuer des opérations flottantes. Sur ces processeurs, chaque instruction machine capable de gérer des nombres flottants était convertie en interne (c'est-à-dire dans le processeur) en une suite d'instructions entières qui émulaient l'instruction voulue. Nous verrons dans quelques chapitres comment cette conversion était faite - pour ceux qui savent, ces instructions étaient microcodées. Nous allons juste dire que le processeur incorporait une petite mémoire ROM qui stockait, pour chaque instruction à émuler, une suite de calculs qui l'émulait.

De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Le choix du mode d'arrondi ou la gestion des exceptions sont implémentés directement dans le matériel et sont souvent configurables grâce à un registre du processeur. Suivant la valeur du registre, le processeur arrondira les résultats des calculs d'une certaine façon et pas d'une autre, ou réagira d'une certaine manière aux exceptions vues au-dessus.

Le support des formats de flottants modifier

Il faut néanmoins préciser que le support de la norme IEEE 754 par la FPU/le jeu d'instruction n'est pas une obligation : certains processeurs s'en moquent royalement. Dans certaines applications, les programmeurs ont souvent besoin d'avoir des calculs qui s'effectuent rapidement et se contentent très bien d'un résultat approché. Dans ces situations, on peut utiliser des formats de flottants différents de la norme IEEE 754 et les circuits de la FPU sont simplifiés pour être plus rapides. Par exemple, certains circuits ne gèrent pas les underflow, overflow, les NaN ou les infinis, voir utilisent des formats de flottants exotiques. Sur d'autres processeurs, le processeur peut être configuré de façon à soit ignorer ces exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Il suffit pour cela de modifier la valeur d'un registre de configuration intégré dans le processeur. Le choix du mode d'arrondi ou la gestion des exceptions flottantes sont implémentés directement dans le matériel et sont souvent configurables grâce à un registre du processeur : suivant la valeur mise dans celui-ci, le processeur arrondira les résultats des calculs d'une certaine façon et pas d'une autre, ou réagira d'une certaine manière aux exceptions vues au-dessus.

Sur certains processeurs, tous les formats de flottants IEEE754 ne sont pas supportés. Il arrive que certains ne supportent que les flottants simple précision, mais pas les double précision ou d'autres formats assez courants. Si un processeur ne supporte pas un format de flottant, il doit émuler son support, généralement par logiciel. Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. Un autre exemple est la gestion des flottants dénormalisés, qui n'est pas forcément supportée par les processeurs. Sur quelques architectures, les dénormaux sont émulés en logiciel. Mais même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente.

Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754.

Les instructions logiques modifier

À côté des instructions de calcul, on trouve des instructions logiques qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont :

  • La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1.
  • Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000.
  • Les instructions similaires avec le OU ou le XOR.

Mais d'autres instructions sont intéressantes à étudier.

Les instructions de décalage et de rotation modifier

Les instructions de décalage décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations :

Schéma Gestion des bits sortants Remplissage des vides laissés par le décalage
Rotation Rotations de bits. Réinjectés dans les vides laissés par le décalage Remplis par les bits sortants
Décalage logique Décalage logique. Les bits sortants sont oubliés Remplis par des zéros
Décalage arithmétique Décalage arithmétique. Remplis par :
  • le bit de signe pour les bits de poids fort ;
  • des zéros pour les bits de poids faible .

Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique.

La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits.

Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour Carry Flag).

Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (Left Rotate Through Carry) et RRCY (Right Rotate Through Carry). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite.

Les instructions de manipulation de bits modifier

D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits.

Une instruction très commune de ce type est l'instruction population count, qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la population count est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors.

Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités :

  • Count Trailing Zeros donne le nombre de zéros situés à droite du 1 de poids faible.
  • Count Leading Zeros donne le nombre de zéros situés à gauche du 1 de poids fort.
  • Count Trailing Ones donnent le nombre de 1 situés à gauche du 0 de poids fort.
  • Count Leading Ones donne le nombre de 1 situés à droite du 0 de poids faible.
Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.

Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort :

  • Find First Set, donne la position du 1 de poids faible.
  • Find highest set donne la position du 1 de poids fort.
  • Find First Zero donne la position du 0 de poids faible.
  • Find Highest Zero donne la position du 0 de poids fort.

Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée Bit manipulation instructions sets (BMI sets), qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions Count Trailing Zeros et Count Leading Zeros sont supportées.

Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc.

Les instructions d'extension de registres modifier

Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les instructions d'extension de nombres.

La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d'instruction de zero extension et instruction d'extension de signe.

Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles.

De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes.

Les instructions de permutation d'octets modifier

Les instructions de byte swap, aussi appelée instructions de permutation d'octets, échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place.

Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs.

Instruction de permutation d'octets

L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme.

Les instructions d'accès mémoire modifier

Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe.

Les instructions d'accès à la RAM : LOAD et STORE modifier

Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre.

  • L'instruction LOAD est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM.
  • L'instruction STORE fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM.
Instruction LOAD. Instruction STORE.

D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre.

Les instructions de transfert entre registres : MOV et XCHG modifier

L'instruction MOV copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés.

Instruction MOV.

Mais quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. En effet, on peut émuler une instruction MOV avec des instructions de calcul assez précises. Les instructions de calcul lisent deux registres d'opérandes, font un calcul et enregistrent le résultat dans un registre de destination. L'idée est de faire un calcul dont le résultat est égal au registre à copier, et d'enregistrer le résultat dans le registre de destination. Par exemple, on peut imaginer un OU bit à bit entre un registre et lui-même : le résultat est égal au contenu du registre source. Même chose avec un ET bit à bit entre un nombre et lui-même.

Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l'instruction XCHG sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres.

Les instructions d'accès mémoire complexes modifier

Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE. A la place, on trouve une instruction d'accès mémoire généraliste. Elle est notamment capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV, mais elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre. Le choix entre les opérations possibles dépend de l'ordre des opérandes et de leurs nature. L'instruction mémoire généraliste utilise deux opérandes, et leur ordre compte. Typiquement, la première opérande désigne la source et l'autre la destination. Par exemple, si les deux opérandes sont des registres, alors le premier registre est copié dans le second. Si la première opérande est un registre, et l'autre une adresse, alors c'est une écriture. Inversez l'adresse et le registre et c'est une lecture.

Pour les transferts entre deux adresses, il existe d'autres instructions mémoires spécialisées. Elles sont connues sous le nom d'instructions de chaines de caractère, bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Fait intéressant, ces instructions mémoires peuvent être répétée automatiquement plusieurs fois, en leur ajoutant un préfixe. Pour cela, le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que ce registre atteint 0, l'instruction n'est plus répété et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque éxecution et s'arrête quand il atteint zéro.

Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. Pour cela, on doit préciser l'adresse de la source et l'adresse de destination dans deux registres spécifiques. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier n adresses consécutives ! Les deux registres pour l'adresse source et destination sont modifiés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Par modifiés, je veux dire qu'ils peuvent être tous les deux soit incrémentés, soit décrémentés. Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes). La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation). Il peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0.

D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies.

D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps.

Les instructions de contrôle (branchements et tests) modifier

Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses structures de contrôle de ce type ont donc étés inventées.

Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer.

Nom de la structure de contrôle Description
SI...ALORS Exécute une suite d'instructions si une condition est respectée
SI...ALORS...SINON Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas.
Boucle WHILE...DO Répète une suite d'instructions tant qu'une condition est respectée.
Boucle DO...WHILE aussi appelée REPEAT UNTIL Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions.
Boucle FOR Répète un nombre fixé de fois une suite d'instructions.

Les conditions à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes :

  • A == B (est-ce que A est égal à B ?) ;
  • A != B (est-ce que A est différent de B ?) ;
  • A > B (est-ce que A est supérieur à B ?) ;
  • A < B (est-ce que A est inférieur à B ?) ;
  • A >= B (est-ce que A est supérieur ou égal à B ?) ;
  • A <= B (est-ce que A est inférieur ou égal à B ?).

Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le program counter, ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler instructions de branchement les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications.

L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de branchement conditionnel effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de branchement inconditionnelle ne teste pas de condition et ne fait qu'un saut dans le programme.

Les structures de contrôle modifier

Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie.

Codage d'un SI...ALORS en assembleur.

Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse).

Codage d'un SI...ALORS..SINON en assembleur.

Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme.

DO...WHILE.

Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas.

WHILE...DO.

Les branchements conditionnels et leur implémentation modifier

Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. Une troisième méthode permet de remplacer les instructions de branchements conditionnels par des branchements inconditionnels. Sur ces processeurs, les instructions de test permettent de zapper l'instruction suivante si la condition testée est fausse. De telles instructions sont appelées des skip instructions et elles effectuent le test d'une condition suivi d'un branchement conditionnel un peu spécial et très limité (skipper l'instruction suivante ou non). Cela permet de simuler un branchement conditionnel à partir d'un branchement inconditionnel.

Implémentations possibles des branchements conditionnels
Plus surprenant, sur quelques rares processeurs, le program counter est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le program counter, avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante !

Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Si on omet le cas des skip instructions, le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements.

Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples.

Les instructions de test proprement dit modifier

Les premières sont réellement des instructions de test, qui effectuent une comparaison et disent si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés.

Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double.

Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs registres à prédicats, des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre.

Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. PAr exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code.

Les instructions de test qui sont des soustractions déguisées modifier

Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent.

Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit null, indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un registre d'état. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe.

La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit null permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant.

Branchements et tests avec un registre d'état

La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits.

Instruction de branchement Bit du registre d'état testé Condition testée si on compare deux nombres A et B avec une instruction de test
JS (Jump if Sign) SF = 1 Le résultat est négatif
JNS (Jump if not Sign) SF = 0 Le résultat est positif
JO (Jump if Overflow) OF = 1 Le calcul arithmétique précédent a généré un débordement signé
JNO (Jump if Not Overflow) OF = 0 Le calcul arithmétique précédent n'a pas généré de débordement signé
JNE (Jump if Not equal) ZF = 1 Les deux nombres A et B sont égaux
JE (Jump if Equal) ZF = 0 Les deux nombres A et B sont différents
JB (Jump if below) CF = 1 A < B, avec A et B non-signés
JAE (Jump if Above or Equal) CF = 0 A >= B, avec A et B non-signés
(JBE) Jump if below or equal CF OU ZF = 1 A >= B si A et B sont non-signés
JA (Jump if above) CF ET ZF = 0 A > B si A et B sont non-signés
JL (Jump if less) SF != OF si A < B, si A et B sont signés
JGE (Jump if Greater or Equal) SF = OF si A >= B, si A et B sont signés
JLE (Jump if less or equal) (SF != OF) OU ZF = 1 si A <= B, si A et B sont signés
JGE (Jump if Greater) (SF = OF) ET (NOT ZF) = 1 si A > B, si A et B sont signés

Les instructions à prédicat modifier

Les instructions à prédicat sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En théorie, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc.

Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de prédication totale. Cela peut paraitre étranger, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre.

Sur les processeurs disposant d'instructions à prédicats, les instructions de test s'adaptent sur plusieurs points. L'un d'entre eux est que les instructions de test utilisent souvent des registres à prédicats. L'utilité principale des instructions à prédicats est d'éliminer les branchements, qui posent des problèmes sur les architectures modernes pour des raisons que nous verrons dans les derniers chapitres de ce cours. Toujours est-il que l'usage d'un registre d'état se marierait un petit peu mal avec cet objectif. Avec des registres à prédicats, on peut ajouter des instructions pour faire un ET, un OU ou un XOR entre registres à prédicats, comme dit plus haut. Cela permet de tester des combinaisons de conditions, voire des conditions complexes, sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats.

Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Les deux résultats sont l'inverse l'un de l'autre : si le premier vaut 1, l'autre vaut 0 (et réciproquement). Les deux résultats sont naturellement fournis dans deux registres à prédicats distincts. L'utilité de cette adaptation est que les instructions à prédicats servent à implémenter des structures de contrôle SI...ALORS simples, qui ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si elle ne l'est pas. pour le dire autrement, le premier morceau de code s’exécute quand la condition est remplie, l'autre quand la condition inverse est remplie. On comprend donc mieux l'utilité pour les isntructions de test de fournir deux résultats, l'un étant l'inverse de l'autre.

Résumé modifier

Pour résumer, les instructions les plus courantes sont les suivantes :

Instruction Utilité
Instructions arithmétiques Ces instructions font simplement des calculs sur des nombres. On peut citer par exemple :
  • L'addition ;
  • la multiplication ;
  • la division ;
  • le modulo ;
  • la soustraction ;
  • la racine carrée ;
  • le cosinus ;
  • et parfois d'autres.
Instructions logiques Elles travaillent sur des bits ou des groupes de bits. On peut citer :
  • Le ET logique.
  • Le OU logique.
  • Le OU exclusif (XOR).
  • Le NON , qui inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1.
  • Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus.
  • Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre.
Instructions de test et de contrôle (branchements) Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses.
Instructions d’accès mémoire Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage.

En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées).

Il existe aussi une grande quantité d'autres instructions, qui sont fournies par certains processeurs pour des besoins spécifiques. Par exemple, certains processeurs ont des instructions spécialement adaptées aux besoins des OS modernes. D'autres permettent de modifier la consommation en électricité de l'ordinateur (instructions de mise en veille du PC, par exemple). On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples...


Dans ce chapitre, nous allons étudier une fonctionnalité du processeur appelée la pile d'appel. Son rôle est de simuler une mémoire de type FIFO, mais à l'intérieur d'une mémoire RAM. Sans elle, certaines fonctionnalités de nos langages de programmation actuels n'existeraient pas ! Pour les connaisseurs, cela signifierait qu'on ne pourrait pas utiliser de fonctions réentrantes ou de fonctions récursives.

La pile d'appel modifier

La pile d'appel est une portion de mémoire qui simule une mémoire FIFO. On peut ajouter ou retirer une donnée de la pile d'appel, mais la donnée retirée est systématiquement la plus récente, la dernière qui fut ajoutée. La pile d'appel est souvent une partie de la mémoire RAM, mais c'est parfois une mémoire séparée. Certains processeurs sauvegardent une copie de la pile dans des registres cachés au lieu de tout mémoriser en RAM. Cette technique permet d'améliorer les performances, une partie de la pile étant mémorisée dans des registres rapides et non dans une mémoire RAM lente.

Les cadres de pile modifier

Les données sont organisées d'une certaine façon à l'intérieur. Les données sont regroupées dans la pile dans ce qu'on appelle des cadres de pile, des espèces de blocs de mémoire de taille fixe ou variable suivant le processeur.

Pile d'appel et cadres de pile.

Le contenu d'un cadre de pile varie fortement suivant le processeur. Certains processeurs se contentent d'y placer une adresse mémoire par cadre de pile. D'autres y placent plusieurs entiers ou adresses bien précises, avec une organisation fixe et immuable, ce qui donne des cadres de pile de taille fixe. Mais d'autres ont des cadres de pile de taille variable, ce qui permet aux logiciels d'y mettre ce qu'ils veulent. Les cadres de taille fixe sont surtout utilisées sur les processeurs anciens, alors que les piles d'appel programmables sont utilisées sur les processeurs modernes.

L'ordre des cadres de pile : une structure de type LIFO modifier

Les cadres sont créés un par uns et sont placés les uns à la suite des autres dans la mémoire. C'est une première contrainte : on ne peut pas créer de cadres n'importe où dans la mémoire. On peut comparer l'organisation des cadres à une pile d'assiette : on peut parfaitement rajouter une assiette au sommet de la pile d'assiette, ou enlever celle qui est au sommet, mais on ne peut pas toucher aux autres assiettes. Sur la pile de notre ordinateur, c'est la même chose : on ne peut accéder qu'à la donnée située au sommet de la pile. Comme pour une pile d'assiette, on peut rajouter ou enlever le cadre au sommet de la pile, mais pas toucher aux cadres en dessous, ni les manipuler.

Le nombre de manipulations possibles sur cette pile se résume donc à trois manipulations de base qu'on peut combiner pour créer des manipulations plus complexes. On peut ainsi :

  • détruire le cadre de pile au sommet de la pile, et supprimer tout son contenu de la mémoire : on dépile.
  • créer un cadre de pile immédiatement après le dernier cadre de pile existante : on empile.
  • utiliser les données stockées dans le cadre de pile au sommet de la pile.
Primitives de gestion d'une pile.

Si vous regardez bien, vous remarquerez que la donnée au sommet de la pile est la dernière donnée à avoir été ajoutée (empilée) sur la pile. Ce sera aussi la prochaine donnée à être dépilée (si on n'empile pas de données au-dessus). Ainsi, on sait que dans cette pile, les données sont dépilées dans l'ordre inverse d'empilement. Ainsi, la donnée au sommet de la pile est celle qui a été ajoutée le plus récemment.

Stack (data structure) LIFO.

Au fait, la pile peut contenir un nombre maximal de cadres, ce qui peut poser certains problèmes. Si l'on souhaite utiliser plus de cadres de pile que possible, il se produit un débordement de pile. En clair, l'ordinateur plante !

La délimitation des cadres de pile modifier

Pour gérer ces piles, on a besoin de sauvegarder deux choses : l'adresse à laquelle commence le cadre de pile en mémoire, et de quoi connaître l'adresse de fin. Le registre qui indique où est le sommet de la pile, quelle est son adresse, est appelé le pointeur de sommet, ou encore le Stack Pointer (SP). À ce registre, on peut rajouter un registre qui sert à donner l'adresse de début du cadre de pile : le Frame Pointer (FP), ou pointeur de contexte.

Pour localiser une donnée dans un cadre de pile, on utilise sa position par rapport au début ou la fin du cadre de pile. On peut donc calculer l'adresse de la donnée en additionnant cette position avec le contenu du pointeur de pile.

Certains processeurs possèdent deux registres spécialisés qui servent respectivement de pointeur de contexte et de pointeur de pile : on ne peut pas les utiliser pour autre chose. Si ce n'est pas le cas, on est obligé de stocker ces informations dans deux registres normaux, et se débrouiller avec les registres restants.

Frame pointer.

D'autres processeurs arrivent à se passer de Frame Pointer. Ceux-ci n'utilisent pas de registres pour stocker l'adresse de la base du cadre, mais préfèrent calculer cette adresse à partir de l'adresse de fin du cadre et de sa longueur. Cette longueur peut être stockée directement dans certaines instructions censées manipuler la pile : si chaque cadre a toujours la même taille, cette solution est clairement la meilleure. Cette solution est idéale si le cadre de pile a toujours la même taille. Mais il arrive que les cadres aient une taille qui ne soit pas constante : dans ce cas, on a deux solutions : soit stocker cette taille dans un registre, soit la stocker dans les instructions qui manipulent la pile, soit utiliser du code automodifiant.

Les fonctions et procédures logicielles modifier

La pile est très utilisée pour faciliter l'implémentation des fonctions, des fonctionnalités très communes des langages de programmation. Pour comprendre ce que sont ces fonctions, il faut faire quelques rappels.

Un programme contient souvent des suites d'instructions présentes en plusieurs exemplaires, qui servent souvent à effectuer une tâche bien précise : calculer un résultat bien précis, communiquer avec un périphérique, écrire un fichier sur le disque dur, ou autre chose encore. Sans utiliser de sous-programmes, ces suites d'instructions sont présentes en plusieurs exemplaires dans le programme. Le programmeur doit donc recopier à chaque fois ces suites d'instructions, ce qui ne lui facilite pas la tâche (sauf en utilisant l’ancêtre des sous-programmes : les macros). De plus, ces suites d'instructions sont présentes plusieurs fois dans le programme et elles prennent de la place inutilement ! Dans les langages de programmation modernes, il est possible de ne conserver qu'un seul exemplaire en mémoire et l'utiliser au besoin. L'exemplaire en question est ce qu'on appelle une fonction, ou encore un sous-programme. C'est au programmeur de « sélectionner » ces suites d'instructions et d'en faire des fonctions.

Les langages de programmation actuels ont des fonctionnalités liées aux fonctions, qui simplifient bien la vie des programmeurs.

  • En premier lieu, la fonction peut calculer un ou plusieurs résultats, qui sont récupérés par le programme principal et seront utilisés dans divers calculs. Ces résultats sont appelés des valeurs de retour. Généralement, c'est le programmeur qui décide de conserver une donnée et d'en faire une valeur de retour. Celui-ci peut avoir besoin de conserver le résultat d'un calcul pour pouvoir l'utiliser plus tard, par exemple. Ce résultat dépend fortement du sous-programme, et peut être n'importe quelle donnée : nombre entier, nombre flottant, tableau, objet, ou pire encore.
  • En second lieu, il est possible de communiquer des valeurs à une fonction : ce sont des arguments ou paramètres. On peut communiquer ces valeurs de deux manières, soit en en fournissant une copie, soit en fournissant leur adresse.
  • Enfin, une fonction peut calculer des données temporaires, souvent appelées des variables locales. Ces variables locales sont des données accessibles par le code de la fonction mais invisibles pour tout autre code. La variable locale est dite déclarée dans le code de la fonction, mais inaccessible ailleurs. Les variables locales sont opposées aux variables globales, qui sont accessibles dans tout le programme, par toute fonction. L'usage des variables globales est déconseillé, mais on en a parfois besoin.

Les instructions d'appel et de retour de fonction modifier

Pour exécuter une fonction, il faut exécuter un branchement dont l'adresse de destination est celle de la fonction : on dit qu'on appelle la fonction. Toute fonction se termine aussi par un branchement, qui permet au processeur de revenir là où il en était avant d'appeler la fonction.

Principe des sous-programmes.

Lors de l'appel d'une fonction, outre le branchement, il est parfois nécessaire d'effectuer d'autres opérations dont on ne peut pas encore parler à ce stade. Sur beaucoup de processeurs, ces opérations sont effectuées par le programme. Le branchement qui appelle le sous-programme est précédé par d'autres instructions qui s'en occupent. Mais sur d'autres processeurs, ces opérations et le branchement d'appel sont fusionnés en une seule instruction d'appel de fonction.

Function call in assembly.

De même, outre le branchement, d'autres opérations sont parfois nécessaires pour que le retour de la fonction se passe normalement. Là encore, certains processeurs disposent d'une instruction de retour de fonction dédiée, alors que d'autres non. Ces derniers émulent l'instruction de retour avec un branchement complété par d'autres instructions.

La sauvegarde de l'adresse de retour modifier

Le programme doit donc reprendre à l'instruction qui est juste après le branchement d'appel de fonction, l'adresse de celle-ci étant appelée l'adresse de retour. Mais vu qu'une fonction apparaît plusieurs fois dans notre programme, il y a plusieurs possibilités de retour, qui dépend de où est appelé la fonction. Mais alors, comment savoir à quelle instruction reprendre l'exécution de notre programme, une fois notre sous-programme terminé ? La seule solution est de sauvegarder l'adresse de retour lorsqu'on appelle la fonction ! Une fois le sous-programme fini, faut reprendre l’exécution de notre programme principal là où il s'était arrêté pour laisser la place au sous-programme. Pour cela, il suffit de charger l'adresse de retour dans un registre et d’exécuter un branchement inconditionnel vers cette adresse de retour.

La sauvegarde de l'adresse de retour est effectuée soit par l'instruction d'appel de fonction, soit par une instruction dédiée. Pour la restauration de l'adresse de retour dans le program counter, c'est la même chose. Sur certains processeurs, il faut charger l'adresse de retour dans un registre et utiliser un branchement inconditionnel vers cette adresse. Le branchement inconditionnel en question est un branchement inconditionnel indirect. Sur d'autres, l’instruction de retour de fonction fait cela automatiquement. C'est un branchement inconditionnel, mais l'adresse de retour est adressée implicitement.

La sauvegarde des registres modifier

Lorsqu'un sous-programme s'exécute, il va utiliser certains registres qui sont souvent déjà utilisés par le programme principal. Pour éviter d'écraser le contenu des registres, on doit donc conserver une copie de ceux-ci dans la pile, une sauvegarde de ceux-ci. Une fois que le sous-programme a fini de s'exécuter, il remet les registres dans leur état original en chargeant la sauvegarde dans les registres adéquats. Ce qui fait que lorsqu'un sous-programme a fini son exécution, tous les registres du processeur reviennent à leur ancienne valeur : celle qu'ils avaient avant que le sous-programme ne s'exécute. Rien n'est effacé !

Cette sauvegarde peut être effectuées automatiquement par l'instruction d'appel de fonction, mais c'est plutôt rare. La plupart du temps, elle est effectuée registre par registre par le logiciel, un petit morceau de code se chargeant de sauvegarder les registres. Plus rarement, certains processeurs ont une instruction pour sauvegarder les registres du processeur. C'est le cas sur les processeurs ARM1, qui ont une instruction pour sauvegarder n'importe quel sous-ensemble des registres du processeur. On peut par exemple choisir de sauvegarder le premier et le troisième registre en RAM, sans toucher aux autres. Le processeur se charge alors automatiquement de sauvegarder les registres uns par un en mémoire, bien que cela ne prenne qu'une seule instruction pour le programmeur.

L'implémentation matérielle de cette instruction est décrite dans les deux articles suivants :

L'usage de la pile d'appel par les fonctions modifier

Il n'est pas rare qu'un programme imbrique des fonctions, c'est à dire qu'une fonction peut appeler une autre fonction, qui elle-même en appelle une autre, et ainsi de suite. Pour exécuter plusieurs fonctions imbriquées les unes dans les autres, la totalité des langages de programmation actuels utilise la ou les pile d'appel du processeur. C'est d'ailleurs ce qui lui vaut son nom : elle sert pour les appels de fonction, ce qui fait qu'elle s'appelle pile d'appel (de fonctions). Le principe est simple : lorsqu'on appelle une fonction, on crée un cadre de pile qui va stocker toutes les informations nécessaires pour que l'appel de fonction se passe comme prévu : l'adresse de retour, les arguments/paramètres, la copie de sauvegarde des registres du processeur, les variables locales, etc. Une autre méthode utilise une pile séparée pour les appels de retour, une autre pour les arguments, etc. Mais passons, voyons maintenant comment cela fonctionne.

La pile d'adresse de retour modifier

En premier lieu, on doit sauvegarder plusieurs adresses de retour, les unes à la suite des autres dans l'ordre d'appel des fonctions.Ainsi, si une fonction F1 appelle une fonction F2, puis une fonction F3, les adresses de retour doivent se trouver dans le même ordre. De plus, quand une fonction se termine, l'adresse de retour est utilisée et doit être supprimée. On voit bien que ce comportement correspond à une pile. Si on stocke les adresses de retour dans une pile, l'adresse de retour à utiliser est située au sommet de la pile. La pile d'appel sert donc pour les fonctions imbriquées et est une pile d'adresses de retour pour les fonctions. Il est primordial que les adresses de retour soient stockées dans le bon ordre, pour que le programme reprenne au bon endroit.

Sur certains processeurs, la pile d'appel gère les adresses de retour et tout le reste des données. Mais sur d'autres, la pile d'appel est découpée en plusieurs piles séparées : la pile d'adresses de retour et une seconde pile appelée la pile de données. Cette séparation a des avantages et des inconvénients. Les avantages sont que la gestion des données est plus simple pour le programmeur, bien qu'elle manque de flexibilité. Un autre avantage est que certaines techniques de sécurité sont plus faciles à implémenter avec une pile d'appel séparée, comme on le verra plus bas. Par contre, la séparation a divers désavantages. Le principal est que les deux piles séparées sont potentiellement éloignées en mémoire, ce qui ne respecte pas très bien le principe de localité spatiale. Les performances sont alors réduites sur les processeurs avec une mémoire cache.

Si une fonction retourne et que l'adresse de retour n'est pas la bonne, le programme ne fonctionne pas comme prévu et cela peut donner des plantages ou tout autre conséquence fâcheuse. Diverses attaques informatiques cherchent justement à modifier l'adresse de retour d'une fonction pour exécuter du code malicieux. L'attaque demande d'insérer un morceau de code malicieux dans un programme (par exemple, un virus) et de l’exécuter lors d'un retour de fonction. Pour cela, l'assaillant trouve un moyen de modifier l'adresse de retour d'une fonction mal codée, puis en détourne le retour pour que le processeur ne reprenne pas là où il le devrait, mais reprenne l’exécution sur le virus/code malicieux. Le code malicieux est programmé pour que, une fois son travail accompli, le programme reprenne là où il devait une fois la fonction détournée terminée.

Il existe diverses techniques pour éviter cela et certaines de ces techniques se basent sur des procédés matériels, intégrés dans le processeur. Le premier de ces procédé est l'usage d'une shadow stack, une pile fantôme. Celle-ci est une simple copie de la pile d'appel, mémorisée en mémoire ou dans le processeur, qui est utilisée pour vérifier que les retours de fonction se passent bien. Sauf que là où la pile d'appel a des cadres volumineux, la pile fantôme ne mémorise que les adresses de retour. L'avantage est que ces attaques consistent généralement à injecter des données volumineuses dans un cadre de pile, afin de déborder d'un cadre de pile, voire de déborder de la mémoire allouée à la pile. Autant c'est possible avec les cadres de la pile d'appel, autant ce n'est pas possible avec la pile fantôme, vu qu'on n'y insère pas ces données. De plus, les deux piles d'appel sont éloignées en mémoire, ce qui fait que toute modification de l'une a peu de chances de se répercuter sur l'autre. Notons que les failles de sécurité de ce type sont plus compliquées si la pile d'adresse de retour est séparée de la pile d'appel, mais que ce n'est pas le cas sur les PC actuels.

La pile d'appel fantôme peut être gérée soit au niveau logiciel, par le langage de programmation ou le système d'exploitation, mais aussi directement en matériel. C'est le cas sur les processeurs x86 récents, depuis l'intégration par Intel de la technologie Control-flow Enforcement Technology. Il s'agit d'une pile fantôme gérée directement par le processeur. Quand le processeur exécute une instruction de retour de fonction RET, il vérifie automatiquement que l'adresse de retour est la même dans la pile d'appel et la pile fantôme. Si il y a une différence, il stoppe l’exécution du programme et prévient le système d'exploitation (pour ceux qui a ont déjà lu le chapitre sur les interruptions : il démarre une exception matérielle spécialisée appelée Control Flow Protection Fault.

Les autres piles/données du cadre d'appel modifier

Outre l'adresse de retour, il faut aussi sauvegarder les registres du processeur avant l'appel de la fonction. Là encore, les registres à sauvegarder sont mémorisés dans une pile, pour les mêmes raisons. Là encore, on peut sauvegarder ces registres dans une pile d'appel unique, ou dans une pile de sauvegarde des registres séparée.

La transmission des arguments à une fonction peut se faire en les copiant soit dans la pile, soit dans les registres. Dans le cas d'un passage par les registres, les registres qui contiennent les paramètres ne sont pas sauvegardés lors de l'appel de la fonction. Généralement, le passage par la pile est très utilisé sur les processeurs avec peu de registres, alors que les processeurs avec beaucoup de registres privilégient le passage par les registres. Si on utilise le passage par les registres, il faut que le nombre de registres soit suffisant. La plupart des fonctions ayant peu d'arguments, cela ne pose que rarement problème. Mais si une fonction a plus d'arguments que de registres, ou que la fonction utilise beaucoup de variables locales, les arguments en trop doivent être passés par la pile.

On a le même genre de compromis à faire avec la valeur de retour d'une fonction, qui peut être conservée soit dans les registres, soit dans la pile d'appel. Il est théoriquement possible de la stocker dans un registre, mais il faut faire attention à ce qu'elle ne soit pas écrasée lors de la restauration des registres. Mais cela ne marche que si la valeur de retour tient dans un registre : un registre contenant 64 bits pourra difficilement contenir une valeur de retour de 5 kilo-octets. Une autre solution consiste à stocker ces valeurs de retour dans la pile d'appel, plus rarement dans une pile des valeurs de retour dédiée (à condition que ces valeurs de retour aient toutes une taille fixe, ce qui n'est pas possible avec certains langages de programmation).

Pour gérer les variables locales, il est possible de réserver une portion de la mémoire statique pour chaque, dédiée au stockage de ces variables locales, mais cela gère mal le cas où une fonction s'appelle elle-même (fonctions récursives). Une autre solution est de réserver un cadre de pile pour les variables locales. Cela demande cependant d'avoir des cadres de pile de taille variable, ce que le processeur et/ou le langage de programmation doit gérer nativement. Le processeur dispose de modes d'adressages spécialisés pour adresser les variables automatiques d'un cadre de pile. Ces derniers ajoutent une constante au pointeur de pile.

Pour résumer, les processeurs processeurs possèdent soit une pile pour tout, soit plusieurs piles spécialisées. La plupart des processeurs ne possèdent qu'une seule pile à la fois pour les adresses de retour, les variables locales, les paramètres, et le reste. La pile utilise alors des cadres de pile de taille variable, dans lesquels on peut ranger plusieurs données. Les variables locales sont souvent regroupées, de même que les arguments, le reste étant placé à une position bien précise dans le cadre de pile.

Pile exécution contenant deux cadres de pile, un pour la fonction drawLine() et un autre pour la fonction drawSquare(). Le bloc d'activation correspond grosso-modo au cadre de pile, auquel on ajoute les arguments (non-compris dans le cadre de pile dans cet exemple, chose plutôt rare).


Les interruptions sont des fonctionnalités du processeur qui ressemblent beaucoup aux appels de fonctions, mais avec quelques petites différences. Les interruptions, comme leur nom l'indique, interrompent temporairement l’exécution d'un programme pour effectuer un sous-programme nommé routine d'interruption. Lorsqu'un processeur exécute une interruption, celui-ci :

  • arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
  • exécute la routine d'interruption ;
  • restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
Interruption processeur

L'appel d'une routine d'interruption est très similaire à un appel de fonction et implique les mêmes chose : sauvegarder les registres du processeur, l'adresse de retour, etc. Tout ce qui a été dit pour les fonctions marche aussi pour les interruptions. La différence est que la routine d'interruption appartient au système d'exploitation ou à un pilote de périphérique, mais pas au programme en cours d'exécution.

Les interruptions sont classées en trois types distincts, aux utilisations très différentes : les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Les deux premières sont des interruptions générés par un évènement extérieur au programme, alors que les interruptions logicielles sont déclenchées quand le programme éxecute une instruction précise pour s'interrompre lui-même, afin d'éxecuter du code appartenant au système d'exploitation ou à un pilote de périphérique.

Les interruptions et les exceptions matérielles modifier

Les exceptions matérielles et les interruptions matérielles permettent de réagir à un événement extérieur : communication avec le matériel, erreur fatale d’exécution d'un programme. Le programme en cours d'exécution est alors stoppé pour réagir, avant d'en reprendre l'exécution. Elles sont initiés par un évènement extérieur au programme, contrairement aux interruptions logicielles.

Déroulement d'une interruption.

Les exceptions matérielles modifier

Une exception matérielle est une interruption déclenchée par un évènement interne au processeur, par exemple une erreur d'adressage, une division par zéro... Le processeur intègre des circuits qui détectent l'évènement déclencheur, ainsi que des circuits pour déclencher l'exception matérielle. Prenons l'exemple d'une exception déclenchée par une division par zéro : le processeur doit détecter les divisions par zéro. Lorsqu'une exception matérielle survient, la routine exécutée corrige l'erreur qui a été la cause de l'exception matérielle, et prévient le système d'exploitation si elle n'y arrive pas. Elle peut aussi faire planter l'ordinateur, si l'erreur est grave, ce qui se traduit généralement par un écran bleu soudain.

Pour donner un exemple d'utilisation, sachez qu'il existe une exception matérielle qui se déclenche quand on souhaite exécuter une instruction non-reconnue par le processeur. Rappelons que les instructions sont codées par des suites de bits en mémoire, codées sur quelques octets. Mais cela ne signifie pas que toutes les suites de bits correspondent à des instructions : certaines suites ne correspondent pas à des instructions et ne sont pas reconnues par le processeur. Dans ce cas, le chargement dans le processeur d'une telle suite de bit déclenche une exception matérielle "Instruction non-reconnue".

Et cela a été utilisé pour émuler des instructions sur les nombres flottants sur des processeurs qui ne les géraient pas. Pour cela, on modifiait la routine de l'exception "Instruction non-reconnue" de manière à ce qu'elle reconnaisse les suites de bits correspondant à des instructions flottantes et exécute une suite d'instruction entière équivalente.

Les interruptions matérielles modifier

Les interruptions matérielles, aussi appelées IRQ, sont des interruptions déclenchées par un périphérique. La routine d'interruption est alors fournie par le pilote du périphérique. Du moins, c'est comme ça sur le matériel moderne, les anciens PC utilisaient des routines d'interruption fournies par le BIOS. Ce sont celles qui vont nous intéresser dans le chapitre sur la communication avec les périphériques, mais nous n'en parlerons pas dans le détail avant quelques chapitres.

Un exemple d'utilisation des interruptions matérielles la gestion de certains périphériques. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc.

Un autre exemple est la gestion des timers. Par exemple, imaginons que vous voyiez à un jeu vidéo, et qu'il vous reste 2 minutes 45 secondes pour sortir d'un laboratoire de recherche avant que l'auto-destruction ne s'active. La durée de 2 minutes 45 est programmée dans un timer, un circuit compteur qui permet de compter une durée. Le jeu vidéo programme le timer pour qu'il compte durant 2 minutes 45 secondes, puis attend que ce dernier ait finit de compter. Une fois la durée atteinte, le timer déclenche une interruption, pour stopper l'exécution du jeu vidéo. La routine d'interruption prévient le système d'exploitation que le timer a finit de compter, le jeu vidéo est alors prévenu, et fait ce qu'il a à faire.

Les interruptions logicielles modifier

Les interruptions logicielles sont différentes des deux précédentes dans le sens où elles ne sont pas déclenchées par un évènement extérieur. A la place, elles sont déclenchées par un programme en cours d'exécution, via une instruction d'interruption. On peut les voir comme des appels de fonction un peu particuliers, si ce n'est que la routine d'interruption exécutée n'est pas fournie par le programme exécuté, mais par le système d'exploitation, un pilote de périphérique ou le BIOS. Le code éxecuté ne fait pas partie du programme éxecuté, mais en est extérieur, et cela change beaucoup de choses.

Sur les PC anciens, le BIOS fournissait les routines de base et le système d'exploitation se contentait d’exécuter les routines fournies par le BIOS. Mais de nos jours, les routines d'interruptions du BIOS sont utilisées lors du démarrage de l'ordinateur, mais ne sont plus utilisées une fois le système d'exploitation lancé. Le système d'exploitation fournit ses propres routines et n'a pas plus besoin des routines du BIOS.

Les interruptions du BIOS et des autres firmwares modifier

Le BIOS fournit des routines d'interruption pour gérer les périphériques et matériels les plus courants. Ce n'est pas pour rien que « BIOS » est l'abréviation de Basic Input Output System, ce qui signifie « programme basique d'entrée-sortie ». Ces routines sont standardisées de façon à assurer la compatibilité des programmes sur tous les BIOS existants. Ce standard, malgré sa simplicité, était extrêmement puissant. Il était possible de créer un OS complet en utilisant juste des appels de routine du BIOS. Par exemple, le DOS, ancêtre de Windows, utilisait exclusivement les routines du BIOS !

Certaines routines peuvent notamment effectuer plusieurs traitements : par exemple, la routine qui permet de communiquer avec le disque dur peut aussi bien lire un secteur, l'écrire, etc. Pour spécifier le traitement à effectuer, on doit placer une certaine valeur dans le registre AH du processeur : la routine est programmée pour déduire le traitement à effectuer uniquement à partir de la valeur du registre AH. Mais certaines routines ne font pas grand-chose : par exemple, l'interruption 0x12h ne fait que lire la taille de la mémoire conventionnelle, qui est mémorisée à un endroit bien précis en mémoire RAM.

Voici une description assez succincte de ces routines. Vous remarquerez que je n'ai pas vraiment détaillé ce que font ces interruptions, ni comment les utiliser. Il faut dire que de nos jours, ce n'est pas franchement utile. Mais si vous voulez en savoir plus, je vous invite à lire la liste des interruptions du BIOS de Ralf Brown, disponible via ce lien : Liste des interruptions du BIOS, établie par Ralf Brown.

Adresse de la routine dans le vecteur d'interruption Description succinte
10h Si aucune ROM vidéo n'est détectée, le BIOS peut quand même communiquer directement avec la carte graphique grâce à cette routine. Elle a plusieurs fonctions différentes et peut tout aussi bien envoyer un caractère à l'écran que renvoyer la position du curseur.
13h Cette routine du BIOS permet de lire ou d'écrire sur le disque dur ou sur une disquette. Plus précisément, cette routine lui sert à lire les premiers octets d'un disque dur afin de pouvoir charger le système d'exploitation. Elle était aussi utilisée par les systèmes d'exploitation du style MS-DOS pour lire ou écrire sur le disque dur.
14h La routine 14h était utilisée pour communiquer avec le port série RS232 de notre ordinateur.
15h La routine 15h a des fonctions diverses et variées, toutes plus ou moins rattachées à la gestion du matériel. Le BIOS était autrefois en charge de la gestion de l'alimentation de notre ordinateur : il se chargeait de la mise en veille, de réduire la fréquence du processeur, d'éteindre les périphériques inutilisés. Pour cela, la routine 15h était utilisée. Ses fonctions de gestion de l'énergie étaient encore utilisées jusqu'à la création de Windows 95. De nos jours, avec l'arrivée de la norme ACPI, le système d'exploitation gère tout seul la gestion de l'énergie de notre ordinateur et cette routine est donc obsolète. À toute règle, il faut une exception : cette routine est utilisée par certains systèmes d'exploitation modernes à leur démarrage afin d'obtenir une description correcte et précise de l'organisation de la mémoire de l'ordinateur. Pour cela, nos OS configurent cette routine en plaçant la valeur 0x0000e820 dans le registre EAX.
16h La routine 16h permet de gérer le clavier et de le configurer. Cette routine est utilisée tant que le système d'exploitation n'a pas démarré, c'est pour cela que vous pouvez utiliser le clavier pour naviguer dans l'écran de configuration de votre BIOS. En revanche, aucune routine standard ne permet la communication avec la souris : il est impossible d'utiliser la souris dans la plupart des BIOS. Certains BIOS possèdent malgré tout des routines capables de gérer la souris, mais ils sont très rares.
17h Cette routine permet de communiquer avec une imprimante sur le port parallèle de l'ordinateur. Comme les autres, on la configure avec le registre AH.
19h Cette routine est celle qui s'occupe du démarrage du système d'exploitation. Elle sert donc à lancer le système d'exploitation lors du démarrage d'un ordinateur, mais elle sert aussi en cas de redémarrage.

Les routines du BIOS étaient parfois recopiées dans la mémoire RAM afin de rendre leur exécution plus rapide. Certaines options du BIOS, souvent nommées BIOS memory shadowing, permettent justement d'autoriser ou d'interdire cette copie du BIOS dans la RAM.

Les appels systèmes des systèmes d'exploitation modifier

Tout OS fournit un ensemble de routines d'interruptions spécifiques qui servent à manipuler la mémoire, gérer des fichiers, etc. Les interruptions en question sont appelées des appels système. Par exemple, linux fournit les appels systèmes open, read, write et close pour manipuler des fichiers, ou encore les appels brk, sbrk, pour allouer et désallouer de la mémoire. Évidemment, ceux-ci ne sont pas les seuls : linux fournit environ 380 appels systèmes distincts.

Les appels systèmes permettent aux programmes d'exécuter des interruptions pré-programmées, mais ne peuvent pas demander n'importe quoi au système d'exploitation. La communication entre OS et programmes est donc standardisée, limitée par une interface, ce qui limite les problèmes de sécurité et simplifie la programmation des applications.

Les appels systèmes sont un concept des systèmes d'exploitation, qui peuvent se mettre en œuvre de plusieurs manières. On peut les implémenter avec des interruptions logicielles, des instructions de commutation en mode noyau, éventuellement d'autres. Assimiler interruptions logicielles et appels systèmes est en soi une erreur, mais même si les deux sont très liés.

L'espace noyau et l'espace utilisateur modifier

La différence entre une interruption et un appel de fonction est notable sur les systèmes d'exploitation modernes, où le système d'exploitation a des privilèges que les autres programmes n'ont pas. Rappelons que le système d'exploitation sert d'intermédiaire entre les autres logiciels et le matériel. Les programmes ne sont pas censés accéder d'eux-mêmes au matériel, pour des raisons de portabilité, de sécurité, et bien d'autres. Ils ne peuvent pas accéder directement au disque dur, au clavier, à la carte son, etc. À la place, ils demandent au système d'exploitation de le faire à leur place et de leur transmettre les résultats.

Les niveaux de privilèges modifier

Pour forcer la séparation entre OS et programmes, les processeurs modernes intègrent des sécurités qui portent le nom de niveaux de privilèges, ou encore d'anneaux mémoires. Tous les processeurs des PC modernes (x86 64 bits) gèrent seulement deux niveaux de privilèges : un mode noyau pour le système d'exploitation et un mode utilisateur pour les applications. Tout est autorisé en mode noyau, alors le mode utilisateur a de nombreuses restrictions quant à l'accès au matériel et la mémoire. Un programme en mode utilisateur ne peut pas accéder aux périphériques, ni gérer certaines portions protégées de la mémoire. C'est un mécanisme qui force à déléguer la gestion du matériel au système d'exploitation.

Au passage, tout l'OS n'est pas en mode noyau, seule une petite partie l'est et elle porte d'ailleurs le nom de noyau du système d’exploitation.

Outre l'accès aux périphériques, l'accès à la mémoire est aussi restreint par divers mécanismes dits de protection mémoire, mais seulement en mode utilisateur. Un programme en mode utilisateur se voit attribuer une certaine portion de la mémoire RAM, et ne peut accéder qu'à celle-ci. En clair, les programmes sont isolés les uns des autres : un programme ne peut pas aller lire ou écrire dans la mémoire d'un autre, les programmes ne se marchent pas sur les pieds, les bugs d'un programme ne débordent pas sur les autres programmes, etc.

Niveaux de privilèges sur les processeurs x86.

Les anneaux mémoire/niveaux de privilèges étaient initialement gérés uniquement par des mécanismes purement logiciels, mais sont actuellement gérés par le processeur. Le processeur contient un registre qui précise si le programme en cours est en espace noyau ou en espace utilisateur. À chaque accès mémoire ou exécution d'instruction, le processeur vérifie si le niveau de privilège permet l'opération demandée. Lorsqu'un programme effectue une instruction interdite en mode utilisateur, une exception matérielle est levée. Généralement, le programme est arrêté sauvagement et un message d'erreur est affiché.

Sur certains processeurs, on trouve des niveaux de privilèges intermédiaires entre l'espace noyau et l'espace utilisateur. Les processeurs x86 des PC 32 bits contiennent 4 niveaux de privilèges. Le système Honeywell 6180 en possédait 8. À l'origine, ceux-ci ont été inventés pour faciliter la programmation des pilotes de périphériques. Mais force est de constater que ceux-ci ne sont pas vraiment utilisés, seuls les espaces noyau et utilisateur étant pertinents.

Les interruptions basculent en mode noyau modifier

Toute interruption bascule automatiquement le processeur dans l'espace noyau. C'est une nécessité : on passe d'un programme en espace utilisateur à une routine qui est en espace noyau. L'accès au matériel n'étant possible qu'en mode noyau, les interruptions matérielles doivent faire la transition en espace noyau, pareil pour les appels systèmes qui manipulent le matériel. Même sans accès aux périphériques, le passage en mode noyau est nécessaire pour passer outre la protection mémoire. A cause de la protection mémoire, impossible de manipuler certaines structures de données du système d'exploitation, chose seulement possible en espace noyau. De plus, le code du système d'exploitation est inaccessible pour les autres programmes. Exécuter une routine du système d'exploitation avec un branchement vers celle-ci ne marchera pas, car le branchement pointera une instruction présente dans une portion de mémoire réservée à l'OS, auquel le programme exécutant n'a pas accès. Cela se traduira par un joli plantage de l'application.

Le passage en mode noyau n'est cependant pas gratuit, de même que l'interruption qui lui est associée. Ainsi, les interruptions sont généralement considérées comme lentes, très lentes. Elles sont beaucoup plus lentes que les appels de fonction normaux, qui sont beaucoup plus simples. Les raisons à cela sont multiples, mais la principale est la suivante : les mémoires caches doivent être vidés lors des transferts entre mode noyau et mode utilisateur. Alors attention : diverses optimisations font que seuls certains caches spécialisés dont nous n'avons pas encore parlé, comme les TLB, doivent être vidés. Mais malgré tout, cela prend beaucoup de temps.

Divers processeurs incorporent des techniques pour rendre les appels systèmes plus rapides, en remplaçant les interruptions logicielles par des instructions spécialisées (SYSCALL/SYSRET et SYSENTER/SYSEXIT d'AMD et Intel). D'autres techniques similaires tentent de faire la même chose, à savoir changer le niveau de privilège sans utiliser d'interruptions : les call gate d’Intel, les Supervisor Call instruction des IBM 360, etc.

L'implémentation des interruptions modifier

Toutes les interruptions, qu'elles soient logicielles ou matérielles, ne s'implémentent pas exactement de la même manière. Mais certaines choses sont communes à toutes les interruptions et à toutes leurs mises en œuvre. Par exemple, on s'attend à ce que la majeure partie des processeurs qui supportent les interruptions disposent des fonctionnalités que nous allons voir dans ce qui suit, à savoir : un vecteur d'interruption, une pile dédiée aux interruptions, la possibilité de désactiver les interruptions, etc. Elles ne sont pas tout le temps présentes, mais leur absence est plus une exception que la régle.

Le vecteur d'interruption modifier

Vu le grand nombre d'interruptions logicielles/appels système, on se doute bien qu'il y a a peu-près autant de routines d'interruptions différentes. Et celles-ci sont placées à des endroits différents en mémoire RAM. Une solution simple serait de placer chaque routine d'interruption systématiquement au même endroit en mémoire. Les appels systèmes se résumeraient alors à des appels de fonctions basiques, avec un branchement inconditionnel vers l'adresse de la routine, connue à l'avance. Mais elle n'est pas très pratique, surtout quand on ne connait pas à l'avance l'emplacement des pilotes de périphériques en mémoire. Autant on peut prévoir tout cela à l'avance pour les routines du système d'exploitation, autant on ne peut pas le faire pour les routines des pilotes de périphériques dont on ne connait pas à l'avance ni le nombre, ni la taille, ni la fonction. De plus, sur les systèmes modernes, les adresses des routines peuvent changer lors de l'exécution des programmes pour de sombres raisons impliquant de la mémoire virtuelle et que nous verrons dans les chapitres à la fin de ce wikilivre.

Pour résoudre ce problème, les systèmes d'exploitation modernes font autrement. Ils numérotent les interruptions, à savoir qu'ils leur attribuent un numéro en commençant par 0. Un PC X86 moderne gére 256 interruptions, numérotées de 0 à 255. Un appel système ne précise pas l'adresse vers laquelle faire un branchement, mais précise le numéro de l'interruption à exécuter. Le système d'exploitation s'occupe ensuite de retrouver l'adresse de la routine à partir du numéro de l'interruption. Pour cela, ils dispersent un petit peu les routines dans la mémoire, mais mémorisent l'emplacement de chaque routine. Pour cela, le système d'exploitation utilise un tableau qui contient les adresses de chaque routine : la case numéro X du tableau contient l'adresse de l'interruption numéro X. Ce tableau s'appelle le vecteur d'interruption.

Pour ceux qui connaissent la programmation, le vecteur d'interruption est un tableau de pointeurs sur fonction, les fonctions étant les routines à exécuter.

Il faut préciser que le vecteur d'interruption mémorise les adresses pour toutes les routines, sans exceptions. Non seulement il mémorise celles des appels systèmes, mais aussi les routines des exceptions matérielles, ainsi que les routines des interruptions matérielles. Sur les PC modernes, le vecteur d'interruption est stocké dans les 1024 premiers octets de la mémoire. Il gére 256 interruptions, et les 32 premières sont réservées aux exceptions matérielles.

Une interruption logicielle se déroule donc comme suit : le programme exécute une instruction d'interruption et précise le numéro de l'interruption logicielle à exécuter, puis l'OS utilise le numéro pour lire dans le vecteur d'interruption, ce qui permet de récupérer l'adresse de la routine adéquate, et effectue un branchement vers celle-ci. Avec cette méthode, l'adresse de la routine n'a pas à être précisée à l'avance, lors de la conception de l'OS, et elle peut même changer lors de l'exécution d'un programme !

Le fait que les interruptions soient désignées non pas par une adresse, mais par un numéro explique pourquoi un appel système n'est pas qu'un simple appel de fonction. Il y a un niveau d'indirection en plus. C'est une des raisons qui fait qu'il est préférable d'avoir une instruction spécifique pour le processeur, séparée de l'instruction d'appel de fonction normale.

La conversion d'un numéro d'interruption en adresse peut se faire au niveau matériel ou logiciel. S'il est fait au niveau matériel, le processeur lit l'adresse automatiquement dans le vecteur d'interruption. Avec la solution logicielle, on délègue ce choix au système d'exploitation. Dans ce cas, le processeur contient un registre qui stocke le numéro de l'interruption, ou du moins de quoi déterminer la cause de l'interruption : est-ce le disque dur qui fait des siennes, une erreur de calcul dans l'ALU, une touche appuyée sur le clavier, etc.

Le vecteur d'interruption peut être mis à jour, ce qui permet de remplacer à la volée les routines d'interruptions utilisées. Il suffit de remplacer les adresses des routines à mettre à jour par les adresses des routines adéquates. On dit qu'on déroute ou qu'on détourne le vecteur d'interruption. Tous les systèmes d'exploitation modernes le font après le démarrage de l'ordinateur, pour remplacer les interruptions du BIOS par les interruptions fournies par le système d'exploitation et les pilotes. Cette mise à jour est effectuée par le système d'exploitation, une fois que le BIOS lui a laissé les commandes.

La pile dédiée aux interruptions modifier

La plupart des systèmes d'exploitation utilisent une pile d'appel dédiée, séparée, pour les interruptions. Les raisons à cela sont multiples. La principale est que sur les systèmes d'exploitation capables de gérer plusieurs programmes en même temps, c'est une solution assez évidente. Chaque programme a sa propre pile d'appel, séparée des autres. Et la routine d'interruption est un programme comme un autre, qui doit donc avoir sa propre pile d'appel.

Désactiver les interruptions modifier

Il faut noter qu'il est possible de désactiver temporairement l’exécution des interruptions, quelle qu’en soit la raison. C'est beaucoup utilisé sur les systèmes multiprocesseurs, afin d'éviter des problèmes lors de lecture/écriture d'une donnée manipulée par plusieurs processeurs. Mais nous verrons cela dans le chapitre adéquat, quand nous parlerons des systèmes multicœurs/multi-processeurs.

C'est aussi utilisé dans certains systèmes dit temps réels, où les concepteurs ont besoin de garanties assez fortes pour le temps d’exécution. Dans ces systèmes, chaque morceau de code doit s’exécuter en un temps définit à l'avance, qu'il ne doit pas dépasser. Pour garantir cela, certaines portions de code ne doivent pas être interrompues par des interruptions. Par exemple, prenons le cas d'une portion de code devant s’exécuter en moins de 300 millisecondes. Imaginons aussi que le code en question prend 200 ms sans interruption. Ce code peut parfaitement dépasser son temps attitré si plusieurs interruptions surviennent avec un timing problématique : il suffit que plus de 2 interruptions de 50 ms surviennent pendant que le code s’exécute. Désactiver les interruptions pendant le temps d’exécution du code permet d'éviter cela.


Un programmeur (ou un compilateur) qui souhaite programmer en langage machine peut manipuler les registres intégrés dans le processeur. À ce stade, il faut faire une petite remarque : tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il existe ainsi deux types de registres : les registres architecturaux, manipulables par des instructions, et les registres internes aux processeurs. Ces derniers servent à simplifier la conception du processeur ou mettre en œuvre des optimisations de performance. Dans ce qui suit, nous allons parler uniquement des registres architecturaux.

L'utilisation des registres modifier

Les registres architecturaux se distinguent par le type de données que peuvent contenir leurs registres. Des processeurs ont des registres banalisés qui peuvent contenir tout type de données, tandis que d'autres ont des registres spécialisés pour chaque type de données. Les deux solutions ont des avantages et inconvénients différents. Elles sont toutefois complémentaires et non exclusives. Certains processeurs ont des registres généraux complémentés avec des registres spécialisés, pour des raisons de facilité.

Les registres spécialisés modifier

Les registres spécialisés ont une utilité bien précise et leur fonction est fixée une bonne fois pour toutes. Un registre spécialisé est conçu pour stocker soit des nombres entiers, des flottants, des adresses, etc; mais pas autre chose. Pour ce qui est des registres spécialisés, le plus important est clairement le Program Counter. On peut aussi trouver les registres pour gérer la pile (le Stack Pointer et le Frame Pointer). Pour ce qui est des registres de données les plus courants, en voici la liste.

  • Les registres entiers sont spécialement conçus pour stocker des nombres entiers.
  • Les registres flottants sont spécialement conçus pour stocker des nombres flottants.
  • Les registres de constante contiennent des constantes assez souvent utilisées. Par exemple, certains processeurs possèdent des registres initialisés à zéro pour accélérer la comparaison avec zéro ou l'initialisation d'une variable à zéro. On peut aussi citer certains registres flottants qui stockent des nombres comme \pi, ou e pour faciliter l'implémentation des calculs trigonométriques).
  • Les registres d'Index servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux.

Nous avons parlé de ces registres dans les chapitres précédents, ce qui fait que nous n'allons pas revenir dessus. Par contre, nous n’avons pas encore vu le registre spécialisé par excellence : le registre d'état.

Le registre d'état modifier

Le registre d'état contient au minimum des bits qui indiquent le résultat d'une instruction de test. Il contient aussi d'autres bits, mais dont l'interprétation dépend du jeu d'instruction. Il arrive que le registre d'état soit mis à jour non seulement par les instructions de test, mais aussi par les instructions arithmétiques. Par exemple, si le résultat d'une opération arithmétique entraine un débordement d'entier, le registre d'état mémorisera ce débordement. Dans le chapitre précédent, nous avions vu que les débordements sont mémorisés par le processeur dans un bit dédié, appelé le bit de débordement. Et bien ce dernier est un bit du registre d'état. Il en est de même pour le bit de retenue vu dans le chapitre précédent, qui mémorise la retenue effectuée par une opération arithmétique comme une addition, une soustraction ou un décalage. En général, le registre d'état contient les bits suivants :

  • le bit d'overflow, qui est mis à 1 lors d'un débordement d'entiers ;
  • le bit de retenue, qui indique si une addition/soustraction a donné une retenue ;
  • le bit null précise que le résultat d'une instruction est nul (vaut zéro) ;
  • le bit de signe, qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif.

Le bit de débordement est parfois présent en double : un bit pour les débordements pour les nombres non-signés, et un autre pour les nombres signés (en complément à deux). En effet, la manière de détecter les débordements n'est pas la même pour des nombres strictement positifs et pour des nombres en complément à deux. Certains processeurs s'en sortent avec un seul bit de débordement, en utilisant deux instructions d'addition : une pour les nombres signés, une autre pour les nombres non-signés. Mais d'autres processeurs utilisent une seule instruction d'addition pour les deux, qui met à jour deux bits de débordements : l'un qui détecte les débordements au cas où les deux opérandes sont signés, l'autre si les opérandes sont non-signées. Sur les processeurs ARM, c'est la seconde solution qui a été choisie. N'oublions pas les bits de débordement pour les entiers BCD, à savoir le bit de retenue et le bit half-carry, dont nous avions parlé au chapitre précédent.

Le registre d'état n'est pas présent sur toutes les architectures, notamment sur les jeux d'instruction modernes, mais beaucoup d'architectures anciennes en ont un.

Sur certains processeurs, comme l'ARM1, chaque instruction arithmétique existe en deux versions : une qui met à jour le registre d'état, une autre qui ne le fait pas. L'utilité de cet arrangement n'est pas évident, mais il permet à certaines instructions arithmétiques de ne pas altérer le registre d'état, ce qui permet de conserver son contenu pendant un certain temps.

Le fait que le registre d'état est mis à jour par les instructions arithmétiques permet d'éviter de faire certains tests gratuitement. Par exemple, imaginons un morceau de code qui doit vérifier si deux entiers A et B sont égaux, avant de faire plusieurs opérations sur la différence entre les deux (A-B). Le code le plus basique pour cela fait la comparaison entre les deux entiers avec une instruction de test, effectue un branchement, puis fait la soustraction pour obtenir la différence, puis les calculs adéquats. Mais si la soustraction met à jour le registre d'état, on peut simplement faire la soustraction, faire un branchement qui teste le bit null du registre d'état, puis faire les calculs. Une petite économie toujours bonne à prendre.

Il faut noter que certaines instructions sont spécifiquement conçues pour altérer uniquement le registre d'état. Par exemple, sur les processeurs x86, certaines instructions ont pour but de mettre le bit de retenue à 0 ou à 1. Il existe en tout trois instructions capables de manipuler le bit de retenue : l'instruction CLC (CLear Carry) le met à 0, l'instruction STC (SeT Carry) le met à 1, l'instruction CMC (CompleMent Carry) l'inverse (passe de 0 à 1 ou de 1 à 0). Ces instructions sont utilisées de concert avec les instructions d'addition ADDC (ADD with Carry) et SUBC (SUB with Carry), qui effectuent le calcul A + B + Retenue et A - B - Retenue, et qui sont utilisées pour additionner/soustraire des opérandes plus grandes que les registres. Nous avions vu ces instructions dans le chapitre sur les instructions machines, aussi je ne reviens pas dessus.

Les registres généraux modifier

Fournir des registres très spécialisés n'est pas très flexible. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers, de 4 registres d'Index pour calculer des adresses, et de 4 registres flottants. Si jamais j’exécute un morceau de programme qui manipule beaucoup de nombres entiers, mais qui ne manipule pas d'adresses ou de nombre flottants, j'utiliserais juste les 4 registres entiers. Une partie des registres du processeur sera inutilisé : tous les registres flottants et d'Index. Le problème vient juste du fait que ces registres ont une fonction bien fixée.

En réfléchissant, un registre est un registre, et il ne fait que stocker une suite de bits. Il peut tout stocker : adresses, flottants, entiers, etc. Pour plus de flexibilité, certains processeurs ne fournissent pas de registres spécialisés comme des registres entiers ou flottants, mais fournissent à la place des registres généraux utilisables pour tout et n'importe quoi. Ce sont des registres qui n'ont pas d'utilité particulière et qui peuvent stocker toute sorte d’information codée en binaire. Pour reprendre l'exemple du dessus, un processeur avec des registres généraux fournira un Program Counter et 12 registres généraux, qu'on peut utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 12 entiers, 10 entiers et 2 flottants, 7 adresses et 5 entiers, etc. Ce qui sera plus flexible et permettra de mieux utiliser les registres.

Les méthodes hybrides modifier

Le choix entre registres spécialisés et registres généraux est une question de pragmatisme. Il existe bien des processeurs qui ne le sont pas et où tous les registres sont des registres généraux, même le Program Counter. Sur ces processeurs, on peut parfaitement lire ou écrire dans le Program Counter sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur le Program Counter, on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans le registre. On peut même faire des calculs sur le contenu du Program Counter : cela n'a pas toujours de sens, mais cela permet parfois d'implémenter facilement certains types de branchements avec des instructions arithmétiques usuelles.

D'autres processeurs font des choix moins extrême mais tout aussi discutables. Par exemple, l'ARM1 fusionne le registre d'état et le program counter en un seul registre de 32 bits... La raison à cela est que ce processeur manipule des manipule entiers et adresses de 32 bits, ce qui fait que ses registres font 32 bits, le le program counter ne fait pas exception. Mais le program counter n'a besoin que de 26 bits pour fonctionner. Il reste donc 32-26=6 bits à utiliser pour autre chose. De plus, les instructions de ce processeur font exactement 32 bits, pas un de plus ni de moins, et elles sont alignées en mémoire. Donc, les 2 bits de poids faibles du program counter sont inutilisés. Au total, cela fait 8 bits inutilisés. Et ils ont été réutilisés pour mémoriser les bits du registre d'état.

Mais le cas précédent est rare, très rare. Dans la réalité, les processeurs utilisent souvent une espèce de mélange entre les deux solutions. Généralement, une bonne partie des registres du processeur sont des registres généraux, à part quelques registres spécialisés, accessibles seulement à travers quelques instructions bien choisies. Sur les processeurs modernes, l'usage de registres spécialisés est tombé en désuétude, sauf évidemment pour le program counter. Les registres d'index ont disparus, les registres pour gérer la pile aussi, le registre d'état est en voie de disparition car il se marie mal avec les optimisations modernes que nous verrons dans quelques chapitres (pipeline, exécution dans le désordre, renommage de registres).

L'adressage des registres architecturaux modifier

Outre leur taille, les registres du processeur se distinguent aussi par la manière dont on peut les adresser, les sélectionner. Les registres du processeur peuvent être adressés par trois méthodes différentes. À chaque méthode correspond un mode d'adressage différent. Les modes d'adressage des registres sont les modes d'adressages absolu (par adresse), inhérent (à nom de registre) et/ou implicite.

Les registres nommés modifier

Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse bien précise. Pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder. Cet identifiant est ce qu'on appelle un nom de registre. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Ce numéro, ou nom de registre, permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur : ce nom de registre, ce numéro, ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire.

Adressage des registres via des noms de registre.

Les registres adressés modifier

Mais il existe une autre solution, assez peu utilisée. Sur certains processeurs assez rares, on peut adresser les registres via une adresse mémoire. Il est vrai que c'est assez rare, et qu'à part quelques vielles architectures ou quelques microcontrôleurs, je n'ai pas d'exemples à donner. Mais c'est tout à fait possible ! C'est le cas du PDP-10.

Adressage des registres via des adresses mémoires.

Les registres adressés implicitement modifier

Les registres de contrôle n'ont pas forcément besoin d'avoir un nom. Par exemple, la gestion de la pile se fait alors via des instructions Push et Pop qui sont les seules à pouvoir manipuler ces registres. Toute manipulation des registres de pile se faisant grâce à ces instructions, on n'a pas besoin de leur fournir un identifiant pour pouvoir les sélectionner. C'est aussi le cas du registre d'adresse d'instruction : sur certains processeurs, il est manipulé automatiquement par le processeur et par les instructions de branchement. C'est aussi le cas pour le Program Counter : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé obligatoirement par les instructions de comparaisons et de test, et certaines opérations arithmétiques.

Dans ces cas bien précis, on n'a pas besoin de préciser le ou les registres à manipuler : le processeur sait déjà quels registres manipuler et comment, de façon implicite. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. Les registres adressés implicitement sont presque toujours des registres de contrôle, beaucoup plus rarement des registres de données. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le Program Counter sont adressables, pareil pour les registres de pile. Inversement, il arrive que certains registres de données puissent être adressés implicitement, notamment certains registres impliqués dans la gestion des adresses mémoire.

La taille des registres architecturaux modifier

Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Les registres principaux en question dépendent de l'architecture. Sur les architectures avec des registres généraux, la taille des registres est celle des registres généraux. Sur les autres architectures, la taille mentionnée est généralement celle des nombres entiers mais les autres registres peuvent avoir une taille totalement différente. Sur les processeurs x86, un registre pour les nombres entiers contient environ 64 bits tandis qu'un registre pour nombres flottants contient entre 80 et 256 bits (suivant les registres utilisés).

Le nombre de bits que peut contenir un registre est parfois différent de la largeur du bus de données (c'est à dire du nombre de bits qui peuvent transiter en même temps sur le bus de données). Exemple : sur les processeurs x86-32 bits, un registre stockant un entier fait 32bits alors que le bus de données peut contenir 64 bits en même temps. Cela a une petite incidence sur la façon dont une donnée est transférée entre la mémoire et un registre. On peut donc se retrouver dans deux situations différentes : soit le bus de données a une largeur égale à la taille d'un registre, soit la largeur du bus de données est plus petite que la taille d'un registre. Dans le premier cas, le bus de données peut charger en une seule fois le nombre de bits que peut contenir un registre. Dans le second cas, on ne peut pas charger le contenu d'un registre en une fois, et on doit charger ce contenu morceau par morceau.

La taille d'un registre est souvent une puissance de deux modifier

Aujourd'hui, les processeurs utilisent presque tous des registres de la même taille que le byte, qui est une puissance de 2 (8, 16, 32, 64, 128, 256, voire 512 bits). Mais cette règle souffre évidemment d'exceptions. Aux tout débuts de l'informatique, certaines machines utilisaient des registres de 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. On peut aussi citer les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaînes HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des registres de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits.

L'usage de bytes qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage. Les problèmes en question surviennent sur les processeurs qui adressent la mémoire par mot (pour rappel, un mot est un groupe de plusieurs bytes). À partir de l'adresse d'un byte, le processeur doit en déduire l'adresse du mot à lire. Et une fois le mot récupéré, le processeur doit décider quel est le byte à prendre en compte. Un mot contenant N bytes, le calcul de l'adresse du mot est une simple division : on divise l'adresse du byte par N. Le reste de la division correspond à la position du byte dans le mot. Avec , la division est un simple décalage et le modulo est un simple ET logique, deux opérations très rapides. Si ce n'est pas le cas, on doit faire une vraie division et un vrai modulo, deux opérations excessivement lentes.

Le système d'aliasing de registres sur les processeurs x86 modifier

Sur les processeurs x86, on trouve des registres de taille différentes. Certains registres sont de 8 bits, d'autres de 16, d'autres de 32, et on a même des registres de 64 bits depuis plus d'une décennie. Limitons-nous pour le moment aux registres 8 et 16 bits, sur lesquels il y a beaucoup de choses à dire. Les premiers processeurs x86 étaient des processeurs 16 bits, mais ils s’inspiraient grandement des processeurs 8008 qui étaient des processeurs 8 bits. Le 8086 et le 8088 étaient en effet des versions améliorées et grandement remaniées des premiers 8008 d'Intel. En théorie, la rétrocompatibilité n'était pas garantie, dans le sens où les jeux d'instruction étaient différents entre le 8086 et le 8008. Mais Intel avait prévu quelques améliorations pour rendre la transition plus facile. Et l'une d'entre elle concerne directement le passage des registres de 8 à 16 bits.

Les registres 16 bits étaient découpés en deux portions de 8 bits, chacune adressable séparément. On pouvait adresser un registre de 16, ou alors adresser seulement les 8 bits de poids fort ou les 8 bits de poids faible. Ces octets étaient considérés comme des registres à part entière. Du moins, c'est le cas pour 4 registres de données, nommés AX, BX, CX et DX. le registre AX fait 16 bits, l'octet de poids fort est adressable comme un registre à part entière nommé AH, alors que l'octet de poids faible est aussi un registre nommé AL (H pour High et L pour Low). idem avec les registres BX, BH et BL, les registres CX, CH et CL, ou encore les registres DX, DH, DL. Pour ces registres, on a un système d'alias de registres, qui permet d'adresser certaines portions d'un registre comme un registre à part entière. Les registres AH, AL, BH, BL, ..., ont tous un nom de registre et peuvent être utilisés dans des opérations arithmétiques, logiques ou autres. Dans ce cas, les opérations ne modifient que l'octet sélectionné, pas le reste du registre. Une même opération peut donc agir sur 16 ou 8 bits suivant le registre sélectionné. Les autres registres ne sont pas concernés par ce découpage, que ce soit les registres de données, d'adresse ou autres.

Registres du 8086, processeur x86 16 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce point du cours, aussi je vais vous demander de les ignorer.

Au départ, l'architecture x86 avait des registres de 16 bits. Puis, par la suite, l'architecture a étendu ses registres à 32 et enfin 64 bits. Mais cela s'est ressentit au niveau des registres, où le système d'alias a été étendu. Les registres 32 bits ont le même système d'alias, mais légèrement modifié. Sur un registre 32 bits, les 16 bits de poids faible sont adressables séparément, mais pas les 16 bits de poids fort. La raison est que les registres 16 bits originaux, présents sur les processeurs x86 16 bits, ont été étendus à 32 bits. Les 16 originaux sont toujours adressables comme avant, avec le même nom de registre que sur les anciens modèles. par contre, le registre étendu a un nouveau nom de registre. Pour rendre tout cela plus clair, voyons l'exemple du registre EAX des processeurs 64 bits. C'est un registre 32 bits, dont les 16 bits de poids faible sont tout simplement le registre AX vu plus haut, ce dernier pouvant être subdivisé en AH et AL. La même chose a lieu pour les registres EBX, ECX et EDX. Chose étonnante, presque tous les registres ont étés étendus ainsi, même le program counter, les registres liés à la pile et quelques autres, notamment pour adresser plus de mémoire.

Registres des processeurs x86 32 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce poitn du cours, aussi je vais vous demander de les ignorer.

Lors du passage au 64 bits, les registres 32 bits ont étés étendus de la même manière, et les registres étendus à 64 bits ont reçu un nom de registre supplémentaire, RAX, RBX, RCX ou RDX. Le passage à 64 bits s'est accompagné de l'ajout de 4 nouveaux registres.

Un point intéressant est qu'Intel a beaucoup utilisé ce système d'alias pour éviter d'avoir à réellement ajouter certains registres. Dans le chapitre sur les architectures à parallélisme de données, nous verrons plusieurs cas assez impressionnants de cela. Pour le moment, bornons-nous à citer les exemples les plus frappants, sans rentrer dans les détails, et parlons du MMX, du SSE et de l'AVX.

Le MMX est une extension du x86, à savoir l'ajout d'instructions particulières au jeu d’instruction x86 de base. Cette extension ajoutait 8 registres appelés MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7, d'une taille de 64 bits, qui ne pouvaient contenir que des nombres entiers. En théorie, on s'attendrait à ce que ces registres soient des registres séparés. Mais Intel utilisa le système d'alias pour éviter d'avoir à rajouter des registres. À la place, il étendit les registres flottants déjà existants, eux-même ajoutés par l'extension x87, qui définit 8 registres flottants de 80 bits. Chaque registre MMX correspondait aux 64 bits de poids faible d'un des 8 registres flottants de la x87 ! Cela posa pas mal de problèmes pour les programmeurs qui voulaient utiliser l'extension MMX. Il était impossible d'utiliser à la fois le MMX et les flottants x87...

Registres AVX.

Par la suite, Intel ajouta une nouvelle extension appelée le SSE, qui ajoutait plusieurs registres de 128 bits, les XMM registers illustrés ci-contre. Le SSE fût décliné en plusieurs versions, appelées SSE1, SSE2, SSE3, SS4 et ainsi de suite, chacune rajoutant de nouvelles instructions. Puis, Intel ajouta une nouvelle extension, l'AVX. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions, et surtout en permettant de traiter des données de 256 bits. Et cette dernière ajoute 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Et c'est là que le système dalias a encore frappé. Les registres AVX sont partagés avec les registres SSE : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. Puis, arriva l'AVX-515 qui ajouta 32 registres de 512 bits, et des instructions capables de les manipuler, d'où son nom. Là encore, les 256 bits de poids faible de ces registres correspondent aux registres de l'AVX précédent. Du moins, pour les premiers 16 registres, vu qu'il n'y a que 16 registres de l'AVX normal.

Pour résumer, ce système permet d'éviter d'ajouter des registres de plus grande taille, en étendant des registres existants pour en augmenter la taille. Ce n'était peut-être pas sa fonction première, du moins sur les processeurs Intel, mais c’est ainsi qu'il a été utilisé à l'excès par Intel. En soi, la technique est intéressante et permet certainement des économies de circuits dignes de ce nom. La longévité des architectures x86 a fait que cette technique a beaucoup été utilisée, l'architecture ayant été étendue deux fois : lors du passage de 16 à 32 bits, puis de 32 à 64 bits. Le système d'extension a aussi été la source de plusieurs usage de l'aliasing de registres. Mais les autres architectures n'implémentent pas vraiment ce système. De plus, ce système marche assez mal avec les processeurs modernes, dont la conception interne se marie mal avec laliasing de registres, pour des raisons que nous verrons plus tard dans ce cours (cela rend plus difficile le renommage de registres et la détection des dépendances entre instructions).

Le pseudo-aliasing des registres sur le processeur Z80 modifier

Le processeur Z80 est très inspiré des premiers processeurs Intel, mais avec un jeu d'instruction légèrement différent. Il a un système de pseudo-aliasing de registres. Formellement, ce n'est pas un système d'alias, mais un système où les registres sont regroupés lors de certaines opérations.

Le Z80 est un processeur 8 bits, qui contient plusieurs registres généraux de 8 bits nommés A, B, C, D, E, F, H, L. Les registres A et F correspondent à l'accumulateur et aux registres d'état, ils ne sont pas concernés par ce qui va suivre. La quasi-totalité des opérations arithmétiques ne manipule que ces registres de 8 bits, mais l'opération d'incrémentation est un peu à part.

Le Z80 supporte 3 paires de registres aliasés. Chaque paire contient deux registres de 8 bits, mais une paire est considérée comme un registre unique de 16 bits pour certaines opérations. Les registres 16 bits sont la paire BC, la paire DE et la paire HL. Le registre BC de 16 bits est composé du registre B de 8 bits et du registre C de 8 bits, et ainsi de suite pour les paires DE et HL. Le système de paires de registres permet d'effectuer une opération d'incrémentation sur une paire complète. Les paires sont concrètement utilisées seulement pour l'incrémentation, avec une instruction spécialisée, qui incrémente le registre de 16 bit d'une paire.

Cela peut paraître étrange, mais c'est en réalité un petit plus qui se marie bien avec le reste de l'architecture. Le Z80 gère des adresses de 16 bits, ce qui signifie que les registres dédiés aux adresses sont de 16 bits. Et cela concerne son pointeur de pile et son program counter, qui sont de 16 bits tous les deux. Aussi, pour mettre à jour le pointeur de pile et le program counter, le processeur incorpore un incrémenteur de 16 bits. Les concepteurs du processeur ont sans doute cherché à rentabiliser cet incrémenteur et lui ont permis d'incrémenter des registres de données. Pour cela, il fallait regrouper les registres de 8 bits par paire de deux, ce qui rendait l’implémentation matérielle la plus simple possible.

Les optimisations liées aux registres architecturaux modifier

Afin d'améliorer les performances, les concepteurs des processeurs ont optimisé les registres architecturaux au mieux. Leur nombre, leur taille, leur adressage : tout est optimisé sur les jeux d'instruction dignes de ce nom. Et sur certains processeurs, on trouve des optimisations assez spéciales, qui visent à dupliquer les registres architecturaux sans que cela se voie dans le jeu d'instruction. Pour le dire autrement, un registre architectural correspond à plusieurs registres physiques dans le processeur.

Les registres dédiés aux interruptions modifier

Sur certains processeurs, les registres généraux sont dupliqués en deux ensembles identiques. Le premier ensemble est utilisé pour exécuter les programmes normaux, alors que le second ensemble est dédié aux interruptions. Mais les noms de registres sont identiques dans les deux ensembles.

Prenons l'exemple du processeur Z80 pour simplifier les explications. Comme dit plus haut, ce processeur a beaucoup de registres et tous ne sont pas dupliqués. Les registres pour la pile ne sont pas dupliqués, le program counter non plus. Par contre, les autres registres, qui contiennent des données, sont dupliqués. Il s'agit des registres nommés A (accumulateur), le registre d'état F et les registres généraux B, C, D, E, H, L, ainsi que les registres temporaires W et Z. L'ensemble de registres pour interruption dispose lui aussi de registres nommés A, F et les registres généraux B, C, D, E, H, L, mais il s'agit de registres différents. Pour éviter les confusions, ils sont notés A', F', B', C', D', E', H', L', mais les noms de registres sont en réalité identiques. Le processeur ne confond pas les deux, car il sait s'il est dans une interruption ou non.

Les deux ensembles de registres sont censés être isolés, il n'est pas censé y avoir d'échanges de données entre les deux. Mais le Z80 permettait d'échanger des données entre les deux ensembles de registres. Dans le détail, une instruction permettait d'échanger le contenu d'une paire de registres avec la même paire dans l'autre ensemble. En clair, on peut échanger les registres BC avec BC', DE avec DE′, et HL avec HL′. Par contre, il est impossible d'échanger le registre A avec A', et F avec F'. Le résultat est que les programmeurs utilisaient l'autre ensemble de registres comme registres pour les programmes, même s'ils n'étaient pas prévus pour.

Ce système permet de simplifier grandement la gestion des interruptions matérielles. Lors d'une interruption sur un processeur sans ce système, l'interruption doit sauvegarder les registres qu'elle manipule, qui sont potentiellement utilisés par le programme qu'elle interrompt. Avec ce système, il n'y a pas besoin de sauvegarder les registres lors d'une interruption, car les registres utilisés par le programme et l'interruption ne sont en réalité pas les mêmes. Les interruptions sont alors plus rapides.

Notons qu'avec ce système, seuls les registres adressables par le programmeur sont dupliqués. Les registres comme le pointeur de pile ou le program counter ne sont pas dupliqués, car ils n'ont pas à l'être. Et attention : certains registres doivent être sauvegardés par l'interruption. Notamment, l'adresse de retour, qui permet de reprendre l'exécution du programme interrompu au bon endroit. Elle est réalisée automatiquement par le processeur.

Le fenêtrage de registres modifier

Le fenêtrage de registres est une amélioration de la technique précédente, qui est adaptée non pas aux interruptions, mais aux appels de fonction/sous-programmes. Là encore, lors de l'appel d'une fonction, on doit sauvegarder les registres du processeur sur la pile, avant qu'ils soient utilisés par la fonction. Plus un processeur a de registres architecturaux, plus leur sauvegarde prend du temps. Et là encore, on peut dupliquer les registres pour éviter cette sauvegarde. Pour limiter le temps de sauvegarde des registres, certains processeurs utilisent le fenêtrage de registres, une technique qui permet d'intégrer cette pile de registre directement dans les registres du processeur.

Le fenêtrage de registres de taille fixe modifier

La technique de fenêtrage de registres la plus simple duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux.

Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres. Ainsi, pas besoin de sauvegarder les registres de cette fenêtre, vu qu'ils étaient vides de toute donnée. S'il ne reste pas de fenêtre inutilisée, on est obligé de sauvegarder les registres d'une fenêtre dans la pile.

Fenêtre de registres.

Les interruptions peuvent aussi utiliser le fenêtrage de registres. Lorsqu'une interruption se déclenche, elle se voit allouer sa propre fenêtre de registres.

Le fenêtrage de registres de taille variable modifier

L'implémentation précédente a des fenêtres de taille fixe, qui sont toutes isolées les unes des autres. C'était le fenêtrage de registre implémenté sur le processeur Berkeley RISC, et quelques autres processeurs. Des techniques similaires permettent cependant d'avoir des fenêtres de taille variable !

Avec des fenêtres de registre de taille variable, chaque fonction peut réserver un nombre de registres différent des autres fonctions. Une fonction peut réserver 5 registres, une autre 8, une autre 6, etc. Par contre, il y a une taille maximale. Les registres du processeur se comportent alors comme une pile de registres. Un exemple est celui du processeur AMD 29000, qui implémente des fenêtres de taille variable, chaque fenêtre pouvant aller de 1 à 8 registres. C'était la même chose sur les processeurs Itanium.

Il faut noter que les processeurs Itanium et AMD 29000 n'utilisaient le fenêtrage de registres que sur une partie des registres généraux. Par exemple, l'AMD 29000 disposait de 196 registres, soit 64 + 128. Les registres sont séparés en deux groupes : un de 64, un autre de 128. Les 128 registres sont ceux avec le fenêtrage de registres, à savoir qu'ils forment une pile de registres utilisée pour les appels de fonction. Les 64 registres restants sont des registres généraux normaux, où le fenêtrage de registre ne s'applique pas, et qui est accessible par toute fonction. Cette distinction entre pile de registres (avec fenêtrage) et registres globaux (sans fenêtrage) existe aussi sur l'Itanium, qui avait 32 registres globaux et 96 registres avec fenêtrage.

Le renommage de registre modifier

Les processeurs récents utilisent des techniques de renommage de registre, que l'on ne peut pas aborder pour le moment (par manque de concepts importants). Mais d'autres techniques similaires sur le principe sont possibles, comme le fenêtrage de registres.


Pour rappel, les programmes informatiques exécutés par le processeur sont placés en mémoire RAM, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire sous la forme de suites de bits, tout comme les données. La seule différence est que les instructions sont chargées via le program counter, alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. Mais il est très rare que le processeur charge et exécute des données, qu'il prend par erreur pour des instructions. Cela demanderait en effet que le program counter ne fasse pas ce qui est demandé, ou que le programme exécuté soit bugué, voire qu'il soit conçu pour.

Toujours est-il qu'une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la taille de l'instruction. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement.

Un bon exemple de processeur à instruction de longueur variable est celui du jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes. Cela expliquent qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. Un autre exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. N'en parlons pas plus, l'encodage de ces instructions est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres !

L'opcode et l'encodage des modes d'adressage modifier

Une instruction n'est pas encodée n'importe comment et la suite de bits associée a une certaine structure. Quelques bits de l’instruction indiquent quelle est l'opération à effectuer : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Cette portion de mémoire s'appelle l'opcode. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. De manière générale, on peut dire qu'il existe autant d'opcode que d'instructions pour un processeur. Évidemment, qui dit beaucoup d'opcodes dit processeur plus complexe : les circuits de gestion des opcodes sont naturellement plus complexes quand ces opcodes sont nombreuses. Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes.

Il arrive que certaines instructions soient composées d'un opcode, sans rien d'autre : elles ont alors une représentation en binaire qui est unique. Mais certaines instructions ajoutent une partie variable, pour préciser la localisation des données à manipuler. Une instruction peut alors fournir au processeur ce qu'on appelle une référence, à savoir quelque chose qui permet de localiser une donnée dans la mémoire. Cette référence pourra ainsi préciser plus ou moins explicitement dans quel registre, à quelle adresse mémoire, à quel endroit sur le disque dur, se situe la donnée à manipuler. Elles indiquent où se situent les opérandes d'un calcul, où stocker son résultat, où se situe la donnée à lire ou écrire, à quel l'endroit brancher. Il faut préciser que toutes les références n'ont pas la même taille : une adresse utilisera plus de bits qu'un nom de registres, par exemple (il y a moins de registres que d'adresses). Par exemple, une instruction de calcul dont les deux références sont des adresse mémoire prendra plus de place qu'un calcul qui manipule deux registres.

Reste à savoir quelle est la nature de la référence : est-ce une adresse, un nombre, un nom de registre ? Chaque manière d’interpréter la partie variable s'appellent un mode d'adressage. Pour résumer, un mode d'adressage indique au processeur que telle référence est une adresse, un registre, autre chose. Il est possible qu'une instruction précise plusieurs références, qui sont chacune soit une adresse, soit une donnée, soit un registre. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser une opérande pour chaque (dans le pire des cas). Les instructions manipulant plusieurs références peuvent parfois utiliser un mode d'adressage différent pour chaque. Comme nous allons le voir, certaines instructions supportent certains modes d'adressage et pas d'autres. Généralement, les instructions d'accès mémoire possèdent plus de modes d'adressage que les autres, encore que cela dépende du processeur (chose que nous détaillerons dans le chapitre suivant).

Il existe deux méthodes pour préciser le mode d'adressage utilisé par l'instruction. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, toutes les instructions arithmétiques ne peuvent manipuler que des registres. Dans un cas pareil, pas besoin de préciser le mode d'adressage, qui est déduit automatiquement via l'opcode: on parle de mode d'adressage implicite. Dans certains cas, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de mode d'adressage explicite. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal.

Exemple d'une instruction avec mode d'adressage explicite.

Les modes d'adressages pour les données modifier

Pour comprendre un peu mieux ce qu'est un mode d'adressage, voyons quelques exemples de modes d'adressages assez communs et qui reviennent souvent. Nous allons commencer par aborder l'adressage des données et les modes d'adressages qui correspondent.

L'adressage implicite modifier

Avec l'adressage implicite, la partie variable n'existe pas ! Il peut y avoir plusieurs raisons à cela. Il se peut que l'instruction n'ait pas besoin de données : une instruction de mise en veille de l'ordinateur, par exemple. Ensuite, certaines instructions n'ont pas besoin qu'on leur donne la localisation des données d'entrée et « savent » où sont les données. Comme exemple, on pourrait citer une instruction qui met tous les bits du registre d'état à zéro. Pareil pour les instructions manipulant la pile : on sait d'avance dans quels registres sont stockées l'adresse de la base ou du sommet de la pile.

L'adressage immédiat modifier

Avec l'adressage immédiat, la partie variable est une constante : un nombre entier, un caractère, un nombre flottant, etc. Avec ce mode d'adressage, la donnée est placée dans la partie variable et est chargée en même temps que l'instruction.

Adressage immédiat

Les constantes en adressage immédiat sont souvent codées sur 8 ou 16 bits. Aller au-delà serait inutile vu que la quasi-totalité des constantes manipulées par des opérations arithmétiques sont très petites et tiennent dans un ou deux octets. La plupart du temps, les constantes sont des entiers signés, c'est à dire qui peuvent être positifs, nuls ou négatifs. Au vu de la différence de taille entre la constante et les registres, les constantes subissent une opération d'extension de signe avant d'être utilisées.

Pour rappel, l'extension de signe convertit un entier en un entier plus grand, codé sur plus de bits, tout en préservant son signe et sa valeur. L'extension de signe des nombres positifs consiste à remplir les bits de poids fort avec des 0 jusqu’à arriver à la taille voulue : c'est la même chose qu'en décimal, où rajouter des zéros à gauche d'un nombre positif ne changera pas sa valeur. Pour les nombres négatifs, il faut remplir les bits à gauche du nombre à convertir avec des 1, jusqu'à obtenir le bon nombre de bits : par exemple, 1000 0000 (-128 codé sur 8 bits) donnera 1111 1111 1000 000 après extension de signe sur 16 bits. L'extension de signe d'un nombre codé en complément à 2 se résume donc en une phrase : il faut recopier le bit de poids fort de notre nombre à convertir à gauche de celui-ci jusqu’à atteindre le nombre de bits voulu.

L'adressage direct modifier

Passons maintenant à l'adressage absolu, aussi appelé adressage direct. Avec lui, la partie variable est l'adresse de la donnée à laquelle accéder. Cela permet de lire une donnée directement depuis la mémoire sans devoir la copier dans un registre.

Adressage direct

L'adressage inhérent modifier

Avec le mode d'adressage inhérent, la partie variable va identifier un registre qui contient la donnée voulue. Ce mode d'adressage demande d'attribuer un nom de registre à chaque registre. Pour rappel, ce dernier est un numéro attribué à chaque registre, utilisé pour préciser à quel registre le processeur doit accéder.

Adressage inhérent

L'adressage indirect à registre modifier

Dans certains cas, les registres généraux du processeur peuvent stocker des adresses mémoire. On peut alors décider d'accéder à l'adresse qui est stockée dans un registre : c'est le rôle du mode d'adressage indirect à registre. Ici, la partie variable identifie un registre contenant l'adresse de la donnée voulue. La différence avec le mode d'adressage inhérent vient de ce qu'on fait de ce nom de registre : avec le mode d'adressage inhérent, le registre indiqué dans l'instruction contiendra la donnée à manipuler, alors qu'avec le mode d'adressage indirect à registre, le registre contiendra l'adresse de la donnée.

Adressage indirects à registre

Ce mode d'adressage indirect à registre permet d'implémenter de façon simple ce qu'on appelle les pointeurs. Il s'agit de fonctionnalités de certains langages de programmation dits bas-niveau (proches du matériel), dont le C. Les pointeurs sont des variables dont le contenu est une adresse mémoire. Cette définition est certes simple, mais beaucoup d'étudiants la trouve très abstraite et ne voient pas à quoi ces variables peuvent servir. Dans les grandes lignes, ils servent dès que l'on manipule/crée des structures de données, peu importe le langage de programmation utilisé. C'est explicite dans des langages comme le C, mais implicite dans les langages haut-niveau. Manipuler des tableaux, des listes chainées, des arbres, ou tout autre structure de donnée un peu complexe, se fait à grand coup de pointeurs. C'est surtout le cas dans les structures de données où les données sont dispersées dans la mémoire, comme les listes chaînées, les arbres, et toute structure éparse. Localiser les données en question dans la mémoire demande d'utiliser des pointeurs qui pointent vers ces données, qui donnent leur adresse.

L'adressage indirect à registre avec auto-incrément modifier

Pour faciliter ces parcours de tableaux, on a inventé les modes d'adressages indirect avec auto-incrément (register indirect autoincrement) et indirect avec auto-décrément (register indirect autodecrement), des variantes du mode d'adressage indirect qui augmentent ou diminuent le contenu du registre d'une valeur fixe automatiquement. Cela permet de passer directement à l’élément suivant ou précédent dans un tableau. Il existe une variante où l'incrémentation ou la décrémentation s'effectuent avant l'utilisation effective de l'adresse.

Adressage indirect à registre post-incrémenté

L'adressage indexed absolute modifier

D'autres modes d'adressage permettent de faciliter le calcul de l'adresse d'un élément du tableau. Pour éviter d'avoir à calculer les adresses à la main avec le mode d'adressage indirect à registre, on a inventé un mode d'adressage pour combler ce manque : le mode d'adressage absolu indexé (indexed absolute, ou encore base+offset). Celui-ci fournit l'adresse de base du tableau, et un registre qui contient l'indice. À partir de ces deux données, l'adresse de l’élément du tableau est calculée, envoyée sur le bus d'adresse, et l’élément est récupéré.

Indexed Absolute

Ce mode d'adressage indexed absolute ne marche que pour des tableaux dont l'adresse est fixée une bonne fois pour toute. Ces tableaux sont assez rares : ils correspondent aux tableaux de taille fixe, déclarée dans la mémoire statique (souvenez-vous de la section précédente).

L'adressage base + index modifier

La majorité des tableaux sont des tableaux dont l'adresse n'est pas connue lors de la création du programme : ils sont déclarés sur la pile ou dans le tas, et leur adresse varie à chaque exécution du programme. On peut certes régler ce problème en utilisant du code automodifiant, mais ce serait vendre son âme au diable ! Pour contourner les limitations du mode d'adressage indexed absolute, on a inventé le mode d'adressage base + index.

Avec ce dernier, l'adresse du début du tableau n'est pas stockée dans l'instruction elle-même, mais dans un registre. Elle peut donc varier autant qu'on veut. Ce mode d'adressage spécifie deux registres dans sa partie variable : un registre qui contient l'adresse de départ du tableau en mémoire, le registre de base, et un qui contient l'indice, le registre d'index. Le processeur calcule alors l'adresse de l’élément voulu à partir du contenu de ces deux registres, et accède à notre élément. En clair : notre instruction ne fait pas que calculer l'adresse de l’élément : elle va aussi le lire ou l'écrire.

Base + Index

Ce mode d'adressage possède une variante qui permet de vérifier qu'on ne « déborde » pas du tableau, en calculant par erreur une adresse en dehors du tableau, à cause d'un indice erroné, par exemple. Accéder à l’élément 25 d'un tableau de seulement 5 éléments n'a pas de sens et est souvent signe d'une erreur. Pour cela, l'instruction peut prendre deux opérandes supplémentaires (qui peuvent être constants ou placés dans deux registres). L'instruction BOUND sur le jeu d'instruction x86 en est un exemple. Si cette variante n'est pas supportée, on doit faire ces vérifications à la main.

L'adressage base + décalage modifier

Outre les tableaux, les programmeurs utilisent souvent ce qu'on appelle des structures. Ces structures servent à créer des données plus complexes que celles que le processeur peut supporter. Mais le processeur ne peut pas manipuler ces structures : il est obligé de manipuler les données élémentaires qui la constituent une par une. Pour cela, il doit calculer leur adresse, ce qui n'est pas très compliqué. Une donnée a une place prédéterminée dans une structure : elle est donc a une distance fixe du début de celle-ci.

Calculer l'adresse d'un élément d'une structure se fait donc en ajoutant une constante à l'adresse de départ de la structure. Et c'est ce que fait le mode d'adressage base + décalage. Celui-ci spécifie un registre qui contient l'adresse du début de la structure, et une constante. Ce mode d'adressage effectue ce calcul, et lit ou écrit la donnée adressée.

Base + offset

L'adressage base + index + décalage modifier

Certains processeurs vont encore plus loin : ils sont capables de gérer des tableaux de structures ! Ce genre de prouesse est possible grâce au mode d'adressage base + index + décalage. Avec ce mode d'adressage, on peut calculer l'adresse d'une donnée placée dans un tableau de structure assez simplement : on calcule d'abord l'adresse du début de la structure avec le mode d'adressage base + index, et ensuite on ajoute une constante pour repérer la donnée dans la structure. Et le tout, en un seul mode d'adressage.

Les modes d'adressage pour les adresses de destination des branchements modifier

Les modes d'adressage des branchements permettent de donner l'adresse de destination du branchement, l'adresse vers laquelle le processeur reprend son exécution si le branchement est pris. Les instructions de branchement peuvent avoir plusieurs modes d'adressages : implicite, direct, relatif ou indirect. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit dans un registre du processeur (branchement indirect), soit calculée à l’exécution (relatif), soit précisée de manière implicite (retour de fonction, adresse sur la pile).

Les branchements implicites modifier

Les branchements implicites se limitent aux instructions de retour de fonction, où l'adresse de destination est située au sommet de la pile d'appel.

Les branchements directs modifier

Avec un branchement direct, l'opérande est simplement l'adresse de l'instruction à laquelle on souhaite reprendre.

Branchement direct.

Les branchements relatifs modifier

Les branchements relatifs permettent de localiser la destination d'un branchement par rapport à l'instruction en cours. Cela permet de dire « le branchement est 50 instructions plus loin ». Avec eux, l'opérande est un nombre qu'il faut ajouter au registre d'adresse d'instruction pour tomber sur l'adresse voulue. On appelle ce nombre un décalage (offset).

Branchement relatif

Les branchements indirects modifier

Avec les branchements indirects, l'adresse vers laquelle on souhaite brancher peut varier au cours de l’exécution du programme. Ces branchements sont souvent camouflés dans des fonctionnalités un peu plus complexes des langages de programmation (pointeurs sur fonction, chargement dynamique de bibliothèque, structure de contrôle switch, et ainsi de suite). Avec ces branchements, l'adresse vers laquelle on veut brancher est stockée dans un registre.

Branchement indirect

Les modes d'adressage pour les conditions (pour les branchements et instructions à prédicats) modifier

Pour rappel, les instructions à prédicats et les branchements s’exécutent si une certaine condition est remplie. Pour rappel, on peut faire face à deux cas. Dans le premier, le branchement et l'instruction de test sont fusionnés en une seule instruction. Dans le second, la condition en question est calculée par une instruction de test séparée du branchement. Dans les deux cas, on doit préciser quelle est la condition qu'on veut vérifier. Cela peut se faire de différentes manières, mais la principale est de numéroter les différentes conditions et d'incorporer celles-ci dans l'instruction de test ou le branchement. Un second problème survient quand on a une instruction de test séparée du branchement. Le résultat de l'instruction de test est mémorisé soit dans un registre de prédicat (un registre de 1 bit qui mémorise le résultat d'une instruction de test), soit dans le registre d'état. Les instructions à prédicats et les branchements doivent alors préciser où se trouve le résultat de la condition adéquate, ce qui demande d'utiliser un mode d'adressage spécialisé.

Pour résumer peut faire face à trois possibilités :

  • soit le branchement et le test sont fusionnés et l'adressage est implicite ;
  • soit l'instruction de branchement doit préciser le registre à prédicat adéquat ;
  • soit l'instruction de branchement doit préciser le bon bit dans le registre d'état.

L'adressage des registres à prédicats modifier

La première possibilité est celle où les instructions de test écrivent leur résultat dans un registre à prédicat, qui est ensuite lu par le branchement. De tels processeurs ont généralement plusieurs registres à prédicats, chacun étant identifié par un nom de registre spécialisé. Les noms de registres pour les registres à prédicats sont séparés des noms des registres généraux/entiers/autres. Par exemple, on peut avoir des noms de registre à prédicats codés sur 4 bits (16 registres à prédicats), alors que les noms pour les autres registres sont codés sur 8 bits (256 registres généraux).

La distinction entre les deux se fait sur deux points : leur place dans l'instruction, et le fait que seuls certaines instructions utilisent les registres à prédicats. Typiquement, les noms de registre à prédicats sont utilisés uniquement par les instructions de test et les branchements. Ils sont utilisés comme registre de destination pour les instructions de test, et comme registre source (à lire) pour les branchements et instructions à prédicats. De plus, ils sont placés à des endroits très précis dans l'instruction, ce qui fait que le décodeur sait identifier facilement les noms de registres à prédicats des noms des autres registres.

L'adressage du registre d'état modifier

La seconde possibilité est rencontrée sur les processeurs avec un registre d'état. Sur ces derniers, le registre d'état ne contient pas directement le résultat de la condition, mais celle-ci doit être calculée par le branchement ou l'instruction à prédicat. Et il faut alors préciser quels sont le ou les bits nécessaires pour connaitre le résultat de la condition. En conséquence, cela ne sert à rien de numéroter les bits du registre d'état comme on le ferais avec les registres à prédicats. A la place, l'instruction précise la condition à tester, que ce soit l'instruction de test ou le branchement. Et cela peut être fait de manière implicite ou explicite.

La première possibilité est d'indiquer explicitement la condition à tester dans l'instruction. Pour cela, les différentes conditions possibles sont numérotées, et ce numéro est incorporé dans l'instruction de branchement. L'instruction de branchement contient donc un opcode, une adresse de destination ou une référence vers celle-ci, puis un numéro qui indique quelle condition tester. Un exemple assez intéressant est l'ARM1, le tout premier processeur de marque ARM. Sur l'ARM1, le registre d'état est mis à jour par une opération de comparaison, qui est en fait une soustraction déguisée. L'opération de comparaison soustrait deux opérandes A et B, met à jour le registre d'état en fonction du résultat, mais n'enregistre pas ce résultat dans un registre et s'en débarrasse. Le registre d'état est un registre contenant 4 bits appelés N, Z, C et V : Z indique que le résultat de la soustraction vaut 0, N indique qu'il est négatif, C indique que le calcul a donné un débordement d'entier non-signé, et V indique qu'un débordement d'entier signé. Avec ces 4 bits, on peut obtenir 16 conditions possibles, certaines indiquant que les deux nombres sont égaux, différents, que l'un est supérieur à l'autre, inférieur, supérieur ou égal, etc. L'instruction précise laquelle de ces 16 conditions est nécessaire : l'instruction s’exécute si la condition est remplie, ne s’exécute pas sinon. Voici les 16 conditions possibles :

Code fournit par l’instruction Test sur le registre d'état Interprétation
0000 Z = 1 Les deux nombres A et B sont égaux
0001 Z = 0 Les deux nombres A et B sont différents
0010 C = 1 Le calcul arithmétique précédent a généré un débordement non-signé
0011 C = 0 Le calcul arithmétique précédent n'a pas généré un débordement non-signé
0100 N = 1 Le résultat est négatif
0101 N = 0 Le résultat est positif
0110 V = 1 Le calcul arithmétique précédent a généré un débordement signé
0111 V = 0 Le calcul arithmétique précédent n'a pas généré de débordement signé
1000 C = 1 et Z = 0 A > B si A et B sont non-signés
1001 C = 0 ou Z = 1 A <= B si A et B sont non-signés
1010 N = V A >= B si on calcule A - B
1011 N != V A < B si on calcule A - B
1100 Z = 0 et ( N = V ) A > B si on calcule A - B
1101 Z = 1 ou ( N = 1 et V = 0 ) ou ( N = 0 et V = 1 ) A <= B si on calcule A - B
1110 L'instruction s’exécute toujours (pas de prédication).
1111 L'instruction ne s’exécute jamais (NOP).

La seconde possibilité est celle de l'adressage implicite du registre d'état. C'est le cas sur les processeurs x86, où il y a plusieurs instructions de branchements, chacune calculant une condition à partir des bits du registre d'état. Le registre d'état est similaire à celui de l'ARM1 vu plus haut. Le registre d'état des CPU x86 contient 5 bits : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, d'autres une combinaison de plusieurs bits.

Instruction de branchement Bit du registre d'état testé Condition testée si on compare deux nombres A et B avec une instruction de test
JS (Jump if Sign) N = 1 Le résultat est négatif
JNS (Jump if not Sign) N = 0 Le résultat est positif
JO (Jump if Overflow) SF = 1 ou Le calcul arithmétique précédent a généré un débordement signé
JNO (Jump if Not Overflow) SF = 0 Le calcul arithmétique précédent n'a pas généré de débordement signé
JNE (Jump if Not equal) Z = 1 Les deux nombres A et B sont égaux
JE (Jump if Equal) Z = 0 Les deux nombres A et B sont différents
JB (Jump if below) C = 1 A < B, avec A et B non-signés
JAE (Jump if Above or Equal) C = 0 A >= B, avec A et B non-signés
(JBE) Jump if below or equal C = 1 ou Z = 0 A >= B si A et B sont non-signés
JA (Jump if above) C = 0 et Z = 0 A > B si A et B sont non-signés
JL (Jump if less) SF != OF si A < BA et B sont signés
JGE (Jump if Greater or Equal) SF = OF si A >= BA et B sont signés
JLE (Jump if less or equal) SF != OF OU ZF = 1 si A <= BA et B sont signés
JGE (Jump if Greater) SF = OF OU ZF = 0 si A > B et B sont signés


Les instructions d'un processeur dépendent fortement du processeur utilisé. La liste de toutes les instructions qu'un processeur peut exécuter s'appelle son jeu d'instructions. Celui-ci définit les instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les anciens macintoshs (la génération de macintosh produits entre 1994 et 2006) utilisaient un jeu d'instruction différent : le PowerPC (depuis 2006, les macintoshs utilisent un processeur X86). Mais les architectures x86 et Power PC ne sont pas les seules au monde : il existe d'autres types d'architectures qui sont très utilisées dans le monde de l’informatique embarquée et dans tout ce qui est tablettes et téléphones portables derniers cris. On peut citer notamment les architectures ARM, MIPS et SPARC. Pour résumer, il existe différents jeux d'instructions, que l'on peut classer suivant divers critères.

L'adressage des opérandes modifier

La première classification que nous allons voir est basée sur l'adressage des opérandes. Le critère de distinction est les modes d'adressage autorisés pour les opérandes. Les accès mémoire et branchements ne sont pas impliqués dans cette classification. La raison à cela est que les branchements ont des modes d'adressages dédiés, tandis que les accès mémoire ont besoin de beaucoup de modes d'adressage pour faire leur travail. Tel n'est pas le cas des instructions de calculs, qui peuvent utiliser un nombre limité de modes d'adressage sans le moindre problèmes. Certaines architectures limitent les modes d'adressages pour les opérandes, afin de simplifier le processeur ou le travail du compilateur. D'autres ont utilisent beaucoup de modes d'adressage, dans un souci de flexibilité ou de performances. Bref, voyons comment les différents types d'architectures gèrent les modes d'adressages pour les opérandes.

Dans les grandes lignes, on trouve trois catégories principales : les architectures mémoire-mémoire, les architectures à registres et les architectures FIFO/LIFO. Dans les architectures mémoire-mémoire, les opérandes sont lues directement depuis la mémoire RAM, sans intermédiaires. Pour les architectures à registres, le processeur stocke ses opérandes dans des registres. Enfin, les architectures à pile et à file utilisent une mémoire FIFO ou LIFO pour stocker les opérandes.

Classe d'architecture Intermédiaire entre la mémoire RAM et le reste du processeur
Architecture mémoire-mémoire Aucun. Les opérandes sont lues en RAM et les résultats y sont aussi écrits
Architecture à registres Registres. Les opérandes et résultats sont placés dans des registres.
Architecture à pile/à file Mémoire FIFO/LIFO. Les opérandes et résultats sont placés dans une mémoire FIFO ou LIFO.

Les architectures à registres sont subdivisées en sous-catégories : les architectures LOAD-STORE, les architectures à accumulateurs et les architectures à registre générales. Pour résumer, nous allons parler des architectures suivantes : les machines à pile, les machines à file, les architectures à accumulateur, les architectures mémoire-mémoire et les architectures à registres.

Les architectures mémoire-mémoire modifier

Architecture mémoire-mémoire.

Les toutes premières machines n'avaient pas de registres pour les données et ne faisaient que manipuler la mémoire RAM ou ROM : on parle d'architectures mémoire-mémoire. Dans cette architecture ci, il n'y a pas de registres généraux : les instructions n'accèdent qu'à la mémoire principale. Néanmoins, les registres d'instruction et pointeur d'instruction existent toujours. Les seules opérandes possibles pour ces processeurs sont des adresses mémoire, ce qui fait qu'un mode d'adressage est très utilisé : l'adressage absolu.

Ces architectures avaient l'avantage d'avoir une gestion de la mémoire assez simple. Le nombre d'instruction d'accès mémoire était assez important, en raison de l'absence de registres, chose essentielle sur les architectures modernes. Ce genre d'architectures est aujourd'hui tombé en désuétude depuis que la mémoire est devenue très lente comparé au processeur.

Les architectures à registres modifier

Les architectures mémoire-mémoire ont un défaut rédhibitoire : elles n'ont pas de registres pour stocker leurs opérandes. La conséquence est que les performances sont mauvaises, la RAM étant assez lente par rapport aux registres du processeur. Et encore une fois, les chercheurs et ingénieurs ont inventé des architectures qui résolvent ce problème : les architectures à registres. Celles-ci possèdent des registres qui permettent de conserver temporairement une opérande destinée à être utilisée souvent, ou des résultats de calculs temporaires. Il en existe plusieurs sous-types, qui se distinguent par leur nombre de registres pour les opérandes et par leurs modes d'adressages.

Les architectures à accumulateur modifier

Architecture à accumulateur.

Les architectures à accumulateur sont des architectures à registres avec un unique registre pour les opérandes, appelé l'accumulateur. L'accumulateur mémorise une opérande et le résultat de l'instruction est automatiquement mémorisé dans l'accumulateur. Si l'instruction manipule plusieurs opérandes, les opérandes qui ne sont pas dans l'accumulateur sont lus depuis la mémoire ou des registres séparés de l'accumulateur. L'accumulateur est adressé grâce au mode d'adressage implicite, de même que le résultat de l'opération. Par contre, les autres opérandes sont localisées avec d'autres modes d'adressage : absolu pour le cas le plus fréquent, inhérent (à registre) sur certaines architectures, indirect à registre pour d'autres, etc.

Historiquement, les premières architectures à accumulateur ne contenaient aucun autre registre que l'accumulateur. Toutes les opérandes hors-accumulateur étaient lues en mémoire. Sur ces processeurs, les modes d'adressages supportés étaient les modes d'adressages implicite, absolus, et immédiat. Ces architectures sont parfois appelées architectures 1-adresse, pour une raison simple : la majorité des instructions manipulent deux opérandes, ce qui fait qu'elles devaient lire une opérande depuis la RAM. Pour ces opérations, le résultat ainsi qu'une des opérandes sont stockés dans l'accumulateur, et adressés de façon implicite, seule la seconde opérande étant adressée directement.

Avec ces seuls modes d'adressages, l'utilisation de tableaux ou de structures était un véritable calvaire. Pour améliorer la situation, les processeurs à accumulateurs ont alors incorporés des registres d'Index, pour faciliter les calculs d'adresse mémoire. Ils étaient capables de stocker des indices de tableaux, ou des constantes permettant de localiser une donnée dans une structure. Au départ, ces processeurs n'utilisaient qu'un seul registre d'Index, accessible et modifiable via des instructions spécialisées, qui se comportait comme un second accumulateur spécialisé dans les calculs d'adresses mémoire. Les modes d'adressages autorisés restaient les mêmes qu'avec une architecture à accumulateur normale. La seule différence, c'est que le processeur contenait de nouvelles instruction capables de lire ou d'écrire une donnée dans/depuis l'accumulateur, qui utilisaient ce registre d'Index de façon implicite. Mais avec le temps, nos processeurs finirent par incorporer plusieurs de ces registres. Nos instructions de lecture ou d'écriture devaient alors préciser quel registre d'Index utiliser. Le mode d'adressage Indexed Absolute vit le jour. Les autres modes d'adressages, comme le mode d'adressage Base + Index ou indirects à registres étaient plutôt rares à l'époque et étaient difficiles à mettre en œuvre sur ce genre de machines.

Ensuite, ces architectures s’améliorèrent un petit peu : on leur ajouta des registres capables de stocker des données. L’accumulateur n'était plus seul au monde. Mais attention : ces registres ne peuvent servir que d’opérande dans une instruction, et le résultat d'une instruction ira obligatoirement dans l'accumulateur. Ces architectures supportaient donc le mode d'adressage inhérent.

Les architectures à registres modifier

Architecture à registres.

Les processeurs à registres peuvent stocker temporairement des données dans des registres généraux ou spécialisés. Pour échanger des données entre la mémoire et les registres, on peut utiliser une instruction à tout faire : MOV. Sur d'autres, on utilise des instructions séparées pour copier une donnée de la mémoire vers un registre (LOAD), copier le contenu d'un registre dans un autre, copier le contenu d'un registre dans la mémoire RAM (STORE), etc.

Ces architectures à registres généraux (ainsi que les architectures Load-Store qu'on verra juste après) sont elles-même divisées en deux sous-classes bien distinctes : les architectures 2 adresses et les architectures 3 adresses. Cette distinction entre architecture 2 et 3 adresses permet de distinguer les modes d'adressages des opérations arithmétiques manipulant deux données : additions, multiplications, soustraction, division, etc. Ces instructions ont donc besoin d'adresser deux données différentes, et de stocker le résultat quelque part. Il leur faut donc préciser trois opérandes dans le résultat : la localisation des deux données à manipuler, et l'endroit où ranger le résultat.

  • Sur les architectures à deux adresses, le résultat d'une instruction est stocké à l'endroit indiqué par la référence du premier opérande : cette donnée sera remplacée par le résultat de l'instruction. Avec cette organisation, les instructions ne précisent que deux opérandes. Mais la gestion des instructions est moins souple, vu qu'un opérande est écrasé. Avec cette organisation, les instructions sont plus courtes.
  • Sur les architectures à trois adresses, on peut préciser le registre de destination du résultat. Ce genre d'architectures permet une meilleure utilisation des registres, mais les instructions deviennent plus longues que sur les architectures à deux adresses.

Les architectures LOAD-STORE modifier

Les architectures LOAD-STORE sont identiques aux architectures à registres à un détail près : les instructions arithmétiques et logiques ne peuvent aller chercher leurs données que dans des registres du processeurs. Dit autrement, seules les instructions d'accès mémoire peuvent accéder à la mémoire, les autres instructions n’accédant pas directement à la mémoire. En conséquence, ces instructions ne peuvent prendre que des noms de registres ou des constantes comme opérandes : cela n'autorise que les modes d'adressage immédiat et à registre. Il faut noter aussi que les architectures Load-store sont elles aussi classées en architectures à 2 ou 3 adresses, comme les architectures à registres.

Architecture LOAD-STORE.

Les architectures à pile et à file modifier

Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.

Les dernières architectures que nous allons voir sont les machines à pile et les machines à file des jeux d'instructions où les registres sont remplacés par une mémoire tampon dédiée aux opérandes de calculs. Il s'agit d'une mémoire de type FIFO pour une machine à file, alors que c'est une LIFO pour une machine à pile. La FIFO/LIFO est placée dans le processeur sur certaines machines, alors que d'autres la placent dans la mémoire RAM. Dans le premier cas, la mémoire tampon sert d'intermédiaire entre la mémoire RAM et le reste du processeur. Dans les deux cas, les opérandes sont stockées dans la mémoire tampon dans leur ordre d'ajout et chaque opérande prend exactement un byte, pas plus ni moins. Les opérandes sont chargées par certaines instructions dans la FIFO/LIFO et y sont placées les unes à la suite des autres. De plus, quand le processeur fait un calcul quelconque, le résultat est ajouté à la pile/file. La pile/file se remplit donc progressivement d'opérandes chargées volontairement ou de résultats de calculs. Les seules opérandes manipulables par une instruction sont placées au sommet de la pile ou file : il faut ajouter les opérandes unes par unes avant d'exécuter l'instruction. L'instruction dépile automatiquement les opérandes qu'elle utilise et empile son résultat.

Machines LIFO et FIFO.
Adressage des opérandes sur une machine à pile.

Sur certaines machines FIFO/LIFO, la pile et la file sont localisées non pas dans le processeur, mais dans la mémoire RAM. Ces machines ont besoin d'un registre pour stocker l'adresse du sommet de la pile/file. Dans le cas d'une machine à pile, celui-ci est appelée le stack pointer, ou pointeur de pile. Sur les machines à file, il est appelé le queue pointer, ou encore le pointeur de file. Les instructions de calcul utilisent uniquement les registres de pile pour savoir où se trouvent les opérandes. En clair, toutes les instructions utilisent uniquement le mode d'adressage implicite. Il va de soit que pour simplifier la conception du processeur, celui-ci peut contenir des registres internes pour stocker temporairement les opérandes des calculs, mais ces registres ne sont pas accessibles au programmeur : ce ne sont pas des registres architecturaux. Par exemple, sur certaines machines à pile, tout ou partie de la pile est stockée directement dans des registres internes au processeur, pour gagner en performance.

Les instructions des machines à pile modifier

Ces jeux d’instructions ont des instructions pour ajouter des opérandes dans la FIFO/LIFO ou en retirer. Dans ce qui va suivre, nous allons prendre l'exemple d'une machine à pile, mais il existe des équivalents pour les machines à file. Dans les grandes lignes, il y a deux instructions principales nommées PUSH et POP pour l'ajout et le retrait d'opérandes dans la pile, ainsi que quelques instructions annexes sur certaines machines. Les machines à file possèdent des instructions équivalentes, à savoir deux instructions Queue et Unqueue pour ajouter ou retirer une opérande dans la file, ainsi que quelques instructions annexes. Voici les deux instructions principales de gestion de la pile, présentes sur toutes les architectures à pile :

  • L'instruction PUSH permet d'empiler une donnée. Elle prend l'adresse de la donnée à empiler, charge la donnée, et met à jour le pointeur de pile.
  • L'instruction POP dépile la donnée au sommet de la pile, la stocke à l'adresse indiquée dans l'instruction, et met à jour le pointeur de pile.
Instruction Push.
Instruction Pop.

Vu qu'une instruction dépile ses opérandes, on ne peut pas les réutiliser. Ceci dit, certaines instructions ont été inventées pour limiter la casse : on peut notamment citer l'instruction DUP, qui copie le sommet de la pile en deux exemplaires. On peut aussi citer l'instruction SWAP, qui échange deux données dans la pile.

Instruction Dup.
Instruction Swap.

Enfin, on trouve des instructions pour sauvegarder la totalité de la pile ou de la file. Ces instructions servent surtout pour les appels de fonctions/sous-programmes, chacun d'entre eux demandant de vider complètement les registres du processeur, ainsi que la file ou la pile. Ils sont l'équivalent sur ces architectures des instructions de spilling, qui sauvegardent la totalité des registres du processeur en mémoire RAM, pour les récupérer à la fin de l’exécution de la fonction.

Les modes d'adressage des machines à pile/file modifier

Les machines à pile que je viens de décrire ne peuvent manipuler que des données sur la pile : ces machines à pile sont ce qu'on appelle des machines à zéro adresse. Avec une telle architecture, les instructions sont très petites, car il n'y a pas besoin de bits pour indiquer la localisation des données dans la mémoire, sauf pour POP et PUSH. Toutefois, certaines machines à pile plus évoluées autorisent certaines instructions à préciser l'adresse mémoire d'un (voire plusieurs, dans certains cas) de leurs opérandes. Ces machines sont appelées des machines à pile à une adresse.

En théorie, les instructions qui manipulent des opérandes ne peuvent qu’accéder aux instructions au sommet de la pile, ou la fin de la file. Ces opérandes sont retirées de la pile par l'instruction et le résultat est placé au sommet de la pile. C'est du moins le cas sur les machines à pile dites "pures", mais d'autres architectures ne fonctionnent pas ainsi. Avec elles, il est possible de préciser la position dans la pile des opérandes à utiliser. Une instruction peut donc indiquer qu'elle veut utiliser les opérandes situées 2, 3 ou 5 cases sous le sommet de la pile. Si les opérandes sélectionnées ne sont pas toujours retirées de la pile (tout dépend de l'architecture), le résultat est lui toujours placé au sommet de la pile. Les opérandes se déplacent donc bizarrement dans ce genre d'architectures. Ces jeux d'instruction n'utilisent pas la pile comme une mémoire LIFO, ce qui lui vaut le nom d'architecture à pseudo-pile. Il existe un équivalent pour les machines à file, qui portent encore une fois le nom de machines à file "impures" ou à pseudo-file.

Avantages et désavantages modifier

Sur ces architectures, les programmes utilisent peu de mémoire. La raison à cela est que les instructions sont très petites : on n'a pas besoin d'utiliser de bits pour indiquer la localisation des données dans la mémoire, sauf pour Pop et Push. Vu que les programmes créés pour ces machines sont souvent très petits, on dit que la densité du code (code density) est bonne. Par contre, une bonne partie des instructions de notre programmes seront des instructions Pop et Push qui ne servent qu'à déplacer les opérandes entre la RAM et la FIFO/LIFO. Une bonne partie des instructions ne sert donc qu'à manipuler la mémoire et pas à faire des calculs. Sans compter que notre programme comprendra beaucoup d'instructions comparé aux autres types de processeurs. Enfin, ces machines n'ont pas besoin d'utiliser beaucoup de registres pour stocker leur état : un Stack Pointer et un Program Counter suffisent. Les machines à pile furent les premières à être inventées et utilisées : dans les débuts de l’informatique, la mémoire était rare et chère, et l'économiser était important. Ces machines à pile permettaient d'économiser de la mémoire facilement et d'utiliser peu de registres, ce qui était le compromis idéal pour l'époque.

Les architectures à pile et à file ont plusieurs défauts. En premier lieu, la complexité de la gestion de la pile/file entraîne l'usage d'un grand nombre d'instructions d'accès mémoire. Mais surtout, il est impossible de réutiliser une donnée chargée dans la pile, toute opération dépilant ses opérandes. Cela entraîne un grand nombre d'accès en mémoire RAM, défaut rédhibitoire quand la RAM est lente et peu chère et où la hiérarchie mémoire dicte sa loi. Les autres classes d'architectures n'ont pas ces défauts et font usage de modes d'adressage autres que le mode d'adressage implicite. L'usage de ces modes d'adressages permet d'éviter d'avoir à copier des données dans une pile, les empiler, et les déplacer avant de les manipuler. Le nombre d'accès à la mémoire est donc plus faible comparé à une machine à pile pure.

Les jeux d'instruction RISC vs CISC modifier

La seconde classification que nous allons aborder se base sur le nombre d'instructions. Elle classe nos processeurs en deux catégories :

  • les RISC (reduced instruction set computer), au jeu d'instruction simple ;
  • et les CISC (complex instruction set computer), qui ont un jeu d'instruction étoffé.
Différences CISC/RISC
Propriété CISC RISC
Instructions
  • Nombre d'instructions élevé, parfois plus d'une centaine.
  • Beaucoup d'instructions complexes (fonctions trigonométriques, gestion de texte, autres).
  • Supportent des types de données complexes : texte, listes chainées, etc.
  • Instructions de taille variable, pour améliorer la densité de code.
  • Faible nombre d'instructions, moins d'une centaine.
  • Pas d'instruction complexes.
  • Types supportés limités aux entiers (adresses comprises) et flottants.
  • Instructions de taille fixe pour simplifier le processeur.
Modes d'adressage
  • Beaucoup de modes d'adressages.
  • Présence de modes d'adressages complexes.
  • Possibilité d'effectuer plusieurs accès mémoires par instruction, avec certains modes d'adressage.
  • Pas d'architecture LOAD-STORE.
  • Peu de modes d’adressage.
  • Pas de modes d'adressages complexes.
  • Pas plus d'un accès mémoire par instruction.
  • Architecture LOAD-STORE.
Registres
  • Présence de registres spécialisés, parfois absence de registres généraux.
  • Peu de registres : rarement plus de 16 registres entiers.
  • Presque pas de registres spécialisés.
  • Beaucoup de registres, souvent plus de 32.

Les processeurs CISC modifier

Les jeux d'instructions CISC sont les plus anciens et étaient à la mode jusqu'à la fin des années 1980. À cette époque, on programmait rarement avec des langages de haut niveau et beaucoup de programmeurs codaient en assembleur. Avoir un jeu d'instruction complexe, avec des instructions de "haut niveau" qu'on ne devait pas refaire à partir d'instructions plus simples, facilitait la vie des programmeurs.

Cette complexité des jeux d'instructions n'a pas que des avantages "humains", mais a aussi quelques avantages techniques. Le premier est une meilleure densité de code : un programme codé sur CISC utilise moins d'instructions et prend moins de mémoire. À l'époque des processeurs CISC, la mémoire était rare et chère, ce qui faisait que les ordinateurs n'avaient pas plusieurs gigaoctets de mémoire : économiser celle-ci était crucial. Cet avantage était donc crucial, ce qui contrebalançait les défauts de ces architectures. Ces défauts étaient essentiellement le fait que le grand nombre d'instructions entraîne une grande consommation de transistors et d'énergie. La difficulté de conception de ces processeur était aussi sans précédent.

Les processeurs RISC modifier

Est-ce que les instructions complexes des processeurs CISC sont vraiment utiles ? Pour le programmeur qui écrit ses programmes en assembleur, elle le sont. Mais depuis l'invention des langages de haut niveau, la réponse dépend de l'efficacité des compilateurs. Des analyses assez anciennes, effectuées par IBM, DEC et quelques laboratoires de recherche, ont montré que les compilateurs n'utilisaient pas la totalité des instructions fournies par un processeur. Nombre de ces instructions ne sont utilisées que dans de rares cas, voire jamais. Autant dire que beaucoup de transistors étaient gâchés ! L'idée de créer des processeurs possédant des jeux d'instructions simples et contenant un nombre limité d'instructions très rapides commença à germer. Ces processeurs sont de nos jours appelés des processeurs RISC (acronyme de Reduced Instruction Set Computer). Ils n'ont pas les défauts des CISC, mais n'en ont pas les avantages : la densité de code est mauvaise, en contrepartie d'une simplicité non-négligeable du processeur et une moindre consommation thermique/de transistors.

Mais de tels processeurs RISC, complètement opposés aux processeurs CISC, durent attendre un peu avant de percer. Par exemple, IBM décida de créer un processeur possédant un jeu d'instruction plus sobre, l'IBM 801, qui fût un véritable échec commercial. Mais la relève ne se fit pas attendre. C'est dans les années 1980 que les processeurs possédant un jeu d'instruction simple devinrent à la mode. Cette année là, un scientifique de l'université de Berkeley décida de créer un processeur possédant un jeu d'instruction contenant seulement un nombre réduit d'instructions simples, possédant une architecture particulière. Ce processeur était assez novateur et incorporait de nombreuses améliorations qu'on retrouve encore dans nos processeurs haute performances actuels, ce qui fit son succès : les processeurs RISC étaient nés.

Conclusion modifier

Durant longtemps, les CISC et les RISC eurent chacun leurs admirateurs et leurs détracteurs. Au final, on ne peut pas dire qu'un processeur CISC sera toujours meilleur qu'un RISC ou l'inverse et chacun a des avantages et des inconvénients, qui rendent le RISC/CISC adapté ou pas selon la situation. Par exemple, on mettra souvent un processeur RISC dans un système embarqué, devant consommer très peu. Le CISC était bien plus utile quand on programmait encore en assembleur et que la mémoire était limitée. En tout cas, la performance d'un processeur dépend assez peu du fait que le processeur soit un RISC ou un CISC, même si cela peut faire la différence en termes de transistors ou de simplicité de conception.

De plus, de nos jours, les différences entre CISC et RISC commencent à s'estomper. Les processeurs actuels sont de plus en plus difficiles à ranger dans des catégories précises. Les processeurs actuels sont conçus d'une façon plus pragmatique : au lieu de respecter à la lettre les principes du RISC et du CISC, on préfère intégrer les techniques et instructions qui fonctionnent, peu importe qu'elles viennent de processeurs purement RISC ou CISC. Les anciens processeurs RISC se sont ainsi garnis d'instructions et techniques de plus en plus complexes et les processeurs CISC ont intégré des techniques provenant des processeurs RISC (pipeline, etc). La guerre RISC ou CISC n'a plus vraiment de sens de nos jours.

En parallèle de ces architectures CISC et RISC, qui sont en quelques sorte la base de tous les jeux d'instructions, d'autres classes de jeux d'instructions sont apparus, assez différents des jeux d’instructions RISC et CISC. On peut par exemple citer le Very Long Instruction Word, qui sera abordé dans les chapitres à la fin du tutoriel. La plupart de ces jeux d'instructions sont implantés dans des processeurs spécialisés, qu'on fabrique pour une utilisation particulière. Ce peut être pour un langage de programmation particulier, pour des applications destinées à un marche de niche comme les supercalculateurs, etc.

Les jeux d'instructions spécialisés modifier

En parallèle de ces architectures CISC et RISC, d'autres classes de jeux d'instructions sont apparus. Ceux-ci visent des buts distincts, qui changent suivant le jeu d'instruction :

  • soit ils cherchent à diminuer la taille des programmes et à économiser de la mémoire ;
  • soit ils cherchent à gagner en performance, en exécutant plusieurs instructions à la fois ;
  • soit ils tentent d'améliorer la sécurité des programmes et les rendent résistants aux attaques ;
  • soit ils sont adaptés à certaines catégories de programmes ou de langages de programmation.

Les architectures compactes modifier

Certains chercheurs ont inventé des jeux d’instruction pour diminuer la taille des programmes. Certains processeurs disposent de deux jeux d'instructions : un compact, et un avec une faible densité de code. Il est possible de passer d'un jeu d'instructions à l'autre en plein milieu de l’exécution du programme, via une instruction spécialisée.

D'autres processeurs sont capables d’exécuter des binaires compressés (la décompression a lieu lors du chargement des instructions dans le cache).

Les architectures parallèles modifier

Certaines architectures sont conçues pour pouvoir exécuter plusieurs instructions en même temps, lors du même cycle d’horloge : elles visent le traitement de plusieurs instructions en parallèle, d'où leur nom d’architectures parallèles. Ces architectures visent la performance, et sont relativement généralistes, à quelques exceptions près. On peut, par exemple, citer les architectures very long instruction word, les architectures dataflow, les processeurs EDGE, et bien d'autres. D'autres instructions visent à exécuter une même instruction sur plusieurs données différentes : ce sont les instructions SIMD, vectorielles, et autres architectures utilisées sur les cartes graphiques actuelles. Nous verrons ces architectures plus tard dans ce tutoriel, dans les derniers chapitres.

Les Digital Signal Processors modifier

Certains jeux d'instructions sont dédiés à des types de programmes bien spécifiques, et sont peu adaptés pour des programmes généralistes. Parmi ces jeux d'instructions spécialisés, on peut citer les fameux jeux d'instructions Digital Signal Processor, aussi appelés des DSP. Nous reviendrons plus tard sur ces processeurs dans le cours, un chapitre complet leur étant dédié, ce qui fait que la description qui va suivre sera quelque peu succincte. Ces DSP sont des processeurs chargés de faire des calculs sur de la vidéo, du son, ou tout autre signal. Dès que vous avez besoin de traiter du son ou de la vidéo, vous avez un DSP quelque part, que ce soit une carte son ou une platine DVD.

Ls DSP ont un jeu d'instruction similaire aux jeux d'instructions RISC, avec quelques instructions supplémentaires spécialisées pour faire du traitement de signal. On peut par exemple citer l'instruction phare de ces DSP, l'instruction MAD, qui multiplie deux nombres et additionne un 3éme au résultat de la multiplication. De nombreux algorithmes de traitement du signal (filtres FIR, transformées de Fourier) utilisent massivement cette opération. Ces DSP possèdent aussi des instructions dédiées aux boucles, ou des instructions capables de traiter plusieurs données en parallèle (en même temps). De plus, les DSP utilisent souvent des nombres flottants assez particuliers qui n'ont rien à voir avec les nombres flottants que l'on a vu dans le premier chapitre. Certains DSP supportent des instructions capables d'effectuer plusieurs accès mémoire en un seul cycle d'horloge : ils sont reliés à plusieurs bus mémoire et sont donc capables de lire et/ou d'écrire plusieurs données simultanément. L'architecture mémoire de ces DSP est une architecture Harvard, couplée à une mémoire multi-ports. Les caches sont rares dans ces architectures, quoique parfois présents.

Les architectures dédiées à un langage de programmation modifier

Certains processeurs sont carrément conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des processeurs dédiés. Par exemple, l'ALGOL-60, le COBOL et le FORTRAN ont eu leurs architectures dédiées. Les fameux Burrough E-mode B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau à avoir été directement câblé en assembleur sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du FORTH.

Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leur circuits : elles possédaient notamment un garbage collector câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution.

Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Les programmes compilés faisaient un mauvais usage des instructions machines, ne savaient pas bien utiliser les modes d'adressages adéquats, manipulaient assez mal les registres, etc. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi étés inventées, avant que les concepteurs se rendent compte des défauts de cette approche. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues.

Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle.

Les implémentations matérielles de machines virtuelles modifier

Comme vous le savez sûrement, les langages de programmation de haut niveau sont souvent compilés vers un langage intermédiaire, avant d'être transformés en langage machine. Le langage intermédiaire qui sert...d'intermédiaire, peut être vu comme l'assembleur d'une machine abstraite, que l'on appelle une machine virtuelle, qui n'existe pas forcément dans la réalité. Le code machine associé à cette architecture est appelé le bytecode. Faire ainsi a de nombreux avantages pour les concepteurs de compilateurs. Notamment, cela permet d'avoir un compilateur qui traduit le langage de haut niveau pour plusieurs jeux d’instructions différents. Par exemple, on peut plus facilement créer un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. Pour cela, le compilateur est composé de deux partie : une partie commune qui traduit le C en langage intermédiaire, et plusieurs back-end qui traduisent le langage intermédiaire en code machine cible. C'est aussi utilisé par certains langages de programamtion comme le Python ou le Java, afin d'obtenir une bonne compatibilité : on compile le Python/Java en bytecode, qui lui-même est compilé ou interprété à l’exécution. Tout ordinateur sur lequel on a installé une machine virtuelle Java/Python peut alors exécuter ce bytecode.

Le jeu d'instruction de la machine virtuelle varie grandement d'une machine virtuelle à l'autre, mais il est optimisé de manière à ce que la traduction en code machine soit la plus simple possible. Classiquement, la machine virtuelle est une machine à pile, pour diverses raisons. Premièrement, cela réduit la taille du bytecode. Deuxièmement, il existe un algorithme assez simple et assez rapide qui traduit un code écrit pour une machine à pile en un code écrit pour une machine à registre. Et cet algorithme se débrouille pas trop mal pour attribuer les registres et les utiliser au mieux. Et il fonctionne assez bien peu importe le nombre de registres. C'est un avantage assez important pour les langages interprétés, où on n'utilise non pas un compilateur, mais un interpréteur. Sur les machines virtuelles récentes utilisées par les compilateurs, le nombre de registres n'est pas précisé, voire est infini. Cela déporte la phase d'allocation de registres (mettre telle valeur dans tel registre, gérer les échanges entre registres ou registres<->RAM) lors de la traduction en langage machine. Et cela permet de gérer des architectures très différentes qui n'ont pas les mêmes nombres de registres.

Si une machine virtuelle est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises. Et on peut l'implémenter en matériel ! le cas le plus étonnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre, avec un jeu d'instruction simple, une architecture à pile, etc. En temps normal, le bytecode Java n'est pas exécuté directement par le processeur, mais est compilé ou interprété. Et c'est ce qui permet au bytecode Java d'être portable sur des architectures différentes. Mais certains processeurs ARM, qu'on trouve dans des système embarqués, sont une implémentation matérielle de la machine virtuelle Java. Les architectures de ce type permettent de se passer de l'étape de traduction bytecode -> langage machine, vu que leur langage machine est le bytecode Java lui-même.

Il existe d'autres cas un peu particuliers où une machine virtuelle a été implémentée en hardware. Par exemple, les concepteurs de langages fonctionnels apprécient notamment la machine SECD. Celle-ci a été implémentée en matériel par plusieurs équipes, par l'intermédiaire de micro-code. La première implémentation a été créée par les chercheurs de l'université de Calgary, en 1989. DAns le même genre, quelques processeurs simples étaient capables d’exécuter directement le bytecode utilisé comme représentation intermédiaire pour le langage FORTH.

L'intérêt de ce genre de stratagèmes reste cependant mince. Cela permet d’exécuter plus vite les programmes compilés en bytecode, comme des programmes Java pour la JVM Java, mais cela n'a guère plus d'intérêt.


Outre le jeu d'instruction et l'architecture interne, les processeurs différent par la façon dont ils lisent et écrivent la mémoire. On pourrait croire qu'il n'y a pas grande différence entre processeurs dans la façon dont ils gèrent la mémoire. Mais ce n'est pas le cas : des différences existent qui peuvent avoir un effet assez important. Dans ce chapitre, on va parler de l'endianess du processeur et de son alignement mémoire : on va s'intéresser à la façon dont le processeur va repartir en mémoire les octets des données qu'il manipule. Ces deux paramètres sont sûrement déjà connus de ceux qui ont une expérience de la programmation assez conséquente. Les autres apprendront ce que c'est dans ce chapitre.

Le boutisme modifier

On peut introduire la notion de boutisme par une analogie avec les langues humaines : certaines s’écrivent de gauche à droite et d'autres de droite à gauche. Dans un ordinateur, c'est pareil avec les octets des mots mémoire : on peut les écrire soit de gauche à droite, soit de droite à gauche. Quand on veut parler de cet ordre d'écriture, on parle de boutisme (endianness).

Les différents types de boutisme modifier

Les deux types de boutisme les plus simples sont le gros-boutisme et le petit-boutisme. Sur les processeurs gros-boutistes, la donnée est stockée des adresses les plus faibles vers les adresses plus grande. Pour rendre cela plus clair, prenons un entier qui prend plusieurs octets et qui est stocké entre deux adresses. L'octet de poids fort de l'entier est stocké dans l'adresse la plus faible, et inversement pour le poids faible qui est stocké dans l'adresse la plus grande. Sur les processeurs petit-boutistes, c'est l'inverse : l'octet de poids faible de notre donnée est stocké dans la case mémoire ayant l'adresse la plus faible. La donnée est donc stockée dans l'ordre inverse pour les octets.

Certains processeurs sont un peu plus souples : ils laissent le choix du boutisme. Sur ces processeurs, on peut configurer le boutisme en modifiant un bit dans un registre du processeur : il faut mettre ce bit à 1 pour du petit-boutiste, et à 0 pour du gros-boutiste, par exemple. Ces processeurs sont dits bi-boutistes.

Gros-boutisme. Petit-boutisme.

Petit et gros-boutisme ont pour particularité que la taille des mots ne change pas vraiment l'organisation des octets. Peu importe la taille d'un mot, celui-ci se lit toujours de gauche à droite, ou de droite à gauche. Cela n’apparaît pas avec les techniques de boutismes plus compliquées.

Comparaison entre big-endian et little-endian, pour des tailles de 16 et 32 bits.
Comparaison entre un nombre codé en gros-boutiste pur, et un nombre gros-boutiste dont les octets sont rangés dans un groupe en petit-boutiste. Le nombre en question est 0x 0A 0B 0C 0D, en hexadécimal, le premier mot mémoire étant indiqué en jaune, le second en blanc.

Certains processeurs ont des boutismes plus compliqués, où chaque mot mémoire est découpé en plusieurs groupes d'octets. Il faut alors prendre en compte le boutisme des octets dans le groupe, mais aussi le boutisme des groupes eux-mêmes. On distingue ainsi un boutisme inter-groupe (le boutisme des groupes eux-même) et un boutisme intra-groupe (l'ordre des octets dans chaque groupe), tout deux pouvant être gros-boutiste ou petit-boutiste. Si l'ordre intra-groupe est identique à l'ordre inter-groupe, alors on retrouve du gros- ou petit-boutiste normal. Mais les choses changent si jamais l'ordre inter-groupe et intra-groupe sont différents. Dans ces conditions, on doit préciser un ordre d’inversion des mots mémoire (byte-swap), qui précise si les octets doivent être inversés dans un mot mémoire processeur, en plus de préciser si l'ordre des mots mémoire est petit- ou gros-boutiste.

Avantages, inconvénients et usage modifier

Le choix entre petit boutisme et gros boutisme est généralement une simple affaire de convention. Il n'y a pas d'avantage vraiment probant pour l'une ou l'autre de ces deux méthodes. Cela ne veut pas dire que n'y a pas d'avantage ou d'inconvénient d'une méthode sur l'autre, mais que ceux-ci sont mineurs. Dans les faits, il y a autant d'architectures petit- que de gros-boutistes, la plupart des architectures récentes étant bi-boutistes. Précisons que le jeu d'instruction x86 est de type petit-boutiste. Si on quitte le domaine des jeu d'instruction, il faut savoir que les protocoles réseaux et les formats de fichiers imposent un boutisme particulier. Les protocoles réseaux actuels (TCP-IP) sont de type gros-boutiste, ce qui impose de convertir les données réseaux avant de les utiliser sur les PC modernes. Et au passage, si le gros-boutisme est utilisé dans les protocoles réseau, alors que le petit-boutisme est roi sur le x86, c'est pour des raisons pratiques, que nous allons aborder ci-dessous.

Le gros-boutisme est très facile à lire pour les humains. Les nombres en gros-boutistes se lisent de droite à gauche, comme il est d'usage dans les langues indo-européennes, alors que les nombres en petit boutistes se lisent dans l'ordre inverse de lecture. Pour la lecture en hexadécimal, il faut inverser l'ordre des octets, mais il faut garder l'ordre des chiffres dans chaque octet. Par exemple, le nombre 0x015665 (87 653 en décimal) se lit 0x015665 en gros-boutiste, mais 0x655601 en petit-boutiste. Et je ne vous raconte pas ce que cela donne avec un byte-swap... Cette différence pose problème quand on doit lire des fichiers, du code machine ou des paquets réseau, avec un éditeur hexadécimal. Alors certes, la plupart des professionnels lisent directement les données en passant par des outils d'analyse qui se chargent d'afficher les nombres en gros-boutiste, voire en décimal. Un professionnel a à sa disposition du désassembleur pour le code machine, des analyseurs de paquets pour les paquets réseau, des décodeurs de fichiers pour les fichiers, des analyseurs de dump mémoire pour l'analyse de la mémoire, etc. Cependant, le gros-boutisme reste un avantage quand on utilise un éditeur hexadécimal, quel que soit l'usage. En conséquence, le gros-boutiste a été historiquement pas mal utilisé dans les protocoles réseaux et les formats de fichiers. Par contre, cet avantage de lecture a dû faire face à divers désavantages pour les architectures de processeur.

Le petit-boutisme peut avoir des avantages dans certaines conditions bien précises, sur certains jeux d'instruction particuliers. En premier lieu, il est utile pour les architectures qui peuvent lire des mots de différentes tailles. C'est le cas sur le x86, où l'on peut décider de lire des mots de 8, 16, 32, voire 64 bits à partir d'une adresse mémoire. Avec le petit-boutisme, on s'assure qu'une telle lecture charge bien la même valeur, le même nombre. Par exemple, imaginons que je stocke le nombre 0x 14 25 36 48 sur un mot mémoire, en petit-boutiste. En petit-boutiste, une opération de lecture reverra soit les 8 bits de poids faible (0x 48), soit les 16 bits de poids faible (0x 36 48), soit le nombre complet. Ce ne serait pas le cas en gros-boutiste, où les lectures reverraient respectivement 0x 14, 0x 14 25 et 0x 14 25 36 48. Avec le gros-boutisme, de telles opérations de lecture n'ont pas vraiment de sens. À la rigueur, elles peuvent servir à obtenir une approximation d'un grand nombre entier, mais cela sert peu et peut se faire autrement avec des opérations bit-à-bit. En soit, cet avantage est assez limité et n'est utile que pour les compilateurs et les programmeurs en assembleur.

Un autre avantage est un gain de performance pour certaines opérations. Les instructions qui gagnent en performance sont les opérations où on doit additionner un nombre de plusieurs octets sur un processeur qui ne fait les calculs qu'octet par octet. En clair, le processeur dispose d'instructions de calcul qui additionnent des nombres de 16, 32 ou 64 bit, voire plus. Mais à l'intérieur du processeur, les calculs sont faits octets par octets, l'unité de calcul ne pouvant qu'additionner deux nombres de 8 bits à la fois. Dans ce cas, le petit-boutisme garantit que l'addition des octets se fait dans le bon ordre, en commençant par les octets de poids faible pour progresser vers les octets de poids fort. La gestion de la propagation de la retenue est alors assez simple : il suffit de mémoriser la retenue de l'addition précédente dans un registre, avant de passer à l'addition d'octets suivante. En gros-boutisme, la propagation de la retenue pose de plus gros problèmes...

Pour résumer, les avantages et inconvénients de chaque boutisme sont mineurs. Le gain en performance est nul sur les architectures modernes, qui ont des unités de calcul capables de faire des additions multi-octets. L'usage d'opérations de lecture de taille variable est aujourd'hui tombé en désuétude, vu que cela ne sert pas à grand chose et complexifie le jeu d'instruction. Enfin, l'avantage de lecture n'est utile que dans situations tellement rares qu'on peut légitimement questionner son statut d'avantage. En bref, les différentes formes de boutisme se valent.

L'alignement mémoire modifier

Il arrive que le bus de données ait une largeur de plusieurs cases mémoire. Le processeur peut charger 2, 4 ou 8 cases mémoire d'un seul coup (parfois plus). Une donnée qui a la même taille que le bus de données est appelée un mot mémoire. Quand on veut accéder à une donnée sur un bus plus grand que celle-ci, le processeur ignore les mots mémoire en trop.

Exemple du chargement d'un octet dans un registre de trois octets.

Sur certains processeurs, il existe des restrictions sur la place de chaque mot en mémoire, restrictions résumées sous le nom d'alignement mémoire.

Sans alignement mémoire modifier

Sans alignement, on peut lire ou écrire un mot, peu importe son adresse. Pour donner un exemple, je peux parfaitement lire une donnée de 16 bits localisée à l'adresse 4, puis lire une autre donnée de 16 bits localisée à l'adresse 5 sans aucun problème.

Chargement d'une donnée sur un processeur sans contraintes d'alignement.

Avec alignement mémoire modifier

D'autres processeurs imposent des restrictions dans la façon de gérer ces mots : ils imposent un alignement mémoire. Tout se passe comme si la mémoire était découpée en blocs de la taille d'un mot mémoire et que le processeur accédait à des blocs en entier. Par contre, la capacité de la mémoire reste inchangée, ce qui fait que le nombre d'adresses utilisables diminue : il n'y a plus besoin que d'une adresse par mot mémoire et non par octet.

Chargement d'une donnée sur un processeur avec contraintes d'alignement.

La validité des adresses avec alignement mémoire modifier

Avec cette technique, il y a une différence entre l'adresse d'un mot et l'adresses d'un octet. Les octets ont une adresse qui est gérée par le processeur, alors que la mémoire ne gère que les adresses des mots. Par convention, l'adresse d'un mot est l'adresse de son octet de poids faible. Les autres octets du mot ne sont pas adressables par la mémoire. Par exemple, si on prend un mot de 8 octets, on est certain qu'une adresse sur 8 disparaîtra. L'adresse du mot est utilisée pour communiquer avec la mémoire, mais cela ne signifie pas que l'adresse des octets est inutile au-delà du calcul de l'adresse du mot. En effet, l'accès à un octet précis est encore possible : le processeur lit un mot entier, sélectionne l'octet adéquat et oublie les autres. Et pour cela, il doit déterminer la position du octet dans le mot à partir de l'adresse du octet est utile. Ses bits de poids faibles donnent la position du octet dans le mot.

Prenons un processeur ayant des mots de 4 octets et répertorions les adresses utilisables. Le premier mot contient les octets d'adresse 0, 1, 2 et 3. L'adresse zéro est l'adresse de l'octet de poids faible et sert donc d'adresse au premier mot, les autres sont inutilisables sur le bus mémoire. Le second mot contient les adresses 4, 5, 6 et 7, l'adresse 4 est l'adresse du mot, les autres sont inutilisables. Et ainsi de suite. Si on fait une liste exhaustive des adresses valides et invalides, on remarque que seules les adresses multiples de 4 sont utilisables. Et ceux qui sont encore plus observateurs remarqueront que 4 est la taille d'un mot.

Dans l'exemple précédent, les adresses utilisables sont multiples de la taille d'un mot. Sachez que cela fonctionne quelle que soit la taille du mot. Si N est la taille d'un mot, alors seules les adresses multiples de N seront utilisables. Avec ce résultat, on peut trouver une procédure qui nous donne l'adresse d'un mot à partir de l'adresse d'un octet : il suffit de diviser l'adresse du octet par N (le nombre d'octets dans un mot). Et le reste de cette division donne la position du octet dans le mot : un reste de 0 nous dit que l'octet est le premier du mot, un reste de 1 nous dit qu'il est le second, etc.

Adresse d'un mot avec alignement mémoire strict.

L'usage d'octets qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, ce qui fait que tous les mots ont une taille égale à une puissance de deux. La première raison est que cela permet d'économiser des fils sur le bus d'adresse. Si la taille d'un mot est égale à , seules les adresses multiples de seront utilisables. Or, ces adresses se reconnaissent facilement : leurs n bits de poids faibles valent zéro. On n'a donc pas besoin de câbler les fils correspondant à ces bits de poids faible. En second lieu, cela permet de simplifier le calcul de l'adresse d'un mot, ainsi que le calcul de la position du octet dans le mot. Rappelons que pour un mot de N octets, le calcul de l'adresse du mot est une division, alors que celui de la position du octet est un modulo. Avec , la division est un simple décalage et le modulo est un simple ET logique, deux opérations très rapides par rapport à une division généraliste. Les calculs d'adresse sont donc beaucoup plus rapides et très simples.

Les accès mémoire non-alignés modifier

Maintenant imaginons un cas particulier : je dispose d'un processeur utilisant des mots de 4 octets. Je dispose aussi d'un programme qui doit manipuler un caractère stocké sur 1 octet, un entier de 4 octets et une donnée de deux octets. Mais un problème se pose : le programme qui manipule ces données a été programmé par quelqu'un qui n'était pas au courant de ces histoire d'alignement, et il a répartit mes données un peu n'importe comment. Supposons que cet entier soit stocké à une adresse non-multiple de 4. Par exemple :

Adresse Octet 4 Octet 3 Octet 2 Octet 1
0x 0000 0000 Caractère Entier Entier Entier
0x 0000 0004 Entier Donnée Donnée
0x 0000 0008

Pour charger mon caractère dans un registre, pas de problèmes : celui-ci tient dans un mot. Il me suffit alors de charger mon mot dans un registre en utilisant une instruction de mon processeur qui charge un octet. Pour ma donnée de 2 octets, pas de problèmes non plus ! Le processeur charge le mot entier en ne sélectionne que les octets utiles, chose possible avec quelques décalage et un masque. Mais les problèmes arrivent quand il s'agit de charger l'entier. L'entier est en effet stocké sur deux mots différents, et on ne peut le charger en une seule fois : on dit que l'entier n'est pas aligné en mémoire.

Dans ce cas, il peut se passer des tas de choses suivant le processeur. Sur certains processeurs, la donnée est chargée en deux fois : c'est légèrement plus lent que la charger en une seule fois, mais ça passe. Mais sur d'autres processeurs, la situation devient nettement plus grave : le processeur ne peut en effet gérer ce genre d'accès mémoire dans ses circuits et considère qu'il est face à une erreur, similaire à une division par zéro ou quelque chose dans le genre. Il va alors interrompre le programme en cours d’exécution et exécuter un petit sous-programme qui gérera cette erreur. On dit que notre processeur effectue une exception matérielle. Si on est chanceux, ce programme de gestion d'erreur chargera cette donnée en deux fois : ça prend beaucoup de temps. Mais sur d'autres processeurs, le programme responsable de cet accès mémoire en dehors des clous se fait sauvagement planter. Par exemple, essayez de manipuler une donnée qui n'est pas "alignée" dans un mot de 16 octets avec une instruction SSE, vous aurez droit à un joli petit crash !

Pour éviter ce genre de choses, les compilateurs utilisés pour des langages de haut niveau préfèrent rajouter des données inutiles (on dit aussi du bourrage) de façon à ce que chaque donnée soit bien alignée sur le bon nombre d'octets. En reprenant notre exemple du dessus, et en notant le bourrage X, on obtiendrait ceci :

Adresse Octet 4 Octet 3 Octet 2 Octet 1
0x 0000 0000 Caractère X X X
0x 0000 0004 Entier Entier Entier Entier
0x 0000 0008 Donnée Donnée X X

Comme vous le voyez, ça prend un peu plus de place, et de la mémoire est gâchée inutilement. C'est pas grand chose, mais quand on sait que de la mémoire cache est gâchée ainsi, ça peut jouer un peu sur les performances. Il y a cependant des situations dans lesquelles rajouter du bourrage est une bonne chose et permet des gains en performances assez abominables (une sombre histoire de cache dans les architectures multiprocesseurs ou multi-cœurs, mais je n'en dit pas plus). Cet alignement se gère dans certains langages (comme le C, le C++ ou l'ADA), en gérant l'ordre de déclaration de vos variables. Essayez toujours de déclarer vos variables de façon à remplir un mot intégralement ou le plus possible. Renseignez-vous sur le bourrage, et essayez de savoir quelle est la taille de vos données en regardant la norme de vos langages.


Après avoir vu la théorie, nous allons étudier un cas particulier de jeu d'instruction. Précisément, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'une architecture qui a recu de nombreux ajouts au cours du temps, et qui a été fortement modifiée, tout en gardant une certaine compatibilité avec les versions plus anciennes. Ce qui a donné un jeu d’instruction particulièrement compliqué. Dans ce chapitre, nous allons étudier un de ces ajout, une extension de ce jeu d'instruction, qui a ajouté la gestion des flottants au x86. La gestion des flottants est une option qui a été rajoutée au cours de l'existence de l'architecture. Si les processeurs x86 des années 80 ne pouvaient pas faire des calculs flottants, ou alors seulement avec l'aide d'un coprocesseur, tous les processeurs x86 actuels le peuvent. L'ensemble des instructions machine x86 liées aux flottants s'appelle l'extension x87. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE.

Les registres x87 modifier

L'extension x87 fournit, en plus des instructions, plusieurs registres :

  • 8 registres pour les opérandes flottantes ;
  • 3 registres d'état pour configurer les exceptions, les arrondis, etc ;
  • 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès.

Une organisation en pseudo-pile modifier

L'extension x87 est un cas d'architecture à pile, assez spécialisée, totalement différente de la gestion des registres x86 normaux. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 : le premier registre est le registre 7, tandis que le dernier registre est le registre 0. Lorsque la FPU x87 est initialisée, ces 8 registres sont complètement vides : ils ne contiennent aucun flottant. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'une opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand.

Pseudo-pile x87 - chargement d'une opérande.
Pseudo-pile x87 - retrait d'une opérande.

Les instructions à une opérande (les instructions de calcul d'une tangente, d'une racine carrée et d'autres) vont dépiler le flottant au sommet de la pile. Les instructions à deux opérandes (multiplication, addition, soustraction et autres) peuvent se comporter de plusieurs manières différentes. Le cas le plus simple est celui attendu de la part d'une architecture à pile : l'instruction dépile les deux opérandes au sommet de la pile. La seconde possibilité est celle attendue de la part d'une architecture à pile à une adresse : elles dépilent le sommet de la pile et chargent l'autre opérande depuis la mémoire RAM. Le troisième cas est plus intéressant : l'instruction dépile le sommet de la pile, et charge l'autre opérande depuis n'importe quel autre registre de la pile. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres.

Le registre d'état modifier

Si vous avez bonne mémoire, vous vous souvenez sûrement de ce que j'ai dit que la FPU contient 3 registres spéciaux qui ne stockent pas de flottants, mais sont malgré tout utiles. Ces 3 registres portent les noms de Control Word, Status Word et Tag Word. Le registre Tag Word indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Ce registre contient 16 bits et pour chacun des 8 registres de données de la FPU, 2 bits sont réservés dans le registre Tag Word. Ces deux bits contiennent des informations sur le contenu du registre de données réservé.

  • Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ;
  • Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ;
  • Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ;
  • Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant.

Passons maintenant au Status Word. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception.

Bit Utilité
TOP Ce registre contient trois bits regroupés en un seul ensemble nommé TOP, qui stocke le numéro du premier registre vide dans l'ordre de remplissage. Idéal pour gérer notre pile de registres
U Sert à détecter les underflow. Il est mis à 1 lorsqu'un underflow a lieu.
O Pareil que U, mais pour les overflow : ce registre est mis à 1 lors d'un overflow
Z C'est un bit qui est mis à 1 lorsque notre FPU exécute une division par zéro
D Ce bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal
I Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0

Enfin, voyons le Control Word, le petit dernier. Il fait 16 bits et contient lui aussi des bits ayant chacun une utilité précise. Beaucoup de bits de ce registre sont inutilisés et on ne va citer que les plus utiles.

Bit Utilité
Infinity Control S'il vaut zéro, les infinis sont tous traités comme s'ils valaient . S'il vaut un, les infinis sont traités normalement
Rouding Control C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé
  • 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ;
  • 01 : vers - l'infini ;
  • 10 : vers + l'infini ;
  • 11 : vers zéro
Precision Control Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754.
  • 00 : mantisse codée sur 24 bits ;
  • 01 : valeur inutilisée ;
  • 10 : mantisse codée sur 53 bits ;
  • 11 : mantisse codée sur 64 bits

Les instructions flottantes x87 modifier

L’extension x87 comprend les instructions de base supportées par la norme IEEE 754, ainsi que quelques autres. On y retrouve les quatre instructions arithmétiques de base de la norme IEEE754 (+, -, *, /), avec quelques autres calculs supplémentaires.

Les comparaisons modifier

Voici une liste de quelques instructions de comparaisons supportées par l'extension x87 :

  • FTST : compare le sommet de la pseudo-pile avec la valeur 0 ;
  • FICOM : compare le contenu du sommet de la pseudo-pile avec une constante entière ;
  • FCOM : compare le contenu du sommet de la pseudo-pile avec une constante flottante ;
  • FCOMI : compare le contenu des deux flottants au sommet de la pseudo-pile.

Les instructions arithmétiques modifier

On trouve aussi des instructions de calculs, qui comprennent les cinq opérations définies par la norme IEE754, mais aussi quelques instructions supplémentaires :

  • l'addition : FADD ;
  • la soustraction FSUB ;
  • la multiplication FMUL ;
  • la division FDIV ;
  • la racine carrée FSQRT ;
  • des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS).

L'extension x87 implémente aussi des instructions trigonométriques et analytiques telles que :

  • le cosinus : instruction FCOS ;
  • le sinus : instruction FSIN ;
  • la tangente : instruction FPTAN ;
  • l'arc tangente : instruction FPATAN ;
  • ou encore des instructions de calcul de logarithmes ou d'exponentielles.

Il va de soi que ces dernières ne sont pas supportées par la norme IEEE 754 et que tout compilateur qui souhaite être compatible avec la norme IEEE 754 ne doit pas les utiliser.

Les instructions d'accès mémoire modifier

En plus de ces instructions de calcul, l'extension x87 fournit des instructions pour transférer des flottants entre la mémoire et les registres. Celles-ci sont des équivalents des instructions PUSH et POP qu'on trouve sur les machines à pile, à l'exception d'une instruction équivalente à l'instruction SWAP. On peut citer par exemple les instructions dans le tableau suivant. D'autres instructions chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres.

Instruction Ce qu'elle fait
FLD Elle est capable de charger un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits
FSTP Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST
FXCH Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre

Le phénomène de double arrondi modifier

Chacun des registres de données vus plus haut stocke un nombre flottant codé sur 80 bits. Oui, vous avez bien lu, 80 bits et non 32 ou 64 : cette FPU calcule sur des nombres flottants double précision étendue et non sur des flottants simple ou double précision, qui ne sont pas gérés par la FPU x87. On peut alors se demander comment le processeur fait pour calculer avec des flottants simple et double précision. Tout se joue lors de l'accès à la mémoire avec l'instruction FLD : celle-ci se comporte différemment suivant le flottant qu'on lui demande de charger. En effet, cette instruction peut charger depuis la mémoire un flottant simple précision, double précision ou double précision étendue. Le format du flottant qui doit être chargé est stocké directement dans l'instruction. Je m'explique : une instruction machine est stockée en mémoire sous la forme d'une suite de bits, et pour certaines instructions, des bits supplémentaires sont ajoutés. Dans notre cas, ces bits optionnels servent à indiquer à notre instruction le format du flottant qu'elle doit charger.

La FPU x87 peut charger depuis la mémoire un nombre flottant 80 bits directement dans un registre. Pour les flottants 32 et 64 bits, la FPU va devoir effectuer une conversion de notre flottant simple ou double précision en un flottant 80 bits. Tous les calculs faits par notre FPU vont donner des résultats codés sur 80 bits, et ceux-ci restent codés sur 80 bits tant que ceux-ci sont stockés dans les registres de la FPU. Par contre, dès qu'il faut enregistrer un nombre flottant en mémoire RAM, les problèmes commencent. Si le flottant en question est stocké dans la mémoire sur 32 ou 64 bits, notre processeur doit convertir le contenu du registre dans le format du flottant en mémoire, histoire de conserver le bon format de base. Cette conversion est faite automatiquement par l'instruction d'écriture en mémoire utilisée. Par contre, si notre flottant est représenté en mémoire sur 80 bits, l'écriture en mémoire est directe : pas de conversion. Et ces conversions posent problème : elles ne respectent pas la norme IEEE 754 !

Comparons un calcul effectué sur un processeur gérant nativement les formats 64 et 32 bits et ce même calcul exécuté par la x87. Dans tous les cas, les flottants seront chargés dans les registres, le calcul s'effectuera et le résultat sera enregistré en mémoire RAM. Sur un processeur qui gére nativement les formats simple et double précision, ni le chargement, ni les calculs, ni l'enregistrement ne demanderont de faire des conversions vers des flottants 80 bits. Avec la x87, les flottants 32/64 bits sont convertis en un flottant x87 80 bits lors des échanges entre la pseudo-pile et la RAM. Les calculs sont effectués sur des flottants 80 bits uniquement, sans conversions. Lors de l'enregistrement d'un flottant x87 80 bits en mémoire, celui-ci est converti dans son format de base, au flottant 32 ou 64 bits le plus proche. On se retrouve donc avec un arrondi supplémentaire, en plus des arrondis liés aux calculs : c'est le phénomène du double rounding (qui signifie double-arrondi en français). Et rien n'implique que le résultat de ces deux conversions aurait donné le même résultat que le calcul effectué sur des flottants 64 bits !

Phénomène de double arrondi sur les coprocesseurs x87

Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées et qui avaient fait la une de la presse informatique étaient causées par ces arrondis supplémentaires. Bien sûr, sachez que ce bogue a pu être reproduit sur de nombreux autres langages et n'était certainement pas limité au PHP ou au Java : c'est le non-respect de la norme IEE754 par notre unité de calcul x87 qui était clairement en cause.

De plus, si une série de calculs est faite sur des flottants stockés dans les registres, les résultats intermédiaires auront une précision supérieure à ce qui se serait passé avec des flottants simple ou double précision. Dans ces conditions, le résultat peut être différent de celui qu'on aurait obtenu en utilisant seulement des flottants 64 bits lors des calculs. Le pire, c'est qu'on n'a aucune solution à ce problème, pour les calculs faits avec l'extension x87.

Autre problème, lié au précédent : rares sont les calculs effectués intégralement dans les registres, et on est parfois obligé de temporairement sauvegarder en mémoire le contenu d'un registre pour laisser le registre libre pour un autre nombre flottant. C'est le programmeur ou le compilateur qui gère quand effectuer ce genre de sauvegarde et sur quels registres. Chacune de ces sauvegardes va arrondir le flottant que l'on souhaite sauvegarder. Conséquence : suivant l'ordre de ces sauvegardes, le moment auquel elles ont lieu et les flottants qui sont choisis pour être sauvegardés, le résultat ne sera pas le même ! Avec le même programme, si vous décidez de sauvegarder un flottant et votre voisin un autre, ce ne sera pas le même flottant qui sera arrondi lors de son transfert en mémoire, et le résultat des calculs sur votre ordinateur sera différent des résultats obtenus sur l'ordinateur de votre voisin. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul.


La mémoire virtuelle et la protection mémoire modifier

L'espace d'adressage du processeur correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses et l'ensemble de ces adresses forme son espace d'adressage. L'espace d'adressage n'est pas la mémoire réellement installée : s'il n'y a pas assez de RAM installée, des adresses seront inoccupées. De plus, une partie de l'espace d'adressage peut être détourné pour communiquer avec les périphériques, comme nous le verrons plus bas. Nous verrons aussi dans ce chapitre qu'il est possible qu'un processeur ait plusieurs espaces d'adressages séparés. Et même si cela peut sembler contre-intuitif, nous allons voir que les architectures avec plusieurs espaces d'adressage sont plus simples à comprendre !

Les processeurs avec un seul espace d'adressage modifier

Pour le moment, considérons le cas intuitif où on ne dispose que d'un seul espace d'adressage. Même si on omet les portions inoccupées de l'espace d'adressage, la RAM n'est pas la seule occupante de l'espace d'adressage. On y trouve aussi la mémoire ROM, les périphériques et d'autres choses encore.

Les architectures Von Neumann modifier

Si on n'a qu'un seul espace d'adressage unique, il est utilisé pour adresser non seulement la mémoire RAM, mais aussi la mémoire ROM. On est alors face à une architecture Von Neumann, où un seul espace d'adressage est découpé entre la mémoire RAM d'un côté et la mémoire ROM de l'autre. Une adresse correspond soit à la mémoire RAM, soit à la mémoire ROM, mais pas aux deux. Typiquement, la mémoire ROM est placée dans les adresses hautes, les plus élevées, alors que la RAM est placée dans les adresses basses en commençant par l'adresse 0. C'est une convention qui n'est pas toujours respectée, aussi mieux vaut éviter de la tenir pour acquise.

Vision de la mémoire par un processeur sur une architecture Von Neumann.

Les entrées-sorties mappées en mémoire modifier

Sur les ordinateurs avec un seul espace d'adressage, une partie de l'espace d'adressage peut être détourné pour communiquer avec les périphériques. L'idée est que le périphérique se retrouve inclus dans l'ensemble des adresses utilisées pour manipuler la mémoire : on dit qu'il est mappé en mémoire. Les adresses mémoires associées à un périphérique sont redirigées automatiquement vers le périphérique en question. On parle alors d'entrées-sorties mappées en mémoire.

IO mappées en mémoire

On remarque ainsi le défaut inhérent à cette technique : les adresses utilisées pour les périphériques ne sont plus disponibles pour la mémoire RAM. Dit autrement, on ne peut plus adresser autant de mémoire qu'avant. La perte peut être très légère ou très importante, en fonction des périphériques installés et de leur gourmandise en adresses mémoires. C'est ce qui causait autrefois un problème assez connu sur les ordinateurs 32 bits, qui ne géraient que 2^32 octets = 4 gibioctets. Certaines personnes installaient 4 gigaoctets de mémoire sur leur ordinateur 32 bits et se retrouvaient avec « seulement » 3,5 à 3,8 gigaoctets de mémoire, les périphériques prenant le reste. Et mine de rien, quand on a une carte graphique avec 512 mégaoctets de mémoire intégrée, une carte son, une carte réseau PCI, des ports USB, un port parallèle, un port série, des bus PCI Express ou AGP, et un BIOS à stocker dans une EEPROM/Flash, ça part assez vite.

Un autre défaut de cette méthode apparait sur les processeurs disposant de mémoire cache. Pour rappel, le cache est une petite mémoire censée accélérer les accès à la mémoire RAM, dans laquelle on stocke des copies des données en RAM. Or, pour le cache, une adresse mémoire est une adresse mémoire: le cache ne sait pas si l'adresse correspond à un périphérique ou non, et il mettra en cache son contenu automatiquement. Mais le problème est que les adresses liées aux périphériques, qui correspondent à des registres ou à la mémoire des périphériques, peuvent être modifiés sans que le cache soit mis au courant. Le périphérique peut à tout instant modifier son état ou sa mémoire interne, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches.

Nous reparlerons plus en détail des entrées-sorties mappées en mémoire dans un chapitre dédié à l'adressage des périphériques. Aussi, je ne détaillerais pas plus que ça cette technique dans ce chapitre.

La memory map d'un ordinateur avec un seul espace d'adressage modifier

Voici quelques exemples tirés de vrais ordinateurs existants. On voit qu'outre la mémoire RAM principale, des mémoires vidéo et même plusieurs mémoires ROM sont mappées en mémoire. Il y a un unique espace d'adressage qui contient tout ce qui est adressable : toutes les mémoires et tous les périphériques de l'ordinateur. Généralement, les adresses hautes sont réservées aux périphériques et aux mémoires ROM, alors que les adresses basses sont pour la RAM. La ROM est au sommet de l'espace d'adressage, les périphériques sont juste en-dessous, la RAM commence à l'adresse 0 et prend les adresses basses.

Espace d'adressage classique avec entrées-sorties mappées en mémoire

Un bon exemple est celui des ordinateurs PC un peu anciens, avec des processeurs x86. A l'époque, les processeurs x86 avaient des adresses de 20 bits, ce qui fait 1 mébioctet de mémoire adressable. Le premier mébioctet de mémoire est décomposé en deux portions de mémoire : les premiers 640 kibioctets sont ce qu'on appelle la mémoire conventionnelle, alors que les octets restants forment la mémoire haute. La mémoire au-delà du premier mébioctet, la mémoire étendue, est apparue quand les processeurs x86 32 bits sont apparus.

  • Les deux premiers kibioctets de la mémoire conventionnelle sont initialisés au démarrage de l'ordinateur. Ils sont utilisés pour stocker le vecteur d'interruption (on expliquera cela dans quelques chapitres) et servent aussi au BIOS. La portion réservée au BIOS, la BIOS Data Area, mémorise des informations en RAM. Elle commence à l'adresse 0040:0000h, a une taille de 255 octets, et est initialisée lors du démarrage de l'ordinateur.
  • Le reste de la mémoire conventionnelle est réservée à la mémoire RAM utilisée par le système d'exploitation (MS-DOS, avant sa version 5.0) et le programme en cours d’exécution.
  • Le bas de la mémoire haute est réservé pour communiquer avec les périphériques. On y trouve les BIOS des périphériques (dont celui de la carte vidéo, s'il existe) , qui sont nécessaires pour les initialiser et parfois pour communiquer avec eux. De plus, on y trouve la mémoire de la carte vidéo, et éventuellement la mémoire d'autres périphériques comme la carte son.
  • Le sommet de la mémoire haute est réservé au BIOS.
  • La mémoire étendue n'est pas réservée pour une utilisation précise.
Organisation Mémoire des vieux PC, à l'époque du DOS.

Les vielles machines, notamment les premiers ordinateurs comme les Commodores et les Amiga et les vielles consoles de jeux, utilisaient cette méthode pour sa simplicité. Ces machines n'étaient pas comme les ordinateurs personnels, pour lesquels on a une variété de cartes graphiques ou de cartes sons différentes. Tous avaient la même configuration matérielle, le matériel était fourni tel quel, ne pouvait pas être changé ni upgradé. Toutes les commodores 64 avaient exactement le même matériel, par exemple : la même carte son, la même carte graphique, les mêmes périphériques. Cette standardisation faisait que cela ne servait à rien de limiter l'accès au matériel. De telles machines n'avaient pas de système d'exploitation, ou bien celui-ci était rudimentaire et ne contrôlait pas vraiment l'accès au matériel. Les programmeurs avaient donc totalement accès au matériel et mapper les entrées/sorties en mémoire rendait la programmation des périphériques très simple.

Les processeurs avec plusieurs espaces d'adressages modifier

Il existe des processeurs qui sont capables de gérer plusieurs espaces d'adressage. Cela peut paraitre surprenant, mais nous avons déjà abordé un exemple dans les chapitres précédents (essayez de deviner lequel). Toujours est-il que l'on peut se demander quelle est l'utilité d'avoir plusieurs espaces d'adressage. La raison est pourtant simple, et même intuitive. Avec un seul espace d'adressage, les périphériques et la ROM sont mappés dans l'espace d'adressage. Des adresses censées être disponibles pour la RAM sont détournées vers la ROM ou les périphériques. Si très peu de RAM est installé, alors ce n'est pas un problème : des adresses inutilisées sont détournées pour des choses utiles. Mais si on veut utiliser plus de RAM, les choses se compliquent. L'idée est d'utiliser plusieurs espaces d'adressage dans lesquels on ne met pas la même chose, d'utiliser des espaces séparés pour des utilisations distinctes. On peut par exemple utiliser un espace d'adressage séparé pour la RAM, un autre pour la ROM, un autre pour les périphériques, etc. En dire plus demande de détailler plusieurs techniques qui utilisent chacune un espace d'adressage séparé.

Les architectures Harvard modifier

Le premier cas d'espace d'adressage séparé est celui des architectures Harvard. Pour rappel, avec l'architecture Harvard, on a un espace d'adressage séparé pour la RAM et la ROM. Une même adresse peut correspondre soit à la ROM, soit à la RAM : le processeur voit bien deux mémoires séparées, chacune dans son propre espace d'adressage. Les deux espaces d'adressage n'ont pas forcément la même taille : l'un peut contenir plus de mémoire/adresses que l'autre. Il est par exemple possible d'avoir un plus gros espace d'adressage pour la RAM que pour la ROM. Mais cela implique que les adresses des instructions et des données soient de taille différentes. C'est peu pratique et c'est rarement implémenté, ce qui fait que le cas le plus courant est celui où les deux espaces d'adressages ont la même taille.

Vision de la mémoire par un processeur sur une architecture Harvard.

L'espace d'adressage séparé pour les entrées-sorties modifier

Les entrées-sorties et périphériques peuvent avoir leur propre espace d'adressage dédié, séparé de celui utilisé pour la mémoire. Sur ce genre d'architectures, on trouve un espace d'adressage pour la mémoire RAM et la mémoire ROM, et un espace d'adressage spécialisé pour les périphériques et les entrées-sorties.

Bit IO.

Une même adresse peut donc adresser soit une entrée-sortie, soit une case mémoire. Et pour faire la différence, le processeur doit avoir des instructions séparées pour gérer les périphériques et adresser la mémoire. Il a des instructions de lecture/écriture pour lire/écrire en mémoire, et d'autres pour lire/écrire les registres d’interfaçage. Sans cela, le processeur ne saurait pas si une adresse est destinée à un périphérique ou à la mémoire. Cela élimine aussi les problèmes avec les caches : les accès à l'espace d'adressage de la RAM passent par l'intermédiaire de la mémoire cache, alors les accès dans l'espace d'adressage des périphériques le contournent totalement.

Là encore, les deux espaces d'adressage n'ont pas forcément la même taille. Il arrive que les deux espaces d'adressage aient la même taille, le plus souvent sur des ordinateurs complexes avec beaucoup de périphériques. Mais les systèmes embarqués ont souvent des espaces d'adressage plus petits pour les périphériques que pour la ou les mémoires. L'implémentation varie grandement suivant le cas, la première méthode imposant d'avoir deux bus séparés pour les mémoires et les périphériques, l'autre permettant un certain partage du bus d'adresse. Nous reviendrons dessus plus en détail dans le chapitre sur l'adressage des périphériques.

Le bank switching modifier

Le bank switching, aussi appelé commutation de banque, permet d'utiliser plusieurs espaces d'adressage sur un même processeur, sans attribuer chaque espace d'adressage pour une raison précise. L'espace d'adressage est présent en plusieurs exemplaires appelés des banques. Les banques sont numérotées, chaque numéro de banque permettant de l'identifier et de le sélectionner.

Le but de cette technique est d'augmenter la mémoire disponible pour l'ordinateur. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite l'espace d'adressage à 4 kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.

exemple de Bank switching.

Cette technique demande d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle se calcule en concaténant le numéro de banque avec l'adresse accédée. Le numéro de la banque actuellement en cours d'utilisation est mémorisé dans un registre appelé le registre de banque. On peut changer de banque en changeant le contenu de ce registre. Le processeur dispose souvent d'instructions spécialisées qui en sont capables.

Banque mémoire. Registre de banque.


Après avoir vu qu'un processeur pouvait gérer plusieurs espaces d'adressage, nous allons voir comment les programmes gèrent les espaces d'adressage. Nous allons étudier le cas où un seul programme d'éxecute, mais aussi celui où plusieurs programmes partagent la mémoire. Nous allons voir les deux cas, l'un après l'autre. Mais avant toute chose, parlons de la protection mémoire.

La protection mémoire : généralités modifier

Sans protection particulière, les programmes peuvent techniquement lire ou écrire les données des autres. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses. Il faut donc introduire des mécanismes de protection mémoire, pour isoler les programmes les uns des autres, et éviter toute modification problématique. La protection mémoire regroupe plusieurs techniques assez variées, qui ont des objectifs différents. Elles ont pour point commun de faire intervenir à des niveaux divers le système d'exploitation et le processeur.

Le premier objectif est l'isolation des processus. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un processus, d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.

La protection de l'espace exécutable empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.

D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gérent des droits d'accès, qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisaeur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.

Un seul espace d'adressage, non-partagé modifier

Le cas le plus simple est celui où il n'y a pas de système d'exploitation et où un seul programme s'exécute sur l'ordinateur. Le programme a alors accès à tout l'espace d'adressage. L'usage qu'il fait du ou des espaces d'adressage dépend de si on est sur une architecture Von Neumann ou Harvard.

Sur une architecture Von Neumann, le programme et ses données sont placées dans le même espace d'adressage. Le programme organise la mémoire en plusieurs sections, dans lesquelles le programme range des données différentes. Typiquement, on trouve quatre sections, qui regroupent des données suivant leur utilisation. Voici ces trois sections :

  • Le segment text contient le code machine du programme, de taille fixe.
  • Le segment data contient des données de taille fixe qui occupent de la mémoire de façon permanente.
  • Le segment pour la pile, de taille variable.
  • le reste est appelé le tas, de taille variable.
Organisation d'un espace d'adressage unique utilisé par un programme unique

Sur une architecture Harvard, le programme est placé dans un espace d'adressage à part du reste. Le problème, c'est que cet espace d'adressage ne contient pas que le code machine à exécuter. Il contient aussi des constantes, à savoir des données qui gardent la même valeur lors de l'exécution du programme. Elles peuvent être lues, mais pas modifiées durant l'exécution du programme. L'accès à ces constantes demande d'aller lire celles-ci dans l'autre espace d'adressage, pour les copier dans l'autre espace d'adressage. Pour cela, les architectures Harvard modifiées ajoutent des instructions pour copier les constantes d'un espace d'adressage à l'autre.

Organisation des espaces d'adressage sur une archi harvard modifiée
Typical computer data memory arrangement

La pile et le tas sont de taille variable, ce qui veut dire qu'ils peuvent grandir ou diminuer à volonté, contrairement au reste. Entre le tas et la pile, on trouve un espace de mémoire inutilisée, qui peut être réquisitionné selon les besoins. La pile commence généralement à l'adresse la plus haute et grandit en descendant, alors que le tas grandit en remontant vers les adresses hautes. Il s'agit là d'une convention, rien de plus. Il est possible d'inverser la pile et le tas sans problème, c'est juste que cette organisation est rentrée dans les usages.

Il va de soi que cette vision de l'espace d'adressage ne tient pas compte des périphériques. C'est-à-dire que les schémas précédents partent du principe qu'on a un espace d'adressage séparé pour les périphériques. Dans le cas où les entrées-sorties sont mappées en mémoire, l'organisation est plus compliquée. Généralement, les adresses associées aux périphériques sont placées juste au-dessus de la pile, dans les adresses hautes.

La protection mémoire avec seul espace d'adressage non-partagé est très simple. On n'a pas besoin d'isolation des processus, la gestion des droits d'accès est minimale quand elle existe. Par contre, on a besoin de protection de l'espace exécutable. Sur les architectures Harvard, le code machine et les données sont dans des espaces d'adressage séparés, et le code machine est dans une ROM, la protection de l'espace exécutable est donc garantie. Sur une architecture Von Neumann, elle ne l'est pas du tout.

Un seul espace d'adressage, partagé avec le système d'exploitation modifier

Maintenant, étudions le cas où le programme partage la mémoire avec le système d'exploitation. Sur les systèmes d'exploitation les plus simples, on ne peut lancer qu'un seul programme à la fois. Le système d'exploitation réserve une portion de taille fixe réservée au système d'exploitation, alors que le reste de la mémoire est utilisé pour le reste et notamment pour le programme à exécuter. Le programme est placé à un endroit en RAM qui est toujours le même.

Gestion de la mémoire sur les OS monoprogrammés.

L'OS utilise soit les adresses basses, soit les adresses hautes modifier

D'ordinaire, le système d'exploitation est est en mémoire RAM, comme le programme à lancer. Il faut alors copier le système d'exploitation dans la RAM, depuis une mémoire de masse. Sur d'autres systèmes, le système d'exploitation est placé dans une mémoire ROM. C'est le cas si le système d'exploitation prend très peu de mémoire, comme c'est le cas sur les systèmes les plus anciens, ou encore sur certains systèmes embarqués rudimentaires. Les deux cas font généralement un usage différent de l'espace d'adressage.

Si le système d'exploitation est copié en mémoire RAM, il est généralement placé dans les premières adresses, les adresses basses. A l'inverse, un OS en mémoire ROM est généralement placé à la fin de la mémoire, dans les adresses hautes. Mais tout cela n'est qu'une convention, et les exceptions sont monnaie courante. Il existe aussi une organisation intermédiaire, où le système d'exploitation est chargé en RAM, mais utilise des mémoires ROM annexes, qui servent souvent pour accéder aux périphériques. On a alors un mélange des deux techniques précédentes : l'OS est situé au début de la mémoire, alors que les périphériques sont à la fin, et les programmes au milieu.

Méthodes d'allocation de la mémoire avec un espace d'adressage unique

La protection mémoire quand l'espace d'adressage est partagé avec l'OS modifier

Sur de tels systèmes, il n'y a pas besoin d'isolation des processus, juste de protection de l'espace exécutable. Protéger les données de l'OS contre une erreur ou malveillance d'un programme utilisateur est nécessaire. Notons qu'il s'agit d'une protection en écriture, pas en lecture. Le programme peut parfaitement lire les données de l'OS sans problèmes, et c'est même nécessaire pour certaines opérations courantes, comme les appels systèmes. Par contre, la protection de l'espace exécutable doit rendre impossible au programme d'écrire dans la portion mémoire du système d'exploitation.

Dans le cas où le système d'exploitation est placé dans une mémoire ROM, il n'y a pas besoin de faire grand chose. Une écriture dans une ROM n'est pas possible, ce qui fait que l'OS est protégé. Le processeur peut détecter ce genre d'accès et terminer le programme fautif, mais ce n'est pas une nécessité. Mais dans le cas où le système d'exploitation est chargé en RAM, tout change. Il devient possible pour un programme d'aller écrire dans la portion réservée à l'OS et d'écraser le code de l'OS. Chose qu'il faut absolument empêcher.

La solution la plus courante est d'interdire les écritures d'un programme de dépasser une certaine limite, en-dessous ou au-dessus de laquelle se trouve le système d’exploitation. Pour cela, le processeur incorpore un registre limite, qui contient l'adresse limite au-delà de laquelle un programme peut pas aller. Quand un programme applicatif accède à la mémoire, l'adresse à laquelle il accède est comparée au contenu du registre limite. Si cette adresse est inférieure/supérieure au registre limite, le programme cherche à accéder à une donnée placée dans la mémoire réservée au système : l’accès mémoire est interdit, une exception matérielle est levée et l'OS affiche un message d'erreur. Dans le cas contraire, l'accès mémoire est autorisé et notre programme s’exécute normalement.

Protection mémoire avec un registre limite.

Un seul espace d'adressage, partagé entre plusieurs programmes modifier

Les systèmes d’exploitation modernes implémentent la multiprogrammation, le fait de pouvoir lancer plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Les programmes s’exécutent à tour de rôle sur un même processeur, ils partagent la mémoire RAM, etc. Le partage du processeur est géré au niveau logiciel par le système d'exploitation, et il ne nous intéressera pas ici. Par contre, le partage de la RAM et demande la coopération du logiciel et du matériel, ce qui nous intéressera dans ce chapitre.

Un point important est le partage de la RAM entre les différents programmes. Le système d'exploitation répartit les différents programmes dans la mémoire RAM et chaque programme se voit attribuer un ou plusieurs blocs de mémoire. Ils sont appelés des partitions mémoire, ou encore des segments. Dans ce qui va suivre, nous allons parler de segments, pour simplifier les explications. Nous allons partir du principe qu'un programme est égal à un segment, pour simplifier les explications, mais sachez qu'un programme peut être éclaté en plusieurs segments dispersés dans la mémoire, et même être conçu pour ! Nous en reparlerons dans le chapitre sur le mémoire virtuelle.

Partitions mémoire

Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Le premier problème est tout simplement de placer les segments au bon endroit dans l'espace d'adressage, mais c'est quelque chose qui est du ressort du système d'exploitation proprement dit.

Un autre problème est que chaque segment peut être placé n'importe où en RAM et sa position en RAM change à chaque exécution. En conséquence, les adresses des branchements et des données ne sont jamais les mêmes d'une exécution à l'autre. L'usage de branchements relatifs résout en partie le problème, mais il reste à corriger les adresses des données.

Pour résoudre ce problème, le compilateur considère que le segment commence à l'adresse zéro. En clair, les programmes sont conçus sans tenir compte des autres programmes en mémoire, à savoir qu'ils sont compilés de manière à accéder à toutes les adresses disponibles à partir de l'adresse zéro, à tout l'espace d'adressage. Mais l'OS ou le processeur corrigent les adresses internes au segment, en décalant toutes les adresses du segment à partir de sa base. Cette correction est en général réalisée par l'OS, mais il existe aussi des techniques matérielles, que nous verrons dans la suite du chapitre.

Un autre problème est que les programmes peuvent lire ou écrire dans le segment d'un autre. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses. Il faut donc introduire des mécanismes de protection mémoire, pour isoler les segments les uns des autres. Nous parlerons de ces mécanismes dans quelques chapitres.

Il arrive cependant que des programmes partagent une même zone de mémoire, pour échanger des données. En effet, les systèmes d'exploitation modernes gèrent nativement des systèmes de communication inter-processus, très utilisés par les programmes modernes. Les implémentations les plus simples consistent soit à partager un bout de mémoire entre processus, soit à communiquer par l’intermédiaire d'un fichier partagé. Et le partage de la mémoire entre deux processus est très simple avec un espace d'adressage unique. Il suffit de manipuler la protection mémoire pour qu'elle autorise aux deux programmes d'accéder à un même segment. Les adresses utilisées par les deux programmes sont les mêmes.

La protection mémoire avec des registres de base et limite modifier

Chaque programme commence à une adresse précise et se termine à une autre. Les accès mémoire d'un programme doivent donc rester dans cet intervalle d'adresse. Pour cela, le système d'exploitation mémorise l'adresse de départ et de fin de chaque segment. Le processeur utilise ces deux adresses pour vérifier que les accès mémoire sont dans les clous. Quand il démarre un programme, le système d'exploitation charge ces deux adresses dans le processeur, dans deux registres spécialisés : le registre de base pour l'adresse du début du segment, le registre limite pour l'adresse de fin du segment.

Les deux registres servent à vérifier si un programme qui lit/écrit de la mémoire au-delà de sa partition. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà de la partition qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. A noter que le registre de base est parfois utilisé pour la relocation matérielle, à savoir que le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire. La relocation garantit que les adresses utilisées commencent à l'adresse de base, grâce au registre de base. Du moins, c'est le cas si l'addition ne déborde pas au-delà de la mémoire physique, tout débordement signifiant erreur de protection mémoire.

Les deux registres ne sont accessibles que pour le système d'exploitation et ne sont généralement accessibles qu'en espace noyau. Lorsque le processeur exécute un programme, ou reprend son exécution, il charge les limites des partitions dans ces deux registres. Ces deux registres doivent être sauvegardés en cas d'interruption, mais pas d'appel de fonction.

Les clés de protection modifier

Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire assez simple. Ce mécanisme de protection attribue à chaque programme une clé de protection, qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.

Un espace d'adressage par processus : l'abstraction matérielle de processus modifier

L'usage de partitions mémoire est assez complexe, mais est encore en cours aujourd'hui, sous des formes plus ou moins élaborées. Mais de nos jours, la relocation est gérée autrement. La différence principale est que l'on a pas un espace d'adressage partagé entre plusieurs programmes. Grâce à diverses fonctionnalités du processeur, chaque programme a son propre espace d'adressage rien que pour lui ! Le fait que chaque programme ait son propre espace d'adressage ne porte pas de nom, mais on pourrait l'appeler abstraction matérielle des processus. Nous verrons comment elle est implémentée dans la section suivante, qui porte sur l'abstraction mémoire, mais nous pouvons donner quelques explications sur l'abstraction des processus.

Le noyau est mappé en mémoire modifier

Le noyau du système d'exploitation a son propre espace d'adressage, séparé des autres. C'est une sécurité qui isole le noyau et les programmes, et les force à communiquer à travers des appels systèmes. Cependant, sachez que cette isolation n'est pas parfaite, volontairement. Dans l'espace d'adressage d'un programme, les adresses hautes sont remplies avec une partie du noyau ! Le programme peut utiliser cette portion du noyau avec des appels systèmes simplifiés, qui ne sont pas des interruptions, mais des appels systèmes couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire). L'idée est d'éviter des appels systèmes trop fréquents. Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.

L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. On retrouve la même chose que ce qu'on avait avec un espace d'adressage unique, partagé entre un OS et un programme. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes 64 bits, la situation est plus complexe.

Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits.

Sur les systèmes x86 64 bits, l'espace d'adressage est en théorie coupé en deux, la moitié basse pour le programme, la moitié haute pour le noyau. Sauf que les systèmes x86 64 bits actuels utilisent des adresses de 48 bits, le bus mémoire ne gère pas plus. Il manque donc 64 - 48 = 16 bits d'adresses, qui ne peuvent pas être utilisés pour adresser quoi que ce soit. L'espace d'adressage est donc de 64 bits, mais est coupé en trois parties, comme illustré ci-dessous. Les deux premières parties ont des adresses dont les 16 bits de poids fort sont identiques : soit ils sont tous à 0, soit tous à 1. Il s'agit des adresses canoniques. Les adresses canoniques basses ont leurs 16 bits de poids fort à 0, elles sont attribuées au programme. Les adresses canoniques hautes ont leurs 16 bits de poids fort à 1, elles sont attribuées au noyau. Les adresses non-canoniques ne sont pas accessibles, y accéder déclenche la levée d'une exception matérielle. Les futurs systèmes x86 devraient passer à des adresses de 57 bits.

Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.
Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 57 bits.

La communication inter-processus et les threads modifier

L'abstraction des processus fait que deux programmes ne peuvent pas se marcher sur les pieds. L'isolation des processus est donc garantie. Mais un gros problème est alors celui de la communication inter-processus, à savoir faire communiquer plusieurs processus entre eux. Il arrive régulièrement que des applications doivent coopérer pour faire leur travail, et échanger des données. L'isolation des processus met des bâtons dans les roues de ce partage.

Un moyen pour est de partager une portion de mémoire, accessible aux deux processus. Par exemple, l'un peut écrire dans cette zone, l'autre peut lire dedans. Mais l'isolation des processus fait que le partage de la mémoire est plus compliqué. Imaginez que deux programmes veulent partager une même zone de mémoire, pour échanger des données. La portion de mémoire sera placée à une certaine adresse physique en mémoire RAM. Mais cette adresse ne sera pas la même pour les deux programmes, vu qu'ils sont dans deux espaces d'adressage distincts. On peut résoudre ce problème, mais avec des mécanismes assez compliqués, dépendant des techniques de "mémoire virtuelle" qu'on verra au chapitre suivant.

Une autre méthode est de regrouper plusieurs programmes dans un seul processus, afin qu'ils partagent le même morceau de mémoire. Les programmes portent alors le nom de threads. Les threads d'un même processus partagent le même espace d'adressage. Ils partagent généralement certains segments : ils se partagent le code, le tas et les données statiques. Par contre, chaque thread dispose de sa propre pile d'appel.

Distinction entre processus mono et multi-thread.

Les identifiants de processus intégrés au processeur modifier

Pour simplifier la gestion de plusieurs processus, le processeur numérote chaque espace d'adressage. Le numéro est donc spécifique à chaque processus, ce qui fait qu'il est appelé les identifiants de processus CPU, aussi appelés identifiants d'espace d'adressage. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. Le registre d'identifiant est modifié à chaque changement de processus, à chaque commutation de contexte.

L'identifiant de processus CPU est utilisé lors des accès mémoire, afin de ne se pas se tromper d'espace d'adressage. Il est utilisé pour les accès au cache, entre autres. Il sert à savoir si une donnée dans le cache appartient à tel ou tel processus, ce qui est utile pour la protection mémoire. Sans cela, chaque processus peut en théorie accéder à des données qui ne sont pas à lui dans le cache, en envoyant l'adresse adéquate. Nous en reparlerons dans le chapitre sur les mémoires caches.

Un défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.


Pour introduire ce chapitre, nous devons faire un rappel sur le concept d'espace d'adressage. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage.

L'espace d'adressage est un ensemble d'adresses géré par le processeur, et on s'attend à ce qu'il y ait correspondance avec les adresses envoyées à la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas. Mais sachez qu'il existe des techniques d'abstraction mémoire qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM.

L'abstraction mémoire fait que les adresses de l'espace d'adressage sont des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d'adresses logiques, alors que les adresses de la mémoire RAM sont appelées adresses physiques. Pour implémenter l'abstraction mémoire, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques. Ce circuit est placé dans le processeur et est appelé la Memory Management Unit (MMU).

L'abstraction mémoire implémente de nombreuses fonctionnalités complémentaires modifier

Leur utilité n'est pas évidente, mais sachez que l'abstraction matérielle est très utile et que tous les processeurs modernes la prennent en charge. Elles servent notamment à implémenter la relocation directement dans le processeur, à implémenter l'abstraction matérielle des processus. Mais elle sert aussi pour d'autres fonctionnalités, comme la mémoire virtuelle, que nous aborderons dans ce qui suit.

La plupart de ces fonctionnalités manipulent la relation entre adresses logiques et physique. Dans le cas le plus simple, une adresse logique correspond à une seule adresse physique. Mais l'abstraction mémoire et le partage de mémoire entre programmes ne respectent pas cette règle.

L'abstraction matérielle des processus modifier

Dans le chapitre précédent, nous avions vu l'abstraction matérielle des processus, une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.

Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.

Les adresses physiques qui partagent la même adresse logique sont alors appelées des adresses homonymes. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :

  • La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
  • La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.

Le partage de la mémoire entre programmes modifier

Dans le chapitre précédent, nous avions vu qu'il est possible de lancer plusieurs programmes en même temps, mais que ceux-ci doivent se partager la mémoire RAM. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes de protection mémoire, pour isoler les programmes les uns des autres.

Il faut cependant que la protection mémoire permette à deux programmes de partager des morceaux de mémoire, ce qui est nécessaire pour implémenter les mécanismes de communication inter-processus des systèmes d'exploitation modernes. Ce partage de mémoire est rendu très compliqué par l'abstraction mémoire, mais est parfaitement possible.

Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Les adresses logiques sont alors appelées des adresses synonymes. Lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique.

La mémoire virtuelle : quand l'espace d'adressage est plus grand que la mémoire modifier

Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.

Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.

Les techniques de mémoire virtuelle font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.

Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le swapfile ou fichier de swap, qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.

Mémoire virtuelle et fichier de Swap.

Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au swapfile et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.

Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.

L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le swapfile.

On perd du temps dans les copies de données entre RAM et swapfile, mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.

Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.

L'extension d'adressage modifier

Une autre fonctionnalité rendue possible par l'abstraction mémoire est l'extension d'adressage. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.

Dans le chapitre précédent, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.

Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.

Extension de l'espace d'adressage

Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.

Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.

La relocation matérielle modifier

Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des segments, ou encore des partitions mémoire. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.

Espace d'adressage segmenté.

Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la table de segment, un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un descripteur de segment qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.

La relocation avec la relocation matérielle : le registre de base modifier

Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la relocation, et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.

Relocation.

La relocation matérielle va de pair avec les segments, mais la relocation est faite par le processeur. La relocation matérielle traduit les adresses logiques en adresses physiques, directement en matériel. La méthode de relocation matérielle associée s'appelle la segmentation simple. Peut-être avez-vous entendu dire qu'il s'agit d'une technique de mémoire virtuelle. Et ce n'est pas faux ! En fait, il existe plusieurs versions de la segmentation, certaines plus puissantes que les autres. Les plus simples se contentent de découper la RAM en segments et de faire la relocation en matériel, mais ne gèrent pas la mémoire virtuelle. Les versions élaborées incorporent de la protection mémoire et/ou la mémoire virtuelle.

La relocation est intégrée dans le processeur par l'intégration d'un registre : le registre de base, aussi appelé registre de relocation. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.

Registre de base de segment.

Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.

Traduction d'adresse avec la relocation matérielle.

Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.

La protection mémoire avec la relocation matérielle : le registre limite modifier

Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire. Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, en comparant l'adresse accédée à l'adresse de base et l'adresse maximale, l'adresse limite.

Limiter la taille d'un segment demande soit de mémoriser sa taille, soit de mémoriser l'adresse limite (l'adresse de fin de segment, l'adresse limite à ne pas dépasser). Les deux sont possibles et marchent parfaitement, le choix entre les deux solutions est une pure question de préférence. A la rigueur, la vérification des débordements est légèrement plus rapide si on utilise l'adresse de fin du segment. Précisons que l'adresse limite est une adresse logique, le segment commence toujours à l'adresse logique zéro.

Pour cela, la table des segments doit être modifiée. Au lieu de ne contenir que l'adresse de base, elle contient soit l'adresse maximale du segment, soit la taille du segment. En clair, le descripteur de segment est enrichi avec l'adresse limite. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.

De plus, le processeur se voit ajouter un registre limite, qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.

Techniquement, il y a une petite différence de vitesse entre utiliser la taille et l'adresse maximale. Vérifier les débordements avec la taille demande juste de comparer la taille avec l'adresse logique, avant relocation, ce qui peut être fait en parallèle de la relocation. Par contre, l'adresse limite est comparée à une adresse physique, ce qui demande de faire la relocation avant la vérification, ce qui prend un peu plus de temps. Mais l'impact sur les performances est des plus mineurs.

Registre limite

Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.

Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.

La mémoire virtuelle avec la relocation matérielle modifier

Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le swapfile, pour faire de la place.

Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le swapfile. Pour cela, il faut modifier la table des segments, afin d'ajouter un bit de swap qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le swapfile et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le swapfile est le fait d'une structure de données séparée de la table des segments.

L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le swapfile. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.

Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.

L'extension d'adressage avec la relocation matérielle modifier

Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.

L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.

Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.

La segmentation en mode réel des processeurs x86 modifier

Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.

Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le mode réel, et la nouvelle forme de segmentation fut appelée le mode protégé. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Mais nous ne pouvons pas parler du mode protégé à ce moment du cours, ni le voir en même temps que le mode réel, à cause d'une différence très importante : l'interprétation des adresses change complètement, comme on le verra dans la suite du cours. Les registres de segment ne mémorisent pas des adresses de base en mode protégé, la relocation se fait de manière moins directe. Nous allons voir le mode réel seul, dans cette section.

Les segments en mode réel modifier

Typical computer data memory arrangement

L'idée de la segmentation en mode réel est d'offrir à chaque programme plusieurs espaces d'adressage. Pour cela, la segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.

  • Le segment text, qui contient le code machine du programme, de taille fixe.
  • Le segment data contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
  • Le segment pour la pile, de taille variable.
  • le reste est appelé le tas, de taille variable.

Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.

Chaque segment subit la relocation indépendamment des autres. Pour cela, la meilleure solution est d'utiliser plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.

Les registres de segments en mode réel modifier

Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (code segment), DS (data segment), SS (Stack segment), et ES (Extra segment). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données.

Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.

Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.

Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.

La traduction d'adresse en mode réel modifier

La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des offsets. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.

Table des segments dans un banc de registres.

L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.

  0000 0110 1110 11110000 Registre de segment - 16 bits, décalé de 4 bits vers la gauche
+      0001 0010 0011 0100 Décalage/Offset 16 bits
  0000 1000 0001 0010 0100 Adresse finale 20 bits

Le mode réel du 8086 avait une particularité : les adresses calculées ne dépassaient pas 20 bits. Si l'addition de la base du segment et de l'offset déborde, alors les bits au-delà du vingtième sont perdus. Dit autrement, le calcul de l'adresse physique utilise l'arithmétique modulaire sur le 8086.

Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086.Par contre, les offsets restent de 16 bits. L'additionneur du 80286 ne gère pas les débordements comme un 8086 et calcule les bits en trop, au-delà du 20ème. En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Pour résoudre ce problème, certains fabricants de carte mère mettaient à 0 le 20ème fil du bus d'adresse, quand le programmeur leur demandait. La carte mère avait un petit interrupteur qui pouvait être activé de manière à activer ou non la mise à 0 du 20ème bit d'adresse.

Le 80386 ajouta deux registres de segment, les registres FS et GS. Les processeurs 386 gérent un mode réel similaire à celui du 286, mais émulé. Un autre mode de segmentation est ajouté avec le 386 : le mode virtual 8086. Il permet d’exécuter des programmes en mode réel, pendant que le système d'exploitation s’exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire.

Les processeurs x86 64 bits désactivent la segmentation en mode 64 bits.

L'occupation de l'espace d'adressage par les segments modifier

Segments qui se recouvrent en mode réel.

Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et cela ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+offset pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+offset différents.

Vous remarquerez aussi qu'avec 4 registres de segment et des segments de 64 KB, on peut remplir au maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. La raison à cela est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Mais ce n'était pas le cas à l'époque, le DOS était un OS mono-programmé. La raison est tout autre, comme nous allons le voir dans ce qui suit.

La segmentation en mode réel accepte plusieurs segments par programme modifier

Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code, et faire de même pour les données. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme ait besoin de plus juste stocker son code machine ou ses données. La seule contrainte à respecter est que tous les segments doivent être chargés en RAM, il n'y a pas de mémoire virtuelle en mode réel. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.

Il faut alors changer de segment à la volée, en remplaçant le segment de code/données par un autre, en modifiant les registres de segment. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire. Il est possible d'écrire dedans, sans restriction particulière, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.

Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les near jumps et les far jumps. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.

Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le near call est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le far call, la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un far call met à jour le registre CS avec l'adresse de base, ce qui fait que les far call sont plus lents que les near call. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées near return et far return. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.

La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés near pointer et far pointer. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l'offset.

Les modèles mémoire en mode réel modifier

Il n'était pas nécessaire d'avoir 4 à 6 segments différents, un programme pouvait se débrouiller avec seulement 1 ou 2 segments. D'autres programmes faisaient l'inverse, à savoir qu'ils avaient plus de 6 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appélées des modèle mémoire, et il y en a en tout 6. En voici la liste :

Modèle mémoire Configuration des segments Configuration des registres Pointeurs utilisés Branchements utilisés
Tiny* Segment unique pour tout le programme CS=DS=SS near uniquement near uniquement
Small Segment de donnée séparé du segment de code, pile dans le segment de données DS=SS near uniquement near uniquement
Medium Plusieurs segments de code unique, un seul segment de données CS, DS et SS sont différents near et far near uniquement
Compact Segment de code unique, plusieurs segments de données CS, DS et SS sont différents near uniquement near et far
Large Plusieurs segments de code, plusieurs segments de données CS, DS et SS sont différents near et far near et far

La segmentation avec une table des segments modifier

La segmentation avec une table des segments est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 de nos PCs, à partir du 286 d'Intel. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Sauf que le nombre de segments géré par le processeur est plus important. Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.

Pourquoi plusieurs segments par programme ? modifier

La taille et le nombre des segments varie grandement d'un processeur à l'autre. Certains ne supportent qu'un petit nombre de segments par processus, les segments sont alors assez gros. D'autres utilisent un grand nombre de segments, qui sont individuellement plus petits. La différence entre ces deux méthodes permet de faire la différence entre segmentation à granularité grossière et segmentation à granularité fine.

La segmentation à granularité grossière modifier

L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle devient évidente quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.

L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l'overlaying. Le programme était découpé en plusieurs morceaux, appelés des overlays. Certains blocs overlays en permanence en RAM, mais d'autres étaient soit chargés en RAM, soit stockés sur le disque dur. Le chargement des overlays ou leur sauvegarde sur le disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.

Overlay Programming

Avec la segmentation, un programme peut utiliser la technique des overlays, mais avec l'aide du matériel. Il suffit de mettre chaque overlay dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du swapping est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/overlays de lui-même. Sans cela, la segmentation n'est pas très utile.

La segmentation à granularité fine modifier

La segmentation à granularité fine pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.

Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.

La relocation avec la segmentation modifier

La segmentation avec une table des segment utilise, comme son nom l'indique, une ou plusieurs tables des segments. Pour rappel, celle-ci est une table qui mémorise, pour chaque segment, quelles sont ses adresses de base et limite. Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. La solution la plus pratique est d'utiliser une table de segment par processus/programme.

La traduction d'adresse additionne l'adresse de base du segment à chaque accès mémoire. Sauf que la présence de plusieurs segments change la donne. On n'a plus une adresse de base, mais une par segment associé au programme. Il faut donc sélectionner la bonne adresse de base, le bon segment. Pour cela, les segments sont numérotés, le nombre s'appelant un indice de segment, appelé sélecteur de segment dans la terminologie Intel. Les segments sont placés dans la table des segments les uns après les autres, dans l'ordre de numérotation. La table des segments est donc un tableau de segment, et l'indice de segment n'est autre que l'indice du segment dans ce tableau.

Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. A la place, les registres de segment mémorisent des sélecteurs de segment. De fait, tout se passe comme si les registres de relocation, qui mémorisent une adresse de base, étaient remplacés par des registres qui mémorisent des sélecteurs de segment. L'adresse manipulée par le processeur se déduit en combinant l'indice de segment qui sélectionne le segment voulu, avec un décalage (offset) qui donne la position de la donnée dans ce segment.

L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Et il faut dire que les performances s'en ressentent.

Traduction d'adresse avec une table des segments.

Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le pointeur de table. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.

Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).

Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.

La protection mémoire : les accès hors-segments modifier

Comme avec la relocation matérielle, le processeur utilise l'adresse ou la taille limite pour vérifier si l'accès mémoire ne déborde pas en-dehors du segment en cours. Pour cela, le processeur compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. L'information est lue depuis la table des segments à chaque accès.

Traduction d'adresse avec vérification des accès hors-segment.

Par contre, une nouveauté fait son apparition avec la segmentation : la gestion des droits d'accès. Chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à trois bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.

La mémoire virtuelle avec la segmentation modifier

La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.

Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.

Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.

L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.

L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.

Le partage de segments modifier

Il est possible de partager un segment entre plusieurs applications. Cela peut servir quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.

Le partage de segment avec des tables des segments locales modifier

La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.

Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.

Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.

Illustration du partage d'un segment entre deux applications.

Le partage de segment avec une table des segments globale modifier

Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.

Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.

L'extension d'adresse avec la segmentation modifier

L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : il y a un espace d'adressage par segment, plusieurs segments par programme.

Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.

Le mode protégé des processeurs x86 modifier

L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de mode protégé. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.

Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.

Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse de l'adresse physique envoyée sur le bus, de l'adresse de base du segment ou des offsets. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.

Les tables des segments des processeurs x86 modifier

Les processeurs gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée netre tous les processus.

La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.

La table locale gére les segments de son processus. Il est possible d'avoir plusieurs tables locales, mais une seule doit être active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale.

Les descripteurs de segments des processeurs x86 modifier

Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.

Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :

  • le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
  • deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
  • un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
  • un champ Type qui contient les bits suivants : un bit E qui indique si le segment contient du code exécutable ou non, le bit RW qui indique s'il est en lecture seule ou non, les bits A et DC assez spécifiques.

En haut à gauche, en bleu, on trouve deux bits :

  • Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
  • Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
Segment Descriptor

Les sélecteurs de segments sur les processeurs x86 modifier

Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits. Les 16 bits sont organisés comme suit :

  • 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
  • un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
  • deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
Sélecteur de segment 16 bit.

En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.

La segmentation sur les processeurs Burrough B5000 et plus modifier

Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.

Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.

La table des segments modifier

La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la Program Reference Table, ou PRT.

La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un offset. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.

Les descripteurs de segments modifier

La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.

Chaque entrée de la PRT contient un tag, une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un bit de présence qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.

L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l'overlay, le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
Structure d'un mot mémoire sur le B6700.

Les architectures à capacités modifier

Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.

Le partage de la mémoire sur les architectures à capacités modifier

Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.

Partage des segments avec la segmentation

A l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la table des segments globale, ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quelque soit le processus qui l'utilise.

Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.

Les capacités sont des pointeurs protégés modifier

Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des capacités, des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.

Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.

Comparaison entre capacités et adresses segmentées

Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.

La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).

La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.

Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.

Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.

La liste des capacités modifier

Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une liste de capacités, appelée la C-list. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la C-list mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.

Architectures à capacité

La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.

Pour protéger la C-list en écriture, la solution la plus utilisée consiste à placer la C-list dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les C-list, les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.

L'usage d'une C-list permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la C-list qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.

Les capacités dispersées, les architectures taguées modifier

Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.

Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une architecture à tags, ou tagged architectures. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.

Architectures à capacité sans liste de capacité

L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.

Les registres de capacité modifier

Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. A la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.

Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+offset.

Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.

Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.

Le recyclage de mémoire matériel modifier

Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la garbage collection, ou recyclage de la mémoire, à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.

Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.

Pour éviter cela, les langages de programmation actuels incorporent des garbage collectors, des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.

Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les garbage collectors scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur tag.

Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.

L'intel iAPX 432 modifier

Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.

La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.

Les segments prédéfinis de l'Intel iAPX 432 modifier

L'Intel iAPX432 gére plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !

Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.

Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :

  • Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des threads.
  • Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
  • Des segments de domaine, pour les modules ou librairies dynamiques.
  • Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
  • Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
  • Et bien d'autres encores.

Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.

L'Intel 432 possédait dans ses circuits un garbage collector matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.

Le support de la segmentation sur l'Intel iAPX 432 modifier

La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une Object Table Directory, qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des Object Table. Il y a plusieurs Object Table, typiquement une par processus. Plusieurs processus peuvent partager la même Object Table. Les Object Table peuvent être swappées, mais pas l'Object Table Directory.

Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle Object Table utiliser, et l'indice du segment dans cette Object Table. Le premier indice adresse l'Object Table Directory et récupère un descripteur de segment qui pointe sur la bonne Object Table. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette Object Table. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des Access Descriptors dans la documentation officielle.

Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 Object Table différentes dans l'Object Table Directory, et chaque Object Table contient 4096 segments.

Le jeu d'instruction de l'Intel iAPX 432 modifier

L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.

Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, éxecutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.

le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.

Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.

  • Le premier est l'opcode de l'instruction.
  • Le champ reference, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
  • Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
  • Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
Encodage des instructions de l'Intel iAPX-432.

Le support de l'orienté objet sur l'Intel iAPX 432 modifier

L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des domain objects, qui correspondent à une classe. Un domain object est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.

L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le domain object, et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au domain object d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.

Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.

Conclusion modifier

Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :

Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :

La pagination modifier

Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des pages mémoires. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.

L'espace d'adressage est découpé en pages logiques, alors que la mémoire physique est découpée en pages physique de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.

Principe de la pagination.

Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.

Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.

La mémoire virtuelle : le swapping et le remplacement des pages mémoires modifier

Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une table des pages. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit Valid qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des entrées de la table des pages

Table des pages.

De plus, le système d'exploitation conserve une liste des pages vides. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.

Les défauts de page modifier

Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit Valid et l'adresse physique. Si le bit Valid est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un défaut de page. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.

Il existe deux types de défauts de page : mineurs et majeurs. Un défaut de page mineur a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire, ce qui correspond à une allocation paresseuse parfois utilisée par les OS, dont nous parlerons plus bas. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type copy-on-write, etc.

Un défaut de page majeur a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.

Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs registres de statut qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.

Le remplacement des pages modifier

Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.

Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le swapfile. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le swapfile si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le swapfile.

Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un dirty bit à chaque entrée de la table des pages, juste à côté du bit Valid. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.

Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le bit swappable.

Les algorithmes de remplacement des pages pris en charge par l'OS modifier

Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et swapfile. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible.

Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.

Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.

Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.

L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.

L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.

L'algorithme le plus utilisé de nos jours est l'algorithme NRU (Not Recently Used), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les pages froides et les pages chaudes. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.

Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le bit Accessed. La différence avec le bit dirty est que le bit dirty est mis à jour uniquement lors des écritures, alors que le bit Accessed l'est aussi lors d'une lecture. Uen lecture met à 1 le bit Accessed, mais ne touche pas au bit dirty. Les écritures mettent les deux bits à 1.

Implémenter l'algorithme NRU demande juste de mettre à jour le bit Accessed de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit Accessed à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits Accessed. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.

La protection mémoire avec la pagination modifier

Avec la pagination, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.

Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.

Une amélioration de cette protection est la technique dite du Write XOR Execute, abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.

La traduction d'adresse avec la pagination modifier

Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le numéro de page. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le décalage, ou encore l'offset.

Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.

Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.

Traduction d'adresse avec la pagination.

Les tables des pages simples modifier

Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.

Table des pages.

La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.

Address translation (32-bit)

Les tables des pages inversées modifier

Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.

Pour résoudre ce problème, on a inventé les tables des pages inversées. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.

Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.

Table des pages inversée.

Les tables des pages multiples par espace d'adressage modifier

Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.

Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, ele est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.

L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.

L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un offset. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l'offset pour obtenir l'adresse physique finale.

Table des pages hiérarchique.

On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.

L'exemple des processeurs x86 modifier

Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie physical adress extension, dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.

Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme offset. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).

X86 Paging 4M

Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (page directory), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).

X86 Paging 4K

La technique du physical adress extension (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.

La table des pages gardait 2 niveaux pour les pages larges en PAE.

X86 Paging PAE 2M

Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.

X86 Paging PAE 4K

En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.

X86 Paging 64bit

Les circuits liés à la gestion de la table des pages modifier

En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le translation lookaside buffer, ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.

MMU avec une TLB.

Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les page table walkers (PTW), qui s'occupent eux-mêmes du défaut.

Les page table walkers contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des tampons de PTW (PTW buffers).

L'abstraction matérielle des processus : une table des pages par processus modifier

Mémoire virtuelle

Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.

L'usage d'une table des pages unique avec un identifiant de processus dans chaque entrée modifier

La première solution n'utilise qu'une seule table des pages, mais chaque entrée est associée à un processus. Pour cela, chaque entrée contient un identifiant de processus, un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide.

La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. L'intérêt n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Dans ce cas, la correspondance est mémorisée dans une entrée, mais sans identifiant de processus. A la place, l'entrée contient un bit global, qui précise que cette correspondance est valide pour tous les processus. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.

Un défaut de cette méthode est que le partage d'une page entre plusieurs processus est presque impossible. Impossible de partager une page avec seulement certains processus et pas d'autres : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.

L'usage de plusieurs tables des pages modifier

Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.

Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.

Tables des pages de plusieurs processus.

La taille des pages modifier

La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des pages larges. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.

Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type copy-on-write.

Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.

Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un offset : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.

Les entrées de la table des pages modifier

Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit valid pour la mémoire virtuelle, des bits dirty et accessed utilisés par l'OS, des bits de protection mémoire, un bit global et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.

  • Elle contient d'abord le numéro de page physique.
  • Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
  • Le bit G est le bit global.
  • Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
  • Le bit D est le bit dirty.
  • Le bit A est le bit accessed.
  • Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
  • Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache write-through pour cette page).
  • Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
  • Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
  • Le bit P est le bit valid.
Table des pages des processeurs Intel 32 bits.

Comparaison des différentes techniques d'abstraction mémoire modifier

Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.

Avec abstraction mémoire Sans abstraction mémoire
Relocation matérielle Segmentation en mode réel (x86) Segmentation, général Architectures à capacités Pagination
Abstraction matérielle des processus Oui, relocation matérielle Oui, liée à la traduction d'adresse Impossible
Mémoire virtuelle Non, sauf émulation logicielle Oui, gérée par le processeur et l'OS Non, sauf émulation logicielle
Extension de l'espace d'adressage Oui : registre de base élargi Oui : adresse de base élargie dans la table des segments Physical Adress Extension des processeurs 32 bits Commutation de banques
Protection mémoire Registre limite Aucune Registre limite, droits d'accès aux segments Gestion des droits d'accès aux pages Possible, méthodes variées
Partage de mémoire Non Segment partagés Pages partagées Possible, méthodes variées

Les différents types de segmentation modifier

La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.

La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.

La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.

Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.

Segmentation versus pagination modifier

Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.

L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.

Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.

Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.


La micro-architecture modifier

Dans le chapitre sur le langage machine, on a vu notre processeur comme une espèce de boite noire contenant des registres qui exécutait des instructions les unes après les autres et pouvait accéder à la mémoire. Mais on n'a pas encore vu comment celui-ci était organisé et comment celui-ci fait pour exécuter une instruction. Pour cela, il va falloir nous attaquer à la micro-architecture du processeur. C'est le but de ce chapitre : montrer comment les grands circuits de notre processeur sont organisés et comment ceux-ci permettent d’exécuter une instruction. On verra que notre processeur est très organisé et est divisé en plusieurs grands circuits qui effectuent des fonctions différentes.

L'exécution d'une instruction modifier

Le but d'un processeur, c'est d’exécuter une instruction. Cela nécessite de faire quelques manipulations assez spécifiques et qui sont toutes les mêmes quel que soit l'ordinateur. Pour exécuter une instruction, notre processeur va devoir faire son travail en effectuant des étapes bien précises.

Le cycle d'exécution d'une instruction modifier

Les trois cycles d'une instruction.

Pour exécuter une instruction, le processeur va effectuer trois étapes :

  • le processeur charger l'instruction depuis la mémoire : c'est l'étape de chargement (Fetch) ;
  • ensuite, le processeur « étudie » la suite de bits de l'instruction et en déduit quelle est l'instruction à éxecuter : c'est l'étape de décodage (Decode) ;
  • enfin, le processeur exécute l'instruction : c'est l'étape d’exécution (Execute).

On verra plus tard dans le cours qu'une quatrième étape peut être ajoutée : l'étape d'interruption. Celle-ci permet de gérer des fonctionnalités du processeur nommées interruptions. Nous en parlerons dans le chapitre sur la communication avec les entrées-sorties.

Les micro-instructions modifier

Ces trois étapes ne s'effectuent cependant pas d'un seul bloc. Chaque de ces étapes est elle-même découpée en plusieurs sous-étapes, qui va échanger des données entre registres, effectuer un calcul, ou communiquer avec la mémoire. Pour l'étape de chargement, on peut être sûr que tous les processeurs vont faire la même chose : il n'y a pas 36 façons pour lire une instruction depuis la mémoire. Même chose pour la plupart des processeur, pour l'étape de décodage. Mais cela change pour l'étape d’exécution : toutes les instructions n'ont pas les mêmes besoins suivant ce qu'elles font ou leur mode d'adressage. Voyons cela avec quelques exemples.

Commençons par prendre l'exemple d'une instruction de lecture ou d'écriture en mode d'adressage absolu. Vu son mode d'adressage, l'instruction va indiquer l'adresse à laquelle lire dans sa suite de bits qui la représente en mémoire. L’exécution de l'instruction se fait donc en une seule étape : la lecture proprement dite. Mais si l'on utilise des modes d'adressages plus complexes, les choses changent un petit peu. Reprenons notre instruction Load, mais en utilisant une mode d'adressage utilisé pour des données plus complexe. Par exemple, on va prendre un mode d'adressage du style Base + Index. Avec ce mode d'adressage, l'adresse doit être calculée à partir d'une adresse de base, et d'un indice, les deux étant stockés dans des registres. En plus de devoir lire notre donnée, notre instruction va devoir calculer l'adresse en fonction du contenu fourni par deux registres. L'étape d’exécution s'effectue dorénavant en deux étapes assez différentes : une implique un calcul d'adresse, et l'autre implique un accès à la mémoire.

Prenons maintenant le cas d'une instruction d'addition. Celle-ci va additionner deux opérandes, qui peuvent être soit des registres, soit des données placées en mémoires, soit des constantes. Si les deux opérandes sont dans un registre et que le résultat doit être placé dans un registre, la situation est assez simple : la récupération des opérandes dans les registres, le calcul, et l'enregistrement du résultat dans les registres sont trois étapes distinctes. Maintenant, autre exemple : une opérande est à aller chercher dans la mémoire, une autre dans un registre, et le résultat doit être enregistré dans un registre. On doit alors rajouter une étape : on doit aller chercher la donnée en mémoire. Et on peut aller plus loin en allant cherche notre première opérande en mémoire : il suffit d'utiliser le mode d'adressage Base + Index pour celle-ci. On doit alors rajouter une étape de calcul d'adresse en plus. Ne parlons pas des cas encore pire du style : une opérande en mémoire, l'autre dans un registre, et stocker le résultat en mémoire.

Bref, on voit bien que l’exécution d'une instruction s'effectue en plusieurs étapes distinctes, qui vont soit faire un calcul, soit échanger des données entre registres, soit communiquer avec la RAM. Chaque étape s'appelle une micro-opération, ou encore µinstruction. Toute instruction machine est équivalente à une suite de micro-opérations exécutée dans un ordre précis. Dit autrement, chaque instruction machine est traduite en suite de micro-opérations à chaque fois qu'on l’exécute. Certaines µinstructions font un cycle d'horloge, alors que d'autres peuvent prendre plusieurs cycles. Un accès mémoire en RAM peut prendre 200 cycles d'horloge et ne représenter qu'une seule µinstruction, par exemple. Même chose pour certaines opérations de calcul, comme des divisions ou multiplication, qui correspondent à une seule µinstruction mais prennent plusieurs cycles.

Micro-operations

La micro-architecture d'un processeur modifier

Conceptuellement, il est possible de segmenter les circuits du processeur en circuits spécialisés : des circuits chargés de faire des calculs, d'autres chargés de gérer les accès mémoires, etc. Ces circuits sont eux-mêmes regroupés en deux entités : le chemin de données et l'unité de contrôle. Le tout est illustré ci-contre.

  • Le chemin de données est l'ensemble des composants où circulent les données, là où se font les calculs, là où se font les échanges entre mémoire RAM et registres, etc. Il contient un circuit pour faire les calculs, appelé l'unité de calcul, les registres et un circuit de communication avec la mémoire. On l'appelle ainsi parce que c'est dans ce chemin de données que les données vont circuler et être traitées dans le processeur.
  • L’unité de contrôle charge et interprète les instructions, pour commander le chemin de données. Elle est en charge du chargement et du décodage de l'instruction. Elle regroupe un circuit chargé du Fetch, et un décodeur chargé de l'étape de Decode.

Le chemin de données modifier

Pour effectuer ces calculs, le processeur contient un circuit spécialisé : l'unité de calcul. De plus, le processeur contient des registres, ainsi qu'un circuit d'interface mémoire. Les registres, l'unité de calcul, et l'interface mémoire sont reliés entre eux par un ensemble de fils afin de pouvoir échanger des informations : par exemple, le contenu des registres doit pouvoir être envoyé en entrée de l'unité de calcul, pour additionner leur contenu par exemple. Ce groupe de fils forme ce qu'on appelle le bus interne du processeur. L'ensemble formé par ces composants s’appelle le chemin de données.

Chemin de données

L'unité de contrôle modifier

Si le chemin de données s'occupe de tout ce qui a trait aux donnés, il est complété par un circuit qui s'occupe de tout ce qui a trait aux instructions elles-mêmes. Ce circuit, l'unité de contrôle va notamment charger l'instruction dans le processeur, depuis la mémoire RAM. Il va ensuite configurer le chemin de données pour effectuer l'instruction. Il faut bien contrôler le mouvement des informations dans le chemin de données pour que les calculs se passent sans encombre. Pour cela, l'unité de contrôle contient un circuit : le séquenceur. Ce séquenceur envoie des signaux au chemin de données pour le configurer et le commander.

Il est évident que pour exécuter une suite d'instructions dans le bon ordre, le processeur doit savoir quelle est la prochaine instruction à exécuter : il doit donc contenir une mémoire qui stocke cette information. C'est le rôle du registre d'adresse d'instruction, aussi appelé program counter. Cette adresse ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples. Généralement, on profite du fait que le programmeur/compilateur place les instructions les unes à la suite des autres en mémoire, dans l'ordre où elles doivent être exécutées. Ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction chargée au program counter.

Intérieur d'un processeur

Mais sur d'autres processeurs, chaque instruction précise l'adresse de la suivante. Ces processeurs n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau d'argent. Sur de tels processeurs, chaque instruction précise quelle est la prochaine instruction, directement dans la suite de bit représentant l'instruction en mémoire. Les processeurs de ce type contiennent toujours un registre d'adresse d'instruction, pour faciliter l’interfaçage avec le bus d'adresse. La partie de l'instruction stockant l'adresse de la prochaine instruction est alors recopiée dans ce registre, pour faciliter sa copie sur le bus d'adresse. Mais le compteur ordinal n'existe pas. Sur des processeurs aussi bizarres, pas besoin de stocker les instructions en mémoire dans l'ordre dans lesquelles elles sont censées être exécutées. Mais ces processeurs sont très très rares et peuvent être considérés comme des exceptions à la règle.

Encodage d'une instruction sur un processeur sans Program Counter.

Des processeurs vendus en kit aux premiers microprocesseurs modifier

Un processeur est un circuit assez complexe et qui utilise beaucoup de transistors. Avant les années 1970, il n'était pas possible de produire un processeur en un seul morceau. Impossible de mettre un processeur dans un seul boitier. Les tout premiers processeurs étaient fabriqués porte logique par porte logique et comprenaient plusieurs milliers de boitiers reliés entre eux. Par la suite, les progrès de la miniaturisation permirent de faire des pièces plus grandes. L'invention du microprocesseur permis de placer tout le processeur dans un seul boitier, une seule puce électronique.

Avant l'invention du microprocesseur modifier

Avant l'invention du microprocesseur, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Le processeur était composé de plusieurs circuits intégrés, placés sur la même carte mère et connectés ensemble par des fils métalliques. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés : l'Intel 3001 est le séquenceur, l'Intel 3002 est le chemin de données (ALU et registres), le 3003 est un circuit d'anticipation de retenue censé être combiné avec l'ALU, le 3212 est une mémoire tampon, le 3214 est une unité de gestion des interruptions, les 3216/3226 sont des interfaces de bus mémoire. On pourrait aussi citer la famille de circuits intégrés AMD Am2900.

Les ALUs en pièces détachées de l'épique étaient assez simples et géraient 2, 4, 8 bits, rarement 16 bits. Et il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes, par exemple combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Il s'agit de la méthode du bit slicing que nous avions abordée dans le chapitre sur les unités de calcul.

L'intel 4004 : le premier microprocesseur modifier

Par la suite, les progrès de la miniaturisation ont permis de mettre un processeur entier dans un seul circuit intégré. C'est ainsi que sont nés les microprocesseurs, à savoir des processeurs qui tiennent tout entier sur une seule puce de silicium. Les tout premiers microprocesseurs étaient des processeurs à application militaire, comme le processeur du F-14 CADC ou celui de l'Air data computer.

Le tout premier microprocesseur commercialisé au grand public est le 4004 d'Intel, sorti en 1971. Il comprenait environ 2300 transistors, avait une fréquence de 740 MHz, et manipulait des entiers de 4 bits. De plus, le processeur manipulait des entiers en BCD, ce qui fait qu'il pouvait manipuler un chiffre BCD à la fois (un chiffre BCD est codé sur 4 bits). Il pouvait faire 46 opérations différentes. C'était au départ un processeur de commande, prévu pour être intégré dans la calculatrice Busicom calculator 141-P, mais il fut utilisé pour d'autres applications quelque temps plus tard. Son successeur, l'Intel 4040, garda ces caractéristiques et n'apportait que quelques améliorations mineures : plus de registres, plus d'opérations, etc.

Immédiatement après le 4004, les premiers microprocesseurs 8 bits furent commercialisés. Le 4004 fut suivi par le 8008 et quelques autres processeurs 8 bits extrêmement connus, comme le 8080 d'Intel, le 68000 de Motorola, le 6502 ou le Z80. Ces processeurs utilisaient là encore des boitiers similaires au 4004, mais avec plus de broches, vu qu'ils étaient passés de 4 à 8 bits. Par exemple, le 8008 utilisait 18 broches, le 8080 était une version améliorée du 8008 avec 40 broches. Le 8086 fut le premier processeur 16 bits.

L'évolution des processeurs dans le temps modifier

La miniaturisation a eu des conséquences notables sur la manière dont sont conçus les processeurs, les mémoires et tous les circuits électroniques en général. On pourrait croire que la miniaturisation a entrainé une augmentation de la complexité des processeurs avec le temps, mais les choses sont à nuancer. Certes, on peut faire beaucoup plus de choses avec un milliard de transistors qu'avec seulement 10000 transistors, ce qui fait que les puces modernes sont d'une certaine manière plus complexes. Mais les anciens processeurs avaient une complexité cachée liée justement au faible nombre de transistors.

Il est difficile de concevoir des circuits avec un faible nombre de transistors, ce qui fait que les fabricants de processeurs devaient utiliser des ruses de sioux pour économiser des transistors. Les circuits des processeurs étaient ainsi fortement optimisés pour économiser des portes logiques, à tous les niveaux. Les circuits les plus simples étaient optimisés à mort, on évitait de dupliquer des circuits, on partageait les circuits au maximum, etc. La conception interne de ces processeurs était simple au premier abord, mais avec quelques pointes de complexité dispersées dans toute la puce.

De nos jours, les processeurs n'ont plus à économiser du transistor et le résultat est à double tranchant. Certes, ils n'ont plus à utiliser des optimisations pour économiser du circuit, mais ils vont au contraire utiliser leurs transistors pour rendre le processeur plus rapide. Beaucoup des techniques que nous verrons dans ce cours, comme l’exécution dans le désordre, le renommage de registres, les mémoires caches, la présence de plusieurs circuits de calcul, et bien d'autres ; améliorent les performances du processeur en ajoutant des circuits en plus. De plus, on n'hésite plus à dupliquer des circuits qu'on aurait autrefois mis en un seul exemplaire partagé. Tout cela rend le processeur plus complexe à l'intérieur.

Une autre contrainte est la facilité de programmation. Les premiers processeurs devaient faciliter au plus la vie du programmeur. Il s'agissait d'une époque où on programmait en assembleur, c'est à dire en utilisant directement les instructions du processeur ! Les processeurs de l'époque utilisaient des jeu d'instruction CISC pour faciliter la vie du programmeur. Pourtant, ils avaient aussi des caractéristiques gênantes pour les programmeurs qui s'expliquent surtout par le faible nombre de transistors de l'époque : peu de registres, registres spécialisés, architectures à pile ou à accumulateur, etc. Ces processeurs étaient assez étranges pour les programmeurs : très simples sur certains points, difficiles pour d'autres.

Les processeurs modernes ont d'autres contraintes. Grâce à la grande quantité de transistors dont ils disposent, ils incorporent des caractéristiques qui les rendent plus simples à programmer et à comprendre (registres banalisés, architectures LOAD-STORE, beaucoup de registres, moins d'instructions complexes, autres). De plus, si on ne programme plus les processeurs à la main, les langages de haut niveau passe par des compilateurs qui eux, programment le processeur. Leur interface avec le logiciel a été simplifiée pour coller au mieux avec ce que savent faire les compilateurs. En conséquence, l’interface logicielle des processeurs modernes est paradoxalement plus minimaliste que pour les vieux processeurs.

Tout cela pour dire que la conception d'un processeur est une affaire de compromis, comme n'importe quelle tâche d'ingénierie. Il n'y a pas de solution parfaite, pas de solution miracle, juste différentes manières de faire qui collent plus ou moins avec la situation. Et les compromis changent avec l'époque et l'évolution de la technologie. Les technologies sont toutes interdépendantes, chaque évolution concernant les transistors influence la conception des puces électroniques, les technologies architecturales utilisées, ce qui influence l'interface avec le logiciel, ce qui influence ce qu'il est possible de faire en logiciel. Et inversement, les contraintes du logiciel influencent les niveaux les plus bas, et ainsi de suite. Cette morale nous suivra dans le reste du cours, où nous verrons qu'il est souvent possible de résoudre un problème de plusieurs manières différentes, toutes utiles, mais avec des avantages et inconvénients différents.


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 calcul modifier

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 de l'ALU est assez simple : on a des entrées pour les opérandes, et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l'entrée de sélection de l'instruction, spécifie l'instruction à effectuer. Il faut bien prévenir notre unité de calcul qu'on veut faire une addition et pas une multiplication. Sur cette entrée, on envoie un numéro qui précise l'instruction à effectuer. La correspondance entre ce numéro et l'instruction à exécuter dépend de l'unité de calcul. 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".

Unité de calcul usuelle.

La taille des opérandes de l'ALU modifier

L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile.

Mais il arrive rarement que l'ALU manipule des opérandes plus petits que la taille des registres. Un exemple serait une ALU de 8 bits alors que les registres font 16 bits, ou une ALU 4 bits avec des registres de 8 bits. Autant cette solution est faisable sans trop de soucis avec l'addition ou la soustraction, autant la multiplication et la division s'implémentent difficilement quand registres et ALU n'ont pas la même taille.

Par exemple, sur le Z80, les registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. En conséquence, les calculs devaient être faits en deux phases : une qui traite les 4 bits de poids faible, et une autre qui traite les 4 bits de poids fort. L'unité de contrôle gérait tout cela, avec l'aide de registres placés en entrée/sortie de l'ALU, et de multiplexeurs/demultiplexeur. L'ensemble du circuit de l'ALU donnait ceci :

ALU du Z80

Les ALU sérielles modifier

Un exemple extrême est celui des des processeurs sériels (sous-entendu bit-sériels), qui utilisent une ALU sérielle, qui fait leurs calculs bit par bit, un bit à la fois. N'allez pas croire que les processeurs sériels sont tous des processeurs de 1 bit. Certes, de tels processeurs ont existé, le plus connu d'entre eux étant le Motorola MC14500B. Mais beaucoup de processeurs 4, 8 et 16 bits étaient des processeurs sériels. Ils ont généralement un jeu d'instruction assez limité : des opérations logiques, l'addition, la soustraction, guère plus. Les ALU sérielles ne peuvent pas faire de multiplications ou de divisions.

Naturellement, effectuer les opérations bit par bit est plus lent comparé aux processeurs non-sériels. L'avantage de ces ALU est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes. Autant utiliser des registres longs est facile, autant une ALU non-sérielle avec des opérandes aussi grands aurait été impraticable à l'époque.

Un autre avantage est que ces ALu utilisent peu de circuits. Et c'est pour cette raison que beaucoup de processeurs parallèles utilisaient des ALU sérielles. Le but de ces processeurs était d'exécuter pleins de calculs en parallèles, d'exécuter plusieurs calculs simultanément. Et cela demande d'utiliser pleins d'unités de calcul distinctes : exécuter N opérations en parallèle demande N unités de calcul. Mais un grand nombre d'unités de calcul signifie que celles-ci doivent être très simples, les ALU sérielles étant tout indiquées avec cette contrainte.

Le bit-slicing modifier

Avant l'époque des premiers microprocesseurs 8 et 16 bits, le processeur n'était pas un circuit intégré unique, mais était formé de plusieurs puces électroniques soudées à la même carte. L'ALU était souvent une puce séparée, le séquenceur aussi, les registres étaient dans leur propre puce, etc. Les puces en question étaient des puces TTL assez simples, comparé à ce qu'on a aujourd'hui. Les ALU étaient vendues séparément, et elles manipulaient souvent des opérandes de 4/8 bits, les ALU 4 bit étant très fréquentes.

Si on voulait créer une ALU pour des opérandes plus grandes, il n'y avait pas le choix : il fallait construire l'ALU à partir de plusieurs ALU 4/8 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 moindres qu'avec des additionneurs modernes, à anticipation de retenue, mais ce n'était pas un problème pour l'époque.

Les unités de calcul spécialisées modifier

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, la multiplication et la division étant clairement dans ce cas. Les ALU des processeurs modernes sont souvent couplées à un circuit multiplieur séparé, avec éventuellement un circuit diviseur lui aussi séparé des autres. Tous les processeurs récents incorporent un multiplieur, dans ou en-dehors de l'ALU, mais le support de la division est déjà moins fréquent. Les divisions sont des opérations assez rares, leur donner un circuit dédié n'en vaut pas toujours la peine.

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 existe des unités de calcul spécialisées pour les calculs d'adresse. Elles gèrent moins d'opérations que les ALU normales, vu que peu d'opérations sont utiles pour les adresses. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciennes architectures.

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. Il était utilisé pour incrémenter des adresses, ce qui est une opération très fréquente. Elle est utilisée pour manipuler des tableaux, le pointeur de pile, voire le program counter. Mais beaucoup d'architectures augmentaient ses capacités en lui permettant d'incrémenter des données. Pourtant, ce circuit incrémentait 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.

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 registres du processeur modifier

Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. Et là, les choses deviennent bien plus complexes. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres. Un banc de registres (register file) est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire.

Banc de registres simplifié.

Un processeur contient presque toujours un banc de registre, couplé à des registres isolés. Il y a cependant quelques exceptions, comme certaines architectures à accumulateur sans registres généraux, qui se passent de banc de registres. Mais dans une architecture normale, on trouve un ou plusieurs bancs de registres, et un ou plusieurs registres isolés. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.

L'adressage du banc de registres modifier

Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d'identifiant de registre. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.

Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. 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 : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas 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.

Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.

Adressage du banc de registres généraux

Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile peuvent être placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le program counter peuvent se mettre dans le banc de registre ! Nous verrons le cas du program counter dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.

Adressage du banc de registre - cas général

Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.

Les registres généraux modifier

Pour rappel, les registres généraux sont des registres qui ne sont pas spécialisés et peuvent mémoriser n'importe quoi : des entiers, des flottants, des adresses, etc. Ils sont opposés aux registres spécialisés, où chaque registre est spécialisé dans un type de données.

Dans l'exemple pris dans cette section, le processeur n'a que des registres généraux, couplés à un program counter et un registre d'état. Le program counter et le registre d'état sont des registres isolés, les registres généraux sont rassemblés dans un banc de registre. Le banc de registre est appelé le banc de registres généraux, vu qu'il ne contient que ça et qu'ils sont tous dedans.

L'interface du banc de registres généraux modifier

Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).

Banc de registre multiports.

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 :

Register File d'une architecture à 2-adresses

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 :

Register File d'une architecture à 3-adresses

Les bancs de registres scindés modifier

Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. 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 architectures à registres spécialisés modifier

Passons maintenant aux architectures dont les registres sont spécialisés. L'utilisation de plusieurs bancs de registres est plus simple et intuitive. Mais ce n'est pas systèmatiquement le cas et il est possible de regrouper des registres de type différents dans un seul banc de registres. Les deux méthodes, que nous allons détailler ci-dessous, portent respectivement les noms de banc de registre dédié et de banc de registre unifié.

L'exemple type est celui où on a des registres qui ne peuvent contenir qu'un type bien défini de donnée, par exemple des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. 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.

Mais d'autres processeurs utilisent un seul banc de registres unifié, qui regroupe tous les registres de données, qu'ils soient entier ou flottants. 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 la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.

Désambiguïsation de registres sur un banc de registres unifié.

Un autre exemple est celui des vieux processeurs où les adresses sont séparées des nombres entiers, dans deux ensembles de registres distincts. Les adresses et les données n'ont pas la même taille, ce qui fait que la meilleure solution est d'utiliser deux bancs de registres, un pour les adresses, l'autre pour les entiers. Le processeur Z80 faisait cela, en partie parce qu'il gérait des adresses de 16 bits, mais des données de 8 bits.

Le registre d'état modifier

Le registre d'état fait souvent bande à part et n'est pas placé 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 connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/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.

Place du registre d'état dans le chemin de données

Il est techniquement possible de mettre le registre d'état dans le banc de registre, mais cela complexifie l’implémentation du processeur. La principale difficulté est que les instructions arithmétiques doivent faire deux écritures dans le banc de registre : une pour le registre de destination, et une autre pour le registre d'état. Les deux écritures simultanées demandent d'utiliser un banc de registre à deux ports d'écriture, ce qui est très gourmand en transistors. Si le second port n'a pas l'occasion de servir pour d'autres instructions, c'est du gâchis. 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. Dans les faits, je ne connais aucun processeur qui utilise cette technique.

Les registres à prédicats modifier

Les registres à prédicats sont, pour rappel, des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux. Ils sont généralement placés à part, dans un banc de registres séparé. Ils sont placés au même endroit que le registre d'état, et sont connectés de la même manière. Le banc de registres à prédicat a une entrée de 1 bit connectée à l'ALU, et une sortie de un bit connectée au séquenceur. L'unité de calcul écrit dans ce banc de registres à volonté, le séquenceur lit ces registres si besoin.

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. 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.

Banc de registre pour les registres à prédicats

Le pointeur de pile modifier

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 registres, ou de les mettre dans le banc de registre. Sur les architectures où adresses et entiers sont dans des bancs de registre différents, le pointeur de pile est placé avec les adresses. Il faut dire que ces registres contiennent une adresse, pas un entier, et qu'il vaut mieux éviter de mélanger les deux.

Certains processeurs placent le pointeur de pile dans le banc de registre. C'est le cas sur le Z-80 ou sur l'Intel 8085, par exemple, où le pointeur de pile est dans le même banc de registre que les registres entiers (qui contient aussi les adresses). 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. 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.

Intel 8085.

L'autre solution est d'utiliser un registre isolé pour le pointeur de pile. L'avantage est que le pointeur de pile est censé être adressé implicitement. Pour le dire autrement, si on a un banc de registre de 16 registres, on a bien 16 registres adressables, alors que la solution précédente donne 15 registres adressables et un pointeur de pile qui ne l'est pas. L'inconvénient est dans la mise à jour du pointeur de pile, qui demande de l'incrémenter ou de le décrémenter. Soit on trouve un moyen pour le relier à l'ALU, soit on lui dédie un incrémenteur/décrémenteur spécialisé. Les deux solutions ajoutent des circuits et complexifient le chemin de données et le séquenceur.

Sur les vielles architectures, la solution est d'utiliser un incrémenteur spécialisé partagé avec le program counter. Le pointeur de pile est alors regroupé avec le program counter, les deux sont incrémentés par le même incrémenter. Un exemple est celui de l'Intel 4004, qui place les pointeurs de pile et le program counter dans un banc de registres séparé du reste du processeur. LA raison de cet arrangement est une économie de circuit : pas besoin d'utiliser deux incrémenteurs, on n'en utilise qu'un seul.

Microarchitecture de l'Intel 4040, une version améliorée de l'Intel 4004.

Le registre accumulateur modifier

Pour rappel, les architectures à accumulateurs disposent d'un registre accumulateur, où on lit une opérande et enregistre le résultat. Les autres opérandes sont soit lues depuis la mémoire, soit lues depuis des registres nommés.

Le registre accumulateur, présent sur les architectures à accumulateur, n'est jamais mis dans le banc de registres, et est toujours un registre isolé, à part. Il est relié à l'unité de calcul comme indiqué dans le schéma ci-dessous. Il est souvent associé à d'autres registres temporaires qui servent à mémoriser les opérandes temporairement, le temps du calcul.

Accumulateur.

Si le processeur dispose de registres nommés, ils sont placés dans un banc de registres à part. Le banc de registre n'a qu'un seul port, qui sert à la fois pour la lecture et l'écriture. La raison est qu'on ne lit qu'une seule opérande depuis les registres, l'autre opérande étant lue depuis l'accumulateur. De plus, le résultat est enregistré dans l'accumulateur.

Les registres d'interruption et le fenêtrage de registres modifier

Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Par exemple, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Ou encore, chaque appel de fonction avait accès à son propre ensemble de registres architecturaux séparé du reste, grâce à la fonctionnalité du fenêtrage de registres. Reste à voir comment ces fonctionnalités sont implémentées dans le processeur.

Les bancs de registres dédiés/unifiés pour les interruptions modifier

Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.

Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.

Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.

Le processeur Z80 utilisait cette technique. Avec cependant une petite différence : il avait un accumulateur séparé du banc de registre. Les six registres B, C, D, E, H et L étaient dans le banc de registre, de même que leurs copies pour interruptions nommées B', C', D', E', H' et L'. Par contre, l'accumulateur A et le registre d'état étaient aussi dupliqués : un pour les interruptions, l'autre pour les programmes. Le choix entre les deux se faisait par une bascule séparée pour les registres A,F,A',F', appelée la bascule A. Bascule qui parait redondante avec celle pour le banc de registres, mais qui ne l'est pas quand on sait ce qui va suivre.

Le Z80 incorporait des instructions pour échanger le contenu des deux ensembles de registres, accumulateur inclus. L'instruction EXX échangeait le contenu des registres B, C, D, E, H et L et B', C', D', E', H' et L'. L'instruction EX échangeait les registres A,F avec les registres A',F'. Ces instructions permettaient d'utiliser les registres d'interruption pour les programmes et réciproquement. Cela permettait de doubler le nombre de registres pour les programmes, si les interruptions n'étaient pas utilisées. Mais cela demandait de séparer l'accumulateur du reste.

Les instructions d'échange de registres du Z80 ne faisaient que modifier les bascules I et A. Cela évitait de faire des copies d'un ensemble de registre à l'autre. L'instruction EX inverse la bascule A, l'instruction EXX inverse le contenu de la bascule I. Il est aussi possible d'échanger le contenu des registres DE et HL, ce qui est là encore fait par deux bascules : une par ensemble de registres.

Le fenêtrage de registres modifier

Fenêtre de registres.

Le fenêtrage de registres fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.

Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.

Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.

Fenêtrage de registres au niveau du banc de registres.

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.

Désambiguïsation des fenêtres de registres.

L'interface de communication avec la mémoire modifier

L'interface avec la mémoire est, comme son nom 'l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la load-store unit, et j'en oublie.

Unité de communication avec la mémoire, de type simple port.

Sur certains processeurs, elle gère les mémoires multiport.

Unité de communication avec la mémoire, de type multiport.

Les registres d'interfaçage mémoire modifier

L'interface mémoire se résume le plus souvent à des registres d’interfaçage mémoire, intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.

Registres d’interfaçage mémoire.

Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.

L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc.

Cet avantage simplifie l'implémentation du mode d'adressage indirect, où l'adresse à lire/écrire est lue depuis un registre, surtout si le banc de registre a peu de ports, voire un seul. La lecture demande de récupérer l'adresse de lecture dans le banc de registre, mais aussi d'écrire la donnée lue dedans. L'écriture demande de lire la donnée à écrire et son adresse dans le banc de registre. Dans les deux cas, cela demande deux accès au banc de registre, d'accéder à deux registres.

Les deux accès peuvent être faits en même temps si le banc de registre est multiport. Par exemple, lors d'une lecture, il faut connecter le bus de donnée sur le port d'écriture (on écrit la donnée lue dans le banc de registres), et connecter le port de lecture sur le bus d'adresse (pour envoyer l'adresse). L'écriture connecte quant à elle le second port de lecture sur le bus de données.

Mais si le banc de registre n'a qu'un seul port, on doit utiliser des registres d'interfaçage, pas le choix. La lecture envoie l'adresse et récupère la donnée à des instants séparés, grâce aux registres d'interfaçage. L'écriture récupère donnée et adresse dans deux cycles consécutifs, mais les mémorise dans les registres d'interfaçage.

L'unité de calcul d'adresse modifier

Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l'Address generation unit, ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.

Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.

Unité d'accès mémoire avec unité de calcul dédiée

Les registres d'adresse modifier

Il y a quelques chapitres, nous avons vu que certains processeurs ont des registres séparés pour les entiers et les adresses. Typiquement, le processeur incorpore un banc de registre séparé pour les adresses. D'anciens processeurs utilisaient des registres d'index, utilisés pour manipuler des tableaux, séparés des registres entiers. Les indices sont plus petits que les entiers normaux, ce qui fait qu'il vaut mieux utiliser un banc de registre séparé. Dans les deux cas, ces registres sont placés dans l'interface mémoire, juste avant l'unité de calcul d'adresse, seule à manipuler leur contenu.

Unité d'accès mémoire avec registres d'adresse ou d'indice

Sur certains processeurs, il arrive que le program counter soit placé dans le banc de registre pour les adresses et soit mis à jour par l'AGU. 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.

La gestion de l'alignement et du boutisme modifier

L'interface mémoire est aussi celle qui gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés, et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gére automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.

Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, et les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés. Une vulgaire porte OU fait l'affaire, que ce soit dans l'exemple ou dans le cas général. Cette porte génère un signal qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.

La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.

La memory management unit modifier

Dans le chapitre sur l'abstraction mémoire, nous avions parlé de la MMU. Pour rappel, l'abstraction mémoire fait que les adresses de l'espace d'adressage sont des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d'adresses logiques, alors que les adresses de la mémoire RAM sont appelées adresses physiques. Ce niveau d'indirection facilite l'implémentation de certaines fonctionnalités, comme la mémoire virtuelle, la relocation matérielle, la protection mémoire, et quelques autres.

La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé appelé la Memory Management Unit (MMU), qui est souvent intégré directement dans l'interface mémoire. La MMU est souvent associée à une ou plusieurs mémoires caches, qui visent à accélérer la traduction d'adresses logiques en adresses physiques. En effet, nous verrons plus bas que la traduction d'adresse demande d'accéder à des tableaux, gérés par le système d'exploitation, qui sont en mémoire RAM. Aussi, les processeurs modernes incorporent des mémoires caches appelées des Translation Lookaside Buffers, ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.

MMU.

La MMU est aujourd'hui intégrée directement dans le processeur. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.

  • Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
  • Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.

Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.

La MMU avec la segmentation modifier

L'implémentation de la MMU dépend fortement du processeur. Mais sur les processeurs qui font seulement de la segmentation simple, la MMU se résume à quelques registres et des additionneurs/soustracteurs. Pour rappel, la segmentation découpe la mémoire en segments qui contiennent chacun un programme en cours d'exécution (pour simplifier). Le programme suppose qu'il commence à l'adresse 0, mais son segment est placé en mémoire RAM à une adresse différente à chaque exécution. Il faut donc corriger les adresses logiques du programme pour les transformer en adresses physiques destinées à la RAM. Pour cela, le processeur contient un registre de relocation, qui contient l'adresse du début du segment en RAM. A chaque accès mémoire, le processeur ajoute cette adresse à l'adresse logique. La MMU d'un processeur avec segmentation contient donc un ou plusieurs registres de relocation, et un additionneur. Les registres de relocation disposent de leur propre banc de registre et ne sont pas mélangés avec les autres.

Relocation assistée par matériel

Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la Bus Interface Unit, et le reste du processeur est appelé l'Execution Unit. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions, dont nous parlerons dans le chapitre suivant, sur l'unité de chargement.

Sur le 8086, la MMU est fusionnée avec les circuits de gestion du program counter. Les registres de segment sont regroupés avec le program counter dans un même banc de registres. Au lieu d'utiliser un additionneur séparé pour le program counter et un autre pour le calcul de l'adresse physique, un seul additionneur est utilisé pour les deux. L'idée était de partager l'additionneur, qui servait à la fois à incrémenter le program counter et pour gérer la segmentation. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.

Architecture du 8086, du 80186 et de ses variantes.

La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.

Intel i80286 arch

Le bus interne au processeur modifier

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 interne modifier

Chemin de données à un seul bus, principe général.

Dans le cas le plus simple, le processeur utilise un seul bus interne. Les architectures à accumulateur les plus simples étaient de ce type. Sur ces processeurs, l’exécution d'une instruction dyadique (deux opérandes) prend plusieurs étapes, car on ne peut transmettre deux opérandes en même temps sur un seul bus. Pour résoudre ce problème, on doit utiliser un registre 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. Sur les architectures à accumulateur, ce registre temporaire n'est autre que l'accumulateur lui-même.

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.

De tels processeurs sont souvent des architectures à accumulateur simples, les plus simples n'ayant pas de banc de registre. Une telle architecture est illustrée ci-dessous. Elles se résument alors à une ALU, l'accumulateur, des registres d'interfaçage avec la mémoire, et l'unité de contrôle qu'on détaillera dans le chapitre suivant. Les architectures à accumulateur plus évoluées disposent de registres architecturaux nommés, et donc d'un banc de registres. Le banc de registre n'à qu'un seul port, vu que la présence d'un seul bus fait de toute façon qu'on ne peut connecter qu'un seul port dessus.

Architecture à accumulateur sans banc de registres.

Les micro-architectures à plusieurs bus modifier

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 :

Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)

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.

Chemin de données d'une architecture LOAD-STORE

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 :

Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport

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 adresses modifier

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.

Bus avec adressage indirect

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.

Chemin de données à trois bus.

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.

Bus avec adressage base+index

L'adressage immédiat modifier

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é.

Chemin de données - Adressage immédiat avec extension de signe.

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.

Implémentation de l'adressage immédiat dans le chemin de données

Les simplifications possibles des chemins de données précédents modifier

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.

Chemin de données avec une ALU capable de faire des NOP

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.


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 : l'unité de chargement qui charge l'instruction depuis la mémoire, et le séquenceur qui commande le chemin de données. 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.

L'étape de chargement (ou fetch) est toujours décomposée en trois étapes :

  • la mise à jour du program counter (parfois faite en parallèle de la seconde étape).
  • l'envoi du contenu du program counter sur le bus d'adresse ;
  • la lecture de l'instruction sur le bus de données.

Le chargement d'une instruction demande d'effectuer trois étapes assez simples. Mais malgré leur simplicité, il y a beaucoup à dire dessus. La mise à jour du program counter est un peu à part vu qu'il s'agit d'un sujet assez velu, mais son envoi sur le bus d'adresse et la lecture de l'instruction demandent eux aussi d'ajouter des circuits au processeur. Voyons-les en détail.

Le program counter et son incrémentation modifier

À chaque chargement, le program counter est mis à jour afin de pointer sur la prochaine instruction à charger. 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. Dans ce qui va suivre, nous allons supposer que les instructions sont de taille fixe, ce qui fait que le program counter est toujours incrémenté de la même valeur.

Les circuits de mise à jour du program counter modifier

Pour résumer, il existe trois méthodes principales pour incrémenter le program counter, 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.

les différentes méthodes de calcul du program counter.

Le program counter mis à jour par l'ALU modifier

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. Ce qui explique que cette méthode était utilisée sur les processeurs assez anciens, à une époque où la technologie des semi-conducteurs ne permettait pas de mettre beaucoup de transistors sur une puce électronique. L'économie de circuit était primordiale et un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Autant dire que les processeurs de l'époque gagnaient à utiliser cette technique.

Le désavantage principal est 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.

Si on met à jour le program counter avec l'ALU, alors il est intéressant de le placer dans le banc de registres. 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. Si le processeur dispose de plusieurs bancs de registres, le program counter est généralement placé dans le banc de registre dédié aux adresses, s'il existe. Sinon, il est placé avec les nombres entiers/adresses.

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.

Le compteur programme modifier

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. Dans ce qui suit, nous allons appeler cette mise en oeuvre sous le nom de compteur ordinal.

Sur les processeurs très anciens, le compteur ordinal était un simple circuit incrémenteur, car ces processeurs arrivaient à caler leurs instructions sur un seul mot mémoire. Sur les processeurs modernes, c'est aussi le cas, sauf que le compteur est incrémenté par pas de 4 si les instructions sont codées sur 4 octets, 8 si les instructions sont codées sur 8 octets, etc. Notons qu'on peut simplifier le compteur en utilisant un simple incrémenteur auquel on aurait retiré quelques bits de poids faible. Par exemple, si les instructions sont codées sur 4 octets, on coupe les 2 derniers bits du compteur. Si les instructions sont codées sur 8 octets, on coupe les 3 derniers bits du compteur. Et ainsi de suite.

L'usage d'un compteur 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

Étape de chargement.

Le partage de l'incrémenteur lié au program counter modifier

Le program counter a donc son propre circuit d'incrémentation, sauf sur des architectures simples où c'est l'ALU qui est utilisée. Et certaines architectures ont tenté d'utiliser cet incrémenteur pour faire d'autres choses. L'idée est de le partager pour effectuer d'autres calculs d'adresse. En effet, les calculs d'adresse sont souvent simples et demandent d'incrémenter ou de décrémenter l'adresse en question.

Il est par exemple possible de partager l'incrémenteur/décrémenteur avec le pointeur de pile. C'est là une utilisation intelligente de l'incrémenteur, qui permet d'éliminer une redondance : ils sont tous deux des registres contenant une adresse, ils sont régulièrement connectés au bus d'adresse, ils sont incrémentés ou décrémentés à chaque fois qu'on les modifie. Il s'agit là d'une simplification assez compliquée à mettre en œuvre, mais qui a son intérêt. Cette technique marche bien si les cadres de pile ont la même taille, s'ils sont de taille fixe : on incrémente/décrémente le pointeur de pile d'une taille fixe, toujours identique. Mais si les cadres de pile sont de taille variable, alors cette solution ne marche pas.

Il est possible de partager l'incrémenteur avec le pointeur de pile, les registres de segmentation, voire avec d'autres registres. Nous allons prendre l'exemple du pointeur de pile, mais ce qui va suivre vaut pour tout autre registre d'adresse. Pour résumer, on a trois cas principaux :

  • Le program counter partage l'incrémenteur/décrémenteur avec un ou plusieurs registres d'adresse.
  • Le program counter est intégré à l'interface mémoire et est mis à jour par l'unité de calcul d'adresse.
  • Le program counter a son propre incrémenteur qui n'est pas partagé.

Les deux premiers cas ont l'avantage de partager l'incrémenteur ou l'ALU entière, alors que le troisième ne permet aucune économie de circuits.

Calcul du program counter avec des registres d'adresse sécialisés

Cas 1 : l'Intel 8086 modifier

Dans le chapitre précédent, nous avions vu le cas de l'Intel 8086, qui partageait un additionneur entre la MMU et l'unité de chargement. Un même additionneur servait à la fois à incrémenter le program counter, et à effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.

Intel 8086, microarchitecture.

Cas 2 : le processeur Z80 modifier

Le Z80 est un processeur dans ce style, à quelques détails près. Il dispose d'un circuit incrémenteur séparé de l'ALU, qui est utilisé pour mettre à jour le program counter et le pointeur de pile. Mais il est aussi utilisé pour le rafraichissement mémoire, qui demande de balayer la mémoire d'adresse en adresse, en incrémentant un compteur de rafraichissement.

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, l'incrémenteur du program counter a été réutilisé de beaucoup de manières originales.

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.

Architecture du Z80, telle que décrite par le jeu d’instruction. Elle ne correspond pas à l'organisation interne réelle du processeur. Notamment, l'incrémenteur (noté +1 sur le schéma) est représenté en double, alors que l'étude du die du processeur montre qu'il n'y en a qu'un seul. De même, l'organisation des registres est différente de celle montrée sur ce schéma.

Quand est mis à jour le program counter ? modifier

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.

Commande de la mise à jour du program counter.

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.

Il existe quelques processeurs pour lesquels le temps de calcul dépend des opérandes de l'instruction. On peut par exemple avoir une division qui prend 10 cycles avec certaines opérandes, mais 40 cycles avec d'autres opérandes. Mais ces processeurs sont rares et cela est surtout valable pour les opérations de multiplication/division, guère plus. Le problème est alors le même qu'avec les accès mémoire et la solution est la même : l'ALU prévient le séquenceur quand le résultat est disponible.

L'initialisation du program counter : le CPU reset vector modifier

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.

  • Avec la première solution, la plus simple, le processeur démarre l’exécution du code à une adresse bien précise, toujours la même, câblée dans ses circuits. On peut par exemple faire démarrer le processeur à l'adresse 0 : le processeur lira le contenu de la toute première adresse et démarrera l’exécution du programme à cet endroit.
  • Avec la seconde solution, on ajoute un niveau de redirection. Le processeur est toujours conçu de manière à accéder à une adresse bien précise au démarrage. Mais l'adresse en question ne contient pas la première instruction à exécuter. A la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la charge dans son program counter. En clair, il effectue un branchement.

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.

Les branchements et le program counter modifier

L'implémentation des branchements implique à la fois l'unité de chargement et 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, le séquenceur détecte si l'instruction exécutée est un branchement ou non, et il en déduit où trouver l'adresse de destination. L'altération du program counter est par contre du fait de l'unité de chargement.

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.

En soi, un branchement consiste juste à écrire l'adresse de destination dans le program counter. L'adresse en question se trouve à un endroit qui dépend du mode d'adressage du branchement. Pour rappel, on distingue quatre modes d'adressage pour les branchements : direct, indirect, relatif et implicite. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le 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, décalage fournit par le séquenceur. 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 modifier

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.

  • Les branchements indirects consistent à copier un registre dans le program counter, ce qui revient simplement à faire une opération MOV entre deux registres, la seule différence étant que le program coutner n'est pas adressable.
  • Les branchements directs et relatifs sont traités comme des opérations en mode d'adressage immédiat.
    • Pour les branchements directs, on utilise l'adressage direct. Concrètement, l'adresse de destination du branchement est directement écrite dans le banc de registre. l'opération est alors équivalent à une opération MOV avec une constante immédiate.
    • Les branchements relatifs demandent de lire le program counter depuis le banc de registre, d'ajouter une constante immédiate dans l'ALU, et d'écrire le tout dans le banc de registre. Au final, l’opération est juste une opération arithmétique avec un opérande lu depuis les registres et une autre en adressage immédiat.
Mise à jour du program counter avec branchements, si PC dans le banc de registres

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 modifier

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.

Mise à jour du Program Counter par l'ALU en cas de branchements

L’implémentation des branchements avec un program counter séparé modifier

É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 :

Fonctionnement d'un compteur (décompteur), schématique

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é.

Unité de détection des branchements dans le décodeur

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.

Unité de chargement qui gère les branchements relatifs.

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.

Unité de chargement qui gère les branchements directs et indirects.

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'envoi du program counter sur le bus d'adresse modifier

Les program counter doit être envoyé sur le bus d'adresse, et cela demande quelques adaptations. Et sur ce point, la situation est différente selon que l'on parler d'une architecture Harvard ou Von Neumann.

Sur les architectures Harvard, où on a un espace d'adressage séparé pour les instructions, l'implémentation est très simple. Le program counter est directement relié au bus mémoire dédié aux instructions, il est le seul à y être relié, il n'y a rien de spécial à faire. Le séquenceur et le chemin de donnée utilisent des interfaces mémoire séparées.

Microarchitecture de l'interface mémoire d'une architecture Harvard

Mais sur les architectures Von-Neumann et affiliées, ou tout du moins les architectures non-Havard, les choses sont différentes. Le séquenceur et le chemin de donnée partagent la même interface mémoire. L'interface avec la mémoire s'occupe alors de toutes les adresses, qu'elles viennent du chemin de données ou du séquenceur, et ca comprend aussi le program counter.

Microarchitecture de l'interface mémoire d'une architecture von neumann

La connexion du program counter sur le bus d'adresse, architectures Von Neumann modifier

Le même bus sert donc soit à envoyer des adresses provenant du chemin de données (calculées/précisées via un mode d'adressage), soit envoyer le program counter. La gestion de ce choix se fait différemment selon que le program counter est isolé, isolé mais connecté au bus interne, ou placé dans le banc de registres.

Si le program counter est intégré au banc de registres, il suffit de connecter la sortie du banc de registre au bus d'adresse, ce qui est équivalent à de l'adressage indirect. L'implémentation est la plus simple possible. L'unité de chargement ne fait que commander le banc de registre et l'interface mémoire.

Connexion du program counter sur les bus avec PC dans le banc de registres

Si le program counter est isolé, mais connecté au bus interne au processeur, il suffit de connecter le program counter au bus d'adresse et déconnecter le banc de registre.

Connexion du program counter sur les bus avec PC isolé

Une solution presque équivalente utilise un MUX placé avant les registres d'interfaçage, qui permet de faire le choix entre sortie de l'AGU/bus interne au CPU et Program Counter.

Unité d'accès mémoire avec program counter
Notez que l'on comprend mieux l'intérêt des registres d'interfaçage. Ce registre contient le program counter, mais celui-ci peut être mis à jour en parallèle. Si on incrémente le program counter pendant l'accès mémoire, le registre d'interfaçage maintient l'adresse adéquate pendant tout l'accès.

Le lien avec les autres registres d'adresse, architecture Von Neumann modifier

Les trois cas précédents montrent ce qu'il se passe suivant la localisation du program counter. Mais il faut aussi parler du cas où le processeur dispose d'une unité de calcul d'adresse séparée, généralement liée à un banc de registres spécialisés pour les adresses. L'interface mémoire peut alors intégrer ou non le program counter. Les trois cas précédents sont alors quelque peu adaptés.

Dans le cas où le program counter est incrémenté par son propre compteur/circuit, rien ne change.

Dans le cas où le program counter est intégré dans un banc de registre spécialisé pour les adresses, il est généralement incrémenté par l'unité de calcul associée. Dans ce cas, il n'y a rien à faire : la sortie de l'unité de calcul et/ou du banc de registre est reliée au bus d'adresse, il suffit d'adresser le program counter, rien de plus. Un exemple de cette organisation est celui du 8086, un des tout premier processeur d'Intel.

Calcul d'adresse par ALU dédiée avec PC intégré au banc de registre d'adresse

Le troisième cas, avec un program counter isolé et incrémenté par l'unité de calcul d'adresse est aussi adapté. Dans ce cas, le program counter est envoyé sur une entrée de l'unité de calcul, à travers un multiplexeur. La sortie de l'unité de calcul est évidemment connectée au program counter, pour l'incrémenter.

Calcul d'adresse par ALU dédiée avec PC séparé

Les trois techniques précédentes peuvent s'adapter dans le cas où le program counter est regroupé avec le pointeur de pile.

La lecture de l'instruction modifier

Passons maintenant à la dernière étape : la lecture de l'instruction proprement dit. L'instruction est alors disponible sur le bus de données, et le processeur doit en faire bon usage.

Le registre d'instruction modifier

Sur certains processeurs, l'instruction chargée est stockée dans un registre d'instruction situé juste avant le séquenceur. 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. Et cela demande de mémoriser l'instruction dans un registre pour libérer le bus, d'où l'existence du registre d'instruction.

Registre d'instruction.

Le chargement des instructions de longueur variable modifier

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.

Instructions non alignées.

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.

Décaleur d’instruction.


Pour rappel, le chemin de données est rempli de composants à configurer d'une certaine manière pour exécuter une instruction. Pour configurer le chemin de données, il faut envoyer les signaux de commande adéquats sur l'entrée de sélection de l'unité de calcul, les entrées du banc de registres, ou les circuits du bus du processeur. Lorsque les circuits du chemin de données reçoivent ces signaux de commande, ils sont conçus pour effectuer une action précise et déterminée.

Mais l'instruction chargée depuis la mémoire ne précise pas les signaux de commande, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une série de signaux de commandes adéquats. C'est le rôle de l'unité de décodage d'instruction, une portion du séquenceur qui « décode » l'instruction. Le séquenceur traduit une instruction en suite de micro-opérations et émet les signaux de commande pour chaque micro-opération.

Il existe des processeurs assez rares où chaque instruction machine précise directement les signaux de commande, sans avoir besoin d'une unité de décodage d'instruction. L'encodage de l'instruction en mémoire est alors très simple : il suffit de placer les signaux de commande les uns à la suite des autres, rien de plus. De telles architectures sont appelées des architectures actionnées par déplacement. Elles feront l'objet d'un chapitre dédié. En attendant, nous allons mettre ces architectures de côté pour le moment et nous concentrer sur des architectures plus courantes.
Unité de décodage d'instruction

La traduction en question n'est pas simple, pour une raison assez importante : les instructions sont décomposées en plusieurs étapes, appelées micro-instructions, chacune configurant le chemin de donnée d'une manière bien précise. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Dans le cas le plus simple, chaque instruction correspond à une micro-opération et la traduction est alors triviale : le circuit est un simple circuit combinatoire. Mais dès que ce n'est pas le cas, le séquenceur devient un circuit séquentiel avec toute la complexité que cela implique.

Les séquenceurs câblés et microcodés modifier

Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé.

Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes.

  • La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques.
  • La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit.
  • La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (Programmable Logic Array).

Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir.

Les séquenceurs câblés modifier

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é. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides. Par contre, les séquenceurs câblés sont souvent utilisés sur les processeurs RISC, qui ont peu d'instructions, pour lesquels la complexité du séquenceur et le nombre de portes est assez faible et supportable. La complexité du séquenceur dépend de la complexité des instructions machine.

L'implémentation du séquenceur sans compteur modifier

Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire.

Séquenceur combinatoire

C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente, ce qui fait qu'un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, la mémoire dans laquelle sont stockées les instructions doit être physiquement séparée de la mémoire dans laquelle on stocke les données, afin de pouvoir charger l'instruction tout en accédant aux données, le tout en un seul cycle d'horloge processeur.

L'implémentation du séquenceur avec un compteur modifier

Sur la plupart des processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur est modifié, notamment par l'intégration d'un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, ce qui fait qu'il mémorise cette information dans un registre. En conséquence, les séquenceurs câblés sont des circuits séquentiels.

Séquenceur séquentiel

Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque cycle d'horloge, ou du moins à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM, par exemple). Il est réinitialisé quand l'instruction se termine, généralement quand le compteur a atteint le nombre de cycles adéquat pour éxecuter l'instruction.

Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. A la place, il s'agit souvent d'un compteur basé un registre à décalage, appelé un compteur one-hot, ou encore un compteur en anneau. Les raisons sont que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur doit coder peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit, à condition qu'on accepte d'ajouter quelques bascules.

En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible. Ce qui fait qu'on l'appelle le décodeur d'instruction. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous.

Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (one hot) est en haut, les signaux de commandes sont émis vers le bas.

Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur one hot facilite l'implémentation de la matrice de portes logiques.

La détermination de la fin d'une instruction modifier

Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le program counter pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur.

Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation.

L'implémentation des instructions de durée variable modifier

Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes.

Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur.

Les séquenceurs microcodés modifier

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.

Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous aprlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide.

Les séquenceurs micro-codés sont plus simples à concevoir et cela simplifie beaucoup le travail des concepteurs de processeurs. L'usage du microcode a un autre avantage majeur : il permet d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie de sa simplicité de conception, un séquenceur microcodé a des inconvénients qui ne sont pas négligeables. Par exemple, ils utilisent plus de portes logiques que les séquenceurs câblés, vu qu'une ROM est un circuit gourmand en portes logique. De plus, un séquenceur micro-codé est plus lent qu'un séquenceur câblé, la raison principale étant qu'une mémoire ROM est bien plus lente qu'un circuit combinatoire fabriqué directement avec des portes logiques.

Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et complexe, avec beaucoup de modes d'adressages différents. Implémenter un grand nombre d'instruction avec un séquenceur câblé aurait été beaucoup trop compliqué, ce qui fait que les concepteurs de processeur préféraient utiliser un microcode. Entre une instruction émulée par une suite d'instructions machines, et la même instruction microcodée, les performances étaient généralement similaires. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, le microcode pouvant être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Pour résumer, les instructions microcodées n'étaient pas forcément plus rapides que leur équivalent logiciel, mais elles existent et cela suffisait sur les architectures CISC qui privilégiaient la taille du programme - la code size. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé.

Le control store modifier

La caractéristique principale du control store est sa capacité, qui est souvent assez petite. La capacité du control store dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du control store correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations.

  • Avec le microcode horizontal, chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits !
  • Avec un microcode vertical, les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le control store. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un control store plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur.

L'implémentation interne du control store ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le control store contenait 512 bytes/microinstructions de 21 bits chacune. Le control store n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes.

Le control store a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le control store contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un control store de 128 bytes, ce qui laisse 8 bytes vides. La position des vides dans le control store dépend de la solution utilisé. On pourrait croire que les vides sont généralement placés à la fin du control store, mais il est parfois préférable de disperser les vides dans le control store, afin de simplifier les circuits adossés au microcode, que nous allons voir dans ce qui suit.

Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le control store. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles.

La première optimisation de ce genre consiste à partager des bouts de microcode entre instructions machines, sur le même principe que les fonctions/sous-programmes/routines logicielles. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage.

La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du control store, dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. Une autre solution consiste à décoder certaines instructions simples sans passer par le microcode, ce qui donne les séquenceur hybrides dont nous parlerons dans la suite du chapitre.

Les circuits d’exécution du microcode modifier

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.

Microcode sans microséquenceur.

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.

Microcode avec un microséquenceur.

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.

Branchements avec microcode horizontal avec microséquenceur.

Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante :

Branchements avec microcode horizontal sans microséquenceur.

Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour.

Localiser la première microinstruction à exécuter dans le control store modifier

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.

Control store adressé par predecodage de l'opcode

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.

Control store d'un microcode horizontal.

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.

Control store adressé par l'opcode - opcode sur bits de poids faible

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.

Control store adressé par l'opcode - opcode sur bits de poids fort

Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du control store. Les deux premières méthodes remplissent le control store au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le control store. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du control store, car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc.

La mise à jour du microcode modifier

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 modifier

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 et instructions à prédicats modifier

L'implémentation des branchements implique tout le séquenceur et l'unité de chargement. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le program counter quand un branchement est détecté. L'altération du program counter est le fait de l'unité de chargement. Elle a juste besoin qu'on lui précise à quelle adresse brancher, et quand un branchement a lieu. Quant au séquenceur, il doit gérer tout le reste.

L'implémentation des branchements conditionnels modifier

Les branchements inconditionnels sont les plus simples à gérer. Il suffit de détecter si une instruction est un branchement inconditionnel, et de déterminer où se trouve l'adresse de destination. Pour cela, on doit ajouter un circuit de détection des branchements, qui détecte si l'instruction exécutée est un branchement ou non. Il est situé dans le décodeur d'instruction. La détermination de l'adresse dépend du mode d'adressage et implique de configurer correctement le chemin de données. Il y a peu çà dire

Par contre, les branchements conditionnels demandent en plus de vérifier qu'une condition est respectée, ils demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels :

  • Ceux qui doivent être précédés d'une instruction de test ou de comparaison.
  • Ceux qui effectuent le test et le branchement en une seule instruction machine.
  • Ceux où les branchements conditionnels sont émulés par une skip instruction, une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel.
Implémentations possibles des 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 modifier

Le calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données.

Premièrement, deux opérandes sont lus depuis les registres, puis sont envoyés à un circuit soustracteur qui soustrait les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les bits intermédiaires, et ils sont combinés pour calculer les différentes conditions.

En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égaux, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs.

Calcul d'une condition pour un branchement

Outre le calcul de la condition, un branchement conditionnel saute ou non à une certaine adresse. On sait déjà que le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du program counter et en mettant à 1 son entrée de réinitialisation. La seule difficulté est de décider s'il faut mettre à jour le program counter ou non.

Le program counter doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : elle est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant.

Implémentation des branchements conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici.

Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple.

Implémentation des branchements.

Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit. Et c'est ce que nous allons voir dans la section suivante.

Le registre d'état ou les registres à prédicats et les circuits associés modifier

Voyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières.

  • La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat.
  • La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement.

Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change.

Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul.

Implémentation des branchements avec un registre d'état

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.

Implémentation de l'unité de chargement avec plusieurs registres à prédicats

L'implémentation des skip instructions modifier

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.

Implémentation des branchements pseudo-conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici, de même que le multiplexeur pour choisir la bonne condition.

L'implémentation des instructions à prédicats modifier

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.

Implémentation des instructions à prédicats

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.

Implémentation des instructions à prédicats simples

La Prefetch input queue modifier

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 modifier

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 modifier

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 modifier

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.


Les jeux d’instructions spécialisés modifier

Les DSP, les processeurs de traitement du signal, sont des jeux d'instructions spécialement conçus pour travailler sur du son, de la vidéo, des images… Le jeu d'instruction d'un DSP est assez spécial, que ce soit pour le nombre de registres, leur utilisation, ou la présence d'instructions insolites.

Les registres des DSP modifier

Pour des raisons de couts, tous les DSP utilisent un faible nombre de registres spécialisés. Un DSP a souvent des registres entiers séparés des registres flottants, ainsi que des registres spécialisés pour les adresses mémoires. On peut aussi trouver des registres spécialisés pour les indices de tableau ou les compteurs de boucle. Cette spécialisation des registres pose de nombreux problèmes pour les compilateurs, qui peuvent donner lieu à une génération de code sous-optimale.

De nombreuses applications de traitement du signal ayant besoin d'une grande précision, les DSP sont dotés de registres accumulateurs très grands, capables de retenir des résultats de calcul intermédiaires sans perte de précision.

De plus, certaines instructions et certains modes d'adressage ne sont utilisables que sur certains types de registres. Certaines instructions d'accès mémoire peuvent prendre comme destination ou comme opérande un nombre limité de registres, les autres leur étant interdits. Cela permet de diminuer le nombre de bits nécessaire pour encoder l'instruction en binaire.

Les instructions courantes des DSP modifier

Les DSP utilisent souvent l'arithmétique saturée. Certains permettent d'activer et de désactiver l'arithmétique saturée, en modifiant un registre de configuration du processeur. D'autres fournissent chaque instruction de calcul en double : une en arithmétique modulaire, l'autre en arithmétique saturée. Les DSP fournissent l'instruction multiply and accumulate (MAC) ou fused multiply and accumulate (FMAC), qui effectuent une multiplication et une addition en un seul cycle d'horloge, ce calcul étant très courant dans les algorithmes de traitement de signal. Il n'est pas rare que l'instruction MAC soit pipelinée : il suffit d'utiliser un multiplieur, un additionneur et d'insérer un registre entre l'additionneur et le multiplieur.

Pour plus d’efficacité dans certaines applications numériques, les DSP peuvent gérer des flottants spéciaux : les nombres flottants par blocs. Ils servent quand plusieurs nombres flottants ont le même exposant. Ils permettent alors de mutualiser l'exposant. Par exemple, si je dois encoder huit nombres flottants dans ce format, je devrai utiliser un mot mémoire pour l'exposant, et huit autres pour les mantisses et les signes.

Pour accélérer les boucles for, les DSP ont des instructions qui effectuent un test, un branchement et une mise à jour de l'indice en un cycle d’horloge. Cet indice est placé dans des registres uniquement dédiés aux compteurs de boucles. Autre fonctionnalité : les instructions autorépétées, des instructions qui se répètent automatiquement tant qu'une certaine condition n'est pas remplie. L'instruction effectue le test, le branchement, et l’exécution de l'instruction proprement dite en un cycle d'horloge. Cela permet de gérer des boucles dont le corps se limite à une seule instruction. Cette fonctionnalité a parfois été améliorée en permettant d'effectuer cette répétition sur des suites d'instructions.

Les DSP sont capables d'effectuer plusieurs accès mémoires simultanés par cycle, en parallèle. Par exemple, certains permettent de charger toutes leurs opérandes d'un calcul depuis la mémoire en même temps, et éventuellement d'écrire le résultat en mémoire lors du même cycle. Il existe aussi des instructions d'accès mémoires, séparées des instructions arithmétiques et logiques, capable de faire plusieurs accès mémoire par cycles : ce sont des déplacements parallèles (parallel moves). Notons qu'il faut que la mémoire soit multiport pour gérer plusieurs accès par cycle. Un DSP ne possède généralement pas de cache pour les données, mais conserve parfois un cache d'instructions pour accélérer l’exécution des boucles. Au passage, les DSP sont basés sur une architecture Harvard, ce qui permet au processeur de charger une instruction en même temps que ses opérandes.

Architecture mémoire des DSP.

Les modes d’adressage sur les DSP modifier

Les DSP incorporent pas mal de modes d'adressages spécialisés. Par exemple, beaucoup implémentent l'adressage indirect à registre avec post- ou préincrément/décrément, que nous avions vu dans le chapitre sur l'encodage des instructions. Mais il en existe d'autres qu'on ne retrouve que sur les DSP et pas ailleurs. Il s'agit de l'adressage modulo et de l'adressage à bits inversés.

L'adressage « modulo » modifier

Les DSP implémentent des modes d'adressages servant à faciliter l’utilisation de files, des zones de mémoire où l’on stocke des données dans un certain ordre. On peut y ajouter de nouvelles données, et en retirer, mais les retraits et ajouts ne peuvent pas se faire n'importe comment : quand on retire une donnée, c'est la donnée la plus ancienne qui quitte la file. Tout se passe comme si ces données étaient rangées dans l'ordre en mémoire.

Ces files sont implémentées avec un tableau, auquel on ajoute deux adresses mémoires : une pour le début de la file et l'autre pour la fin. Le début de la file correspond à l'endroit où l'on insère les nouvelles données. La fin de la file correspond à la donnée la plus ancienne en mémoire. À chaque ajout de donnée, on doit mettre à jour l'adresse de début de file. Lors d'une suppression, c'est l'adresse de fin de file qui doit être mise à jour. Ce tableau a une taille fixe. Si jamais celui-ci se remplit jusqu'à la dernière case, (ici la cinquième), il se peut malgré tout qu'il reste de la place au début du tableau : des retraits de données ont libéré de la place. L'insertion continue alors au tout début du tableau. Cela demande de vérifier si l'on a atteint la fin du tableau à chaque insertion. De plus, en cas de débordement, si l'on arrive à la fin du tableau, l'adresse de la donnée la plus récemment ajoutée doit être remise à la bonne valeur : celle pointant sur le début du tableau. Tout cela fait pas mal de travail.

Le mode d'adressage « modulo » a été inventé pour faciliter la gestion des débordements. Avec ce mode d'adressage, l'incrémentation de l'adresse au retrait ou à l'ajout est donc effectué automatiquement. De plus, ce mode d'adressage vérifie automatiquement que l'adresse ne déborde pas du tableau. Et enfin, si cette adresse déborde, elle est mise à jour pour pointer au début du tableau. Suivant le DSP, ce mode d'adressage est géré plus ou moins différemment. La première méthode utilise des registres « modulo », qui stockent la taille du tableau. Chaque registre est associé à un registre d'adresse pour l'adresse/indice de l’élément en cours. Vu que seule la taille du tableau est mémorisée, le processeur ne sait pas quelle est l'adresse de début du tableau, et doit donc ruser. Cette adresse est souvent alignée sur un multiple de 64, 128, ou 256. Cela permet ainsi de déduire l'adresse de début de la file : c'est le multiple de 64, 128, 256 strictement inférieur le plus proche de l'adresse manipulée. Autre solution : utiliser deux registres, un pour stocker l'adresse de début du tableau et un autre pour sa longueur. Et enfin, dernière solution, utiliser un registre pour stocker l'adresse de début, et un autre pour l'adresse de fin.

L'adressage à bits inversés modifier

L'adressage à bits inversés (bit-reverse) a été inventé pour accélérer les algorithmes de calcul de transformée de Fourier (un « calcul » très courant en traitement du signal). Cet algorithme va prendre des données dans un tableau, et va fournir des résultats dans un autre tableau. Seul problème, l'ordre d'arrivée des résultats dans le tableau d'arrivée est assez spécial. Par exemple, pour un tableau de 8 cases, les données arrivent dans cet ordre : 0, 4, 2, 6, 1, 5, 3, 7. L'ordre semble être totalement aléatoire. Mais il n'en est rien : regardons ces nombres une fois écrits en binaire, et comparons-les à l'ordre normal : 0, 1, 2, 3, 4, 5, 6, 7.

Ordre normal Ordre Fourier
000 000
001 100
010 010
011 110
100 001
101 101
110 011
111 111

Comme vous le voyez, les bits de l'adresse Fourier sont inversés comparés aux bits de l'adresse normale. Nos DSP disposent donc d'un mode d’adressage qui inverse tout ou partie des bits d'une adresse mémoire, afin de gérer plus facilement les algorithmes de calcul de transformées de Fourier. Une autre technique consiste à calculer nos adresses différemment. Il suffit, lorsqu'on ajoute un indice à notre adresse, de renverser la direction de propagation de la retenue lors de l’exécution de l'addition. Certains DSP disposent d'instructions pour faire ce genre de calculs.


Sur les architectures actionnées par déplacement (transport triggered architectures), les instructions machines correspondent directement à des micro-instructions. Chaque instruction du langage machine configure directement le bus interne au processeur. Elles peuvent relier les ALU aux registres, relier les registres entre eux, effectuer un branchement, etc. De tels processeurs n'ont pas besoin d'un décodeur d'instruction pour traduire les instructions machines en signaux de commandes. Mais elles ont quand même besoin d'une unité de chargement, d'un program counter et des circuits pour gérer les branchements.

Architecture déclenchée par déplacement (Transport Triggered Architecture).

Les avantages et désavantages d'un processeur actionné par déplacement modifier

La raison d'exister de ces architectures est tout autant la simplicité du processeur que la performance. Et évidemment, comme vous commencez à vous y habituer, cela ne se fait pas sans contreparties.

Les avantages : l'absence de décodeur d'instruction et des optimisations logicielles modifier

L'avantage le plus flagrant est l'absence de décodeur d'instruction et de microcode, qui rend de tels processeurs très simples à fabriquer. Cette simplicité fait que de tels processeurs utilisent peu de portes logiques, qui peuvent être utilisés pour ajouter plus de cache, de registres, d'unités de calcul, et autres.

L'autre avantage est que le séquencement des micro-instructions n'est pas réalisé par le processeur, mais par le compilateur. Le fait que le compilateur ait la main sur les micro-instructions peut permettre des simplifications assez fines, qui ne seraient pas possibles avec des instructions machines normales. Par exemple, on peut envoyer le résultat fourni par une unité de calcul directement en entrée d'une autre, sans avoir à écrire ce résultat dans un registre intermédiaire du banc de registres. Cette optimisation est très utilisée sur ces architectures, au point que celles-ci adaptent leur bancs de registres en conséquence. Elles peuvent retirer quelques ports de lecture et écriture sans que cela impacte les performances. Du moins, tant que le compilateur arrive efficacement à transférer les données entre unités de calcul sans passer par le banc de registre.

Les désavantages : une portabilité minable et une taille de code beaucoup plus élevée modifier

Le désavantage principal est que la portabilité des programmes compilés pour de telles architecture est faible. La raison principale est qu'il n'y a pas de séparation entre jeu d'instruction et microarchitecture. Si l'on change la microarchitecture du processeur, alors on change aussi son jeu d’instruction et la compatibilité part avec. Impossible de rajouter une unité de calcul, de changer les temps d’exécution des instructions ou quoique ce soit d'autre. Par exemple, les micro-instructions ont un temps de latence à prendre en compte. Sans cela, on pourrait par exemple lire une donnée avant que celle-ci ne soit disponible. Si les temps de latence changent, les programmes écrits en tenant compte des anciens temps de latences peuvent se mettre à dysfonctionner. De fait, de telles architectures ne sont pas utilisables dans les PC grands public,s mais elles peuvent être utilisées dans certains systèmes embarqués, dans des utilisations très spécifiques.

Un second désavantage non-négligeable est que la code size est généralement mauvaise sur ces processeurs. Et cela pour deux raisons : les instructions sont plus longues, et l'instruction path length (le nombre d'instructions du programme) est aussi plus élevé.

  • Premièrement, les instructions sont plus longues que pour les autres processeurs. La raison est que les micro-instructions d'un processeur normal sont plus longues que les instructions machines. Les micro-instructions ont en effet besoin de préciser comment configurer le bus interne, le banc de registres, l'unité de calcul, l'unité de gestion mémoire, etc. À l'inverse, une instruction normale a surtout besoin de préciser l'opération à faire, la localisation des opérandes/résultats, éventuellement le mode d'adressage. Les bits de configuration du bus interne sont ce qu'ils sont et on peut rarement en limiter le nombre pour compresser la micro-instruction, alors que réduire la taille des opcodes et des opérandes est assez facile.
  • Deuxièmement, le nombre d'instructions par programme augmente lui aussi. N'oublions pas qu'une instruction machine correspond à une séquence de plusieurs micro-instructions. Le nombre d'instructions est donc multiplié en conséquence. Et les optimisations qui permettent d'économiser les micro-instructions n'y font pas grand chose. S'il est possible d'éliminer certaines micro-instructions redondantes, dans certaines circonstances, cela ne compense pas le fait que le nombre total d'instructions machines est multiplié par 3 ou 4.

L'implémentation des processeurs actionnés par déplacement modifier

Sur certains de ces processeurs, on n’a besoin que d'une seule instruction MOV, qui permet de copier une donnée d'un emplacement (registre ou adresse mémoire) à un autre. Pas d'instructions LOAD, STORE, ni même d'instructions arithmétiques : on fusionne tout en une seule instruction supportant un grand nombre de modes d'adressages. On peut implémenter ces architectures de deux manières : soit en nommant les ports des unités de calcul, soit en intercalant des registres en entrée et sortie des unités de calcul.

L'implémentation avec des ports modifier

Dans le premier cas, l'instruction machine connecte directement l'ALU sur le bus interne. Mais avec cette organisation, les ports de l'ALU (les entrées et sorties de l'ALU) doivent être sélectionnables. On doit pouvoir dire au processeur que l'on veut connecter tel registre à tel port, tel autre registre à un tel autre port, etc. Pour ce faire, les ports sont identifiés par une suite de bits, de la même manière que les registres sont nommés avec un nom de registre : chaque port reçoit un nom de port. Il existe un port qui permet de déclencher le calcul d'une opération. Quand on connecte celui-ci sur un des bus internes, l'opération démarre. Toute connexion des autre ports d'entrée ou de sortie de l'ALU sur le banc de registres ne déclenche pas l'opération : l'ALU se comporte comme si elle devait faire un NOP et n'en tient pas compte.

Architecture déclenchée par déplacement - micro-architecture avec des ports.

L'implémentation avec des registres modifier

Dans le second cas, on intercale des registres intermédiaires spécialisés en entrée et sortie de l'ALU. Le but de ces registres est de stocker les opérandes et le résultat d'une instruction. Certains de ces registres servent à déclencher des instructions : lorsqu'on écrit une donnée dans ceux-ci, cela va automatiquement déclencher l’exécution d'une instruction bien précise par l'unité de calcul. Les autres registres ne permettent pas de déclencher des opérations : on peut écrire dedans sans que l'ALU ne fasse rien. Par exemple, un processeur de ce type peut contenir trois registres « ajout.opérande.1 », « ajout.déclenchement » et « ajout.résultat ». Le premier registre servira à stocker le premier opérande de l'addition. Pour déclencher l'opération d'addition, il suffira d'écrire le second opérande dans le registre « ajout.déclenchement », et l'instruction s’exécutera automatiquement. Une fois l'instruction terminée, le résultat de l'addition sera automatiquement écrit dans le registre « ajout.résultat ». Il existera des registres similaires pour la multiplication, la soustraction, les comparaisons, etc.

Architecture déclenchée par déplacement - micro-architecture avec des registres pour l'ALU.


Les entrées-sorties et périphériques modifier

Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et envoie sur le bus les données et commandes à adéquates. Ensuite, le processeur attend et reste connecté au bus tant que le périphérique n'a pas traité sa demande correctement, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique.

Pour résoudre ce problème, il suffit d'intercaler des registres d'interfaçage entre le processeur et les entrées-sorties. Une fois que le processeur a écrit les informations à transmettre dans ces registres, il peut faire autre chose dans son coin : le registre se charge de maintenir/mémoriser les informations à transmettre. Le processeur doit vérifier ces registres d’interfaçage régulièrement pour voir si un périphérique lui a envoyé quelque chose, mais il peut prendre quelques cycles pour faire son travail en attendant que le périphérique traite sa commande. Ces registres peuvent contenir des données tout ce qu'il y a de plus normales ou des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées.

Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état. Les registres de données permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données. Les registres de commande sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres. Enfin, beaucoup de périphériques ont un registre d'état, lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.

Registres d'interfaçage.

Les commandes sont traitées par un contrôleur de périphérique, qui va lire les commandes envoyées par le processeur, les interpréter, et piloter le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Si le contrôleur de périphérique peut très bien être séparé du périphérique qu'il commande, certains périphériques intègrent en leur sein ce contrôleur : les disques durs IDE, par exemple.

Contrôleur de périphérique.

Lorsqu'un ordinateur utilise un système d'exploitation, celui-ci ne connaît pas toujours le fonctionnement d'un périphérique ou de son contrôleur : il faut installer un programme qui va s'exécuter quand on souhaite communiquer avec le périphérique, et qui s'occupera de tout ce qui est nécessaire pour le transfert des données, l'adressage du périphérique, etc. Ce petit programme est appelé un driver ou pilote de périphérique. La « programmation » d'un contrôleur de périphérique est très simple : il suffit de savoir quoi mettre dans les registres pour paramétrer le contrôleur.

Un contrôleur de périphérique peut gérer plusieurs périphériques modifier

Dans ce que l'on a dit plus haut, nous sommes partis du principe que chaque périphérique est associé à un contrôleur de périphérique qui ne s'occupe que de lui. Et en effet, les contrôleurs les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un bus, et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de contrôleur de bus que de contrôleur de périphérique dans ce cas précis, mais passons.

Contrôleur de périphérique qui adresse plusieurs périphériques

Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même si ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatible. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir si il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.

L'adressage des périphériques par le contrôleur de périphérique modifier

Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.

Quand le contrôleur de périphérique envoie une transmission sur le bus, il doit faire en sorte qu'elle n'arrive qu'à destination. Pour cela, deux solutions sont possibles.

  • La première solution délègue cette responsabilité aux périphériques et à la mémoire. Chaque composant branché sur le bus vérifie si l'adresse envoyée par le processeur est bien la sienne : si c'est le cas, il va se connecter sur le bus (les autres composants restants déconnectés). En conséquence, chaque contrôleur contient un comparateur pour cette vérification d'adresse, dont la sortie commande les circuits trois états qui relient le contrôleur au bus.
  • La seconde solution est celle du décodage d'adresse. Elle utilise un circuit qui détermine, à partir de l'adresse, quel est le composant adressé. Seul ce composant sera activé/connecté au bus, tandis que les autres seront désactivés/déconnectés du bus.

Pour implémenter la dernière solution, chaque périphérique possède une entrée CS, qui active ou désactive le périphérique suivant sa valeur. Le périphérique se déconnecte du bus si ce bit est à 0 et est connecté s'il est à 1. Pour éviter les conflits sur le bus, un seul contrôleur de périphérique doit avoir son bit CS à 1. Pour cela, il faut ajouter un circuit qui prend en entrée l'adresse et qui commande les bits CS : ce circuit est un circuit de décodage partiel d'adresse.

Décodage d'adresse par le contrôleur de périphérique.

Le décodage d'adresse n'est que rarement utilisé quand on peut ajouter ou retirer des périphériques à la demande. Dans ce cas, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.

Les bus connectés au contrôleur de périphérique modifier

Le contrôleur de périphérique est donc un intermédiaire entre un ou plusieurs périphériques et le reste de l'ordinateur. Et sa nature d'intermédiaire se voit encore mieux quand on compare les bus auxquels il est connecté. D'un côté, il est connecté au processeur par un bus spécialisé appelé le bus d'entrées-sorties, de l'autre il est connecté aux périphériques par un autre bus séparé appelé le bus secondaire. Les deux bus n'ont aucune raison d'être les mêmes et ils sont presque toujours différents dans les faits. Le bus d'entrées-sorties, qui connecte le processeur au contrôleur de périphérique, est généralement un bus à haute performance, rapide, à haut débit. Il faut dire que le processeur est un composant rapide et qu'il vaut mieux que le contrôleur de périphérique communique avec lui avec une liaison rapide. Par contre, le bus secondaire qui relie le contrôleur aux périphériques est souvent un bus moins rapide, à débit moindre, à latence plus élevée. La raison est que les périphériques sont assez lents et qu'il est préférable d'utiliser un bus aux performances moindres, mais moins complexe et avec une interface plus simple.

À ce propos, on s'attend à ce que l'ordinateur utilise deux bus bien séparés, avec un bus mémoire d'un côté et un bus d'entrées-sorties de l'autre. C'est effectivement le cas sur beaucoup d'architectures simples, où les adresses sont petites et où le contrôleur de périphérique est simple. Cela a cependant des défauts, car un second bus demande pas mal de connexions, de broches sur le processeur, de fils d'interconnexion et autres ressources.

Bus entre processeur et contrôleur de périphérique.

Mais sur d'autres ordinateurs, le bus qui connecte le processeur au contrôleur de périphérique est le même bus qui connecte le processeur et la RAM. Nous verrons cela plus en détail dans le chapitre suivant, ainsi que dans le chapitre sur la carte mère, mais nous pouvons en parler rapidement ici. C'est quelque chose de fréquent et qui colle assez bien avec ce qu'on vient de dire dans le paragraphe précédent. Le bus mémoire est en effet un bus à haute performance, au même titre que le bus d'entrées-sorties. Le fait de partager le bus entre mémoire et entrées-sorties fait qu'on économise des fils, des broches sur le processeur, et d'autres ressources. Le câblage est plus simple, la fabrication aussi : les avantages sont nombreux. Et cela a d'autres avantages, notamment au niveau du processeur, qui n'a pas besoin de gérer deux bus séparés, mais un seul. Mais ce partage du bus mémoire et d'entrée-sortie n'est pas systématique. Toujours est-il que le bus qui relie processeur, mémoire et contrôleur de périphérique est appelé un bus système.

Partage du bus entre la mémoire et les périphériques

Un ordinateur moderne contient plusieurs contrôleurs de périphérique modifier

La situation se complique encore plus quand un ordinateur contient plusieurs contrôleurs de périphériques, ce qui est la norme de nos jours et l'est depuis déjà plusieurs décennies. Par exemple, un ordinateur de type PC assez ancien avait un contrôleur de périphérique pour le clavier, un autre pour la souris, un autre pour le port parallèle, un autre pour le port série et quelques autres. Par la suite, d'autres contrôleurs se sont greffés aux précédents : un pour l'USB, un intégré à la carte vidéo, un intégré à la carte son, et ainsi de suite. Concrètement, n'importe quel ordinateur récent contient plusieurs dizaines, si ce n'est centaines, de contrôleurs de périphériques.

La connexion du processeur aux contrôleurs de périphériques modifier

En théorie, la solution la plus simple serait de connecter chaque contrôleur de périphérique au processeur avec une connexion point à point, un ensemble de fils. Cette solution demande cependant beaucoup de fils et de connexions pour être praticable s'il y a beaucoup de contrôleurs à connecter. Elle est surtout utilisé sur les ordinateurs simples, où il y a quelques contrôleurs de périphériques, ce qui limite le nombre de broches et de fils à câbler.

Usage de plusieurs bus d'entrées-sorties

Pour limiter la casse, il est possible de connecter plusieurs contrôleurs de périphériques au processeur en utilisant un seul bus d'entrés-sorties. Mais c'est rarement suffisant.

Bus d'entrées-sorties multiplexé.

Là encore, il est possible de fusionner le bus d'entrées-sorties avec le bus mémoire.

Bus unique avec plusieurs contrôleurs de périphériques.

Le décodage d'adresse, pour les contrôleurs de périphériques modifier

Avec un seul bus pour connecter le processeur à plusieurs contrôleurs de périphériques, on retombe face au même problème que précédemment. Dans la section précédente, nous avions vu ce qui se passe quand on connecte plusieurs périphériques à un même contrôleur de périphérique. Ici, on remplace les périphériques par les contrôleurs de périphérique, et le contrôleur de bus par le processeur. Là encore, le problème est le même : toute transmission doit être prise en charge par le bon contrôleur de périphérique. Il ne faudrait pas qu'une transmission PCI-Express destinée à la carte graphique soit traitée par le contrôleur USB. Et là encore, la solution est la même : chaque contrôleur de périphérique a sa propre adresse mémoire.

Là encore, les solutions utilisées pour sélectionner un contrôleur parmi tous les autres sont les mêmes, le décodage d'adresse étant la possibilité la plus fréquente. Son utilisation est beaucoup plus fréquente, pour une raison assez simple. Les contrôleurs de périphériques sont tous placés soit dans les périphériques, soit sur la carte mère. Pour les contrôleurs sur la carte mère, on ne s'attend pas à ce qu'ils soient retiré ou ajoutés : les contrôleurs restent sur la carte mère et leur nombre ne change pas, les contrôleurs sont fixés une fois pour toutes. C'est le cas classique où on peut facilement utiliser le décodage d'adresse sans problèmes. Une fois les contrôleurs connectés, ajouter un circuit de décodage d'adresse est assez facile. Pour les contrôleurs placés dans les périphériques, comme ceux de la carte graphique et de la carte son, les périphériques en question sont connectés à des bus spécifiques, et le contrôleur du bus est généralement sur la carte mère. Ce qui rend là encore l'usage du décodage d'adresse facile pour le contrôleur de bus, la gestion des adresses sur le bus étant réalisé autrement.

Circuit de décodage d'adresse.

Pour résumer, les registres des périphériques sont identifiés par des adresses mémoires. Et les adresses sont conçues de façon à ce que les adresses des différents périphériques ne se marchent pas sur les pieds. Chaque périphérique, chaque registre, chaque contrôleur a sa propre adresse. D'ordinaire, certains bits de l'adresse indiquent quel contrôleur de périphérique est le destinataire, d'autres indiquent quel est le périphérique de destination, les restants indiquant le registre de destination.

Le chipset et l'intégration des contrôleurs de périphériques sur la carte mère modifier

Les anciens ordinateurs personnels disposaient de nombreux contrôleurs de périphériques, qui étaient tous connectés au bus système. Ils étaient ainsi reliés au processeur et à la mémoire directement. De nos jours, la plupart des contrôleurs de périphériques sont soit intégrés dans le périphérique, soit placés sur la carte mère. Les périphériques qui intègrent leur propre contrôleur sont nombreux et on peut citer les disques durs, les SSD, les cartes sons ou les cartes vidéo. Mais il reste encore de nombreux contrôleurs placés sur la carte mère. Ces derniers sont généralement regroupés dans un seul circuit, appelé le chipset, sauf pour quelques exceptions.

Grâce au chipset, chaque périphérique a un bus qui lui est dédié, et qui est généralement parfaitement adapté aux besoins du périphérique. De nos jours, chaque périphérique a son propre bus dédié, avec cependant quelques exceptions. Il y a ainsi un bus pour la souris, un autre pour le clavier, un autre pour les supports de stockage, un autre pour les cartes d'extension (carte son, carte graphique) et ainsi de suite. Sans chipset, cela ne serait pas possible. précisons aussi que le chipset intègre les mécanismes de décodage d'adresse et/ou de sélection des contrôleurs vus au-dessus. C'est lui qui redirige les commandes/données vers le contrôleur adéquat.

Chipset et contrôleurs de périphériques.

Ce regroupement a de nombreux avantages. Déjà, cette technique permet de profiter des techniques avancées de miniaturisation et l'intégration. Les anciens ordinateurs devaient souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permet de regrouper le tout dans un seul boîtier, limitant les besoins en branchements, en soudures, en coût de production, et j'en passe. Ensuite, cette technique limite le nombre d'interconnexions. Un seul bus relie le processeur, la mémoire, et le chipset, les autres bus sont tous connectés au chipset. Les interconnexions sont beaucoup plus simples et relier les différents composants est facile.


Dans le chapitre précédent, nous avons vu que les périphériques, leurs registres d’interface et leurs contrôleurs, ont chacun une adresse bien précise. Nous avions vu comment le contrôleur de périphérique adresse les périphériques et comment les contrôleurs de périphériques eux-mêmes ont des adresses. Mais nous n'avons pas vu comment le processeur utilise ces adresses. Il nous reste à voir comment le mélange entre adresses mémoires et adresses de périphérique peut se faire, comment le processeur évite les confusions entre adresses de périphériques et adresses mémoire.

Et pour cela, il y a plusieurs manières. La plus simple revient à séparer les adresses mémoire et les adresses périphériques, qui ne sont pas transmises sur les mêmes bus. L'autre méthode revient à utiliser un seul ensemble d'adresse, certaines étant allouées à la mémoire, d'autres aux périphériques. Les deux techniques portent des noms assez clairs : l'espace d'adressage séparé pour la première, l'espace d'adressage unifié pour la seconde. Voyons dans le détail ces deux techniques.

L'espace d’adressage séparé modifier

Avec la première technique, mémoire et entrées-sorties sont adressées séparément, comme illustré dans le schéma ci-dessous. La mémoire et les entrées-sorties ont chacune un ensemble d'adresse, qui commence à 0 et va jusqu’à une adresse maximale. On dit que la mémoire et les entrées-sorties ont chacune leur propre espace d'adressage.

Espaces d'adressages séparés entre mémoire et périphérique.

Avec cette technique, le processeur doit avoir des instructions séparées pour gérer les périphériques et adresser la mémoire. Il a des instructions de lecture/écriture pour lire/écrire en mémoire, et d'autres pour lire/écrire les registres d’interfaçage. L'existence de ces instructions séparées permet de positionner le bit IO correctement et de faire la différence entre mémoire et périphérique. Sans cela, le processeur ne saurait pas si une adresse est destinée à un périphérique ou à la mémoire et ne pourrait pas déterminer le bit IO associé.

L'implémentation matérielle de l'espace d'adressage séparé modifier

Avec un espace d'adressage séparé, on s'attend à avoir deux bus séparés : un pour la communication avec les périphériques et un autre pour la mémoire. Cela arrive et c'est un cas assez fréquent. Il faut dire que cette méthode est intuitive et permet de bien séparer les espaces d'adressage. Si la même adresse est à interpréter différemment selon qu'on l'envoie à un périphérique ou une mémoire, cela va bien avec le fait d'avoir deux bus d'adresse séparés. Outre son aspect intuitif, cette méthode est simple à implémenter, surtout au niveau du processeur.

Un autre avantage est qu'elle permet, en théorie, d'effectuer des accès à la mémoire pendant qu'on accède à un périphérique. Je dis en théorie, car le processeur doit être conçu pour, ce qui n'est pas gagné. Le défaut principal est lié à l'usage de deux bus : cela double le nombre de fils nécessaires, sans compter que le processeur doit avoir deux plus de broches qu'avec un bus unique.

Espace d'adressage séparé, implémentation avec deux bus séparés

Mais il est possible de mutualiser le bus d'adresse et de données. Par contre, autant bus d'adresse et de données sont partagés, autant on a bien deux bus de commandes, un pour le périphérique et un pour la mémoire.

Espace d'adressage séparé.

Si on mutualise le bus d'adresse, on doit indiquer la destination de l'adresse, périphérique ou mémoire. Une même adresse peut donc adresser soit une entrée-sortie, soit une case mémoire. La seule solution est d'utiliser un mécanisme de décodage d'adresse quelconque.

La solution la plus simple et la plus utilisée consiste faire la différence à partir du bit de poids fort de l'adresse. Le bit de poids fort de l'adresse, appelé le bit I/O, vaut 0 pour une adresse mémoire, et 1 pour une adresse de périphérique. L'adresse envoyée sur le bus est formée en récupérant l'adresse à lire/écrire et en positionnant le bit I/O à sa bonne valeur. Tout cela est réalisé par l'instruction adéquate : une instruction d'accès mémoire positionnera ce bit à 0, alors qu'une instruction d'accès aux périphériques le positionnera à 1.

Un défaut de cette solution est qu'elle impose d'avoir deux espaces d'adressage de même taille, un pour la/les mémoires, un autre pour les périphériques. Pas question d'avoir un espace d'adressage plus petit pour les périphériques, alors que ce serait possible avec deux bus séparés.

Bit IO.

L'espace d'adressage des périphériques contourne le cache modifier

Sur les processeurs disposant de mémoire cache, un problème dit de cohérence des caches peut survenir. Pour rappel, le cache est une petite mémoire censée accélérer les accès à la mémoire RAM, dans laquelle on stocke des copies des données en RAM. Un périphérique peut à tout instant modifier son état ou sa mémoire interne, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache.

Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. Il arriverait qu'un périphérique modifie une donnée en RAM, tandis que le cache continuerait de mémoriser une copie périmée de la donnée. Pour éviter tout problème, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre.

Les entrées-sorties mappées en mémoire modifier

La dernière technique que nous allons voir s'appelle l'espace d'adressage unifie, ou encore les entrées-sorties mappées en mémoire. Avec cette technique, certaines adresses mémoires sont redirigées automatiquement vers les périphériques. Le périphérique se retrouve inclus dans l'ensemble des adresses utilisées pour manipuler la mémoire : on dit qu'il est mappé en mémoire.

IO mappées en mémoire

Évidemment, les adresses utilisées pour les périphériques ne sont plus disponibles pour la mémoire RAM. C'est ce qui causait autrefois un problème assez connu sur les ordinateurs 32 bits. Certaines personnes installaient 4 gigaoctets de mémoire sur leur ordinateur 32 bits et se retrouvaient avec « seulement » 3,5 à 3,8 gigaoctets de mémoire, les périphériques prenant le reste. Ce « bug » apparaissait sur les processeurs x86 32 bits, avec un système d'exploitation 32 bits.

On remarque ainsi le défaut inhérent à cette technique : on ne peut plus adresser autant de mémoire qu'avant. Et mine de rien, quand on a une carte graphique avec 512 mégaoctets de mémoire intégrée, une carte son, une carte réseau PCI, des ports USB, un port parallèle, un port série, des bus PCI Express ou AGP, et un BIOS à stocker dans une EEPROM/Flash, ça part assez vite.

L'implémentation matérielle des entrées-sorties mappées en mémoire modifier

La mise en œuvre des entrées-sorties mappées en mémoire peut se faire de plusieurs manières différentes. Plus haut, nous avons vu que l'usage d'un espace d'adressage séparé se marie assez bien avec l'existence de deux bus distincts, un pour la mémoire, un pour les périphériques. On s'attend donc à ce que ce soit l'inverse avec un espace d'adressage unique. Après tout, qui dit un seul espace d'adressage dit un seul bus d'adresse, donc un seul bus. Mais dans les faits, les choses sont plus compliquées. Si l'usage de deux espaces d'adressage séparés pouvait s'implémenter avec un bus partagé couplé à un bit IO, l'inverse est aussi possible avec les entrées-sorties mappées en mémoire. On peut très bien les implémenter soit avec un bus unique partagé entre mémoire et périphériques, soit avec deux bus séparés. Voyons voir ce qu'il en est.

L'usage d'un bus partagé entre RAM et I/O modifier

Dans son implémentation la plus simple, les entrées-sorties mappées en mémoire utilisent un bus unique sur lequel on connecte la RAM et les contrôleurs de périphériques. L'avantage est qu'il n'y a plus besoin de dupliquer les bus, ce qui économise beaucoup de fils. De plus, le bit IO disparait et on n'a pas besoin d'instructions différentes pour accéder aux périphériques et à la mémoire. Tout peut être fait par une seule instruction, qui n'a pas besoin de positionner un quelconque bit IO qui n'existe plus. Le processeur possède donc un nombre plus limité d'instructions machines, et est donc plus simple à fabriquer. Par contre, impossible d'accéder à la fois à la mémoire et à un contrôleur d'entrées-sorties : si le bus est utilisé par un périphérique, le processeur ne peut pas accéder à la mémoire et doit attendre que le périphérique ait fini sa transaction (et vice-versa).

Bus unique avec entrées mappées en mémoire.
Exemple détaillé.

Comme dit plus haut, une partie des adresses pointe alors vers un périphérique, d'autres vers la RAM ou la ROM. La redirection vers le bon destinataire est faite par décodage partiel d'adresse. Le circuit de décodage partiel d'adresse va ainsi placer le bit CS de la mémoire à 1 pour les adresses invalidées, l’empêchant de répondre à ces adresses.

Décodage d'adresse avec entrées-sorties mappées en mémoire.

L'usage d'un bus séparé pour les I/O et la mémoire modifier

Il est cependant possible d'utiliser des entrées-sorties mappées en mémoire sans utiliser un bus unique. On peut adapter cette technique sur des ordinateurs où le bus mémoire est séparé du bus pour les périphériques. Il existe plusieurs manières d'implémenter le tout. La première, de loin la plus simple, consiste à accéder à la RAM d'abord, puis aux périphériques si elle ne répond pas. Une tentative d'accès en RAM fonctionnera du premier coup si l'adresse en question est attribuée à la RAM. Mais si l'adresse est associée à un périphérique, la RAM ne répondra pas et on doit retenter l'accès sur le bus pour les périphériques. L'implémentation est cependant compliquée, sans compter que les performances sont alors réduites, du fait des deux tentatives consécutives.

Les autres solutions font communiquer les deux bus pour que la RAM ou les périphériques détectent précocement les accès qui leur sont dédiés. La première solution de ce type consiste à ajouter un dispositif qui transmet les accès du bus mémoire vers le bus des périphériques. Mais le bus pour les périphériques est souvent moins rapide que le bus mémoire et l'adaptation des vitesses pose des problèmes.

IO mappées en mémoire avec séparation des bus

Une autre solution est d'intercaler, entre le processeur et les deux bus, un circuit répartiteur. Il récupère tous les accès mémoire et distribue ceux-ci soit sur le bus mémoire, soit sur le bus des périphériques. C'était ce qui était fait du temps où les chipsets des cartes mères existaient encore, à l'époque des premiers Pentium. A l'époque, la puce de gestion du bus PCI faisait office de répartiteur. Elle mémorisait des plages mémoires entières, certaines étant attribuées à la RAM, les autres aux périphériques mappés en mémoire. Elles utilisaient ces plages pour faire la répartition. De nos jours, le répartiteur est généralement intégré au processeur, avec l'intégration du contrôleur mémoire dans le CPU.

IO mappées en mémoire avec séparation des bus, usage d'un répartiteur

La cohérence des caches avec des entrées mappées en mémoire modifier

Un défaut de cette méthode apparait sur les processeurs disposant de mémoire cache. Le problème est similaire à celui rencontré pour l'espace d'adressage séparé, mais la solution est différente. Avec des entrées-sorties mappées en mémoire, rien ne ressemble plus à une adresse mémoire qu'une autre adresse mémoire. Le cache ne sait pas si l'adresse correspond à un périphérique ou non, et il est censé mettre en cache son contenu automatiquement. Mais le problème est que les adresses liées aux périphériques, qui correspondent à des registres ou à la mémoire des périphériques, peuvent être modifiés sans que le cache soit mis au courant. Le périphérique peut à tout instant modifier son état ou sa mémoire interne, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache.

La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches.

L'exemple des cartes graphiques modernes modifier

Notons qu'il est possible que toute la RAM du périphérique soit mappée en RAM, mais qu'il arrive souvent que seule une partie le soit. Un exemple classique est celui des cartes graphiques. Seule une portion de la mémoire vidéo (celle de la carte 3D) est mappée en mémoire RAM. Le processeur a donc accès à une partie de la mémoire vidéo, dans laquelle il peut lire ou écrire comme bon lui semble. Le reste de la mémoire vidéo est invisible du point de vue du processeur, mais manipulable par le GPU à sa guise.

Il est possible pour le CPU de copier des données dans la portion invisible de la mémoire vidéo, mais cela se fait de manière indirecte en passant par le GPU d'abord. Il faut typiquement envoyer une commande spéciale au GPU, pour lui dire de charger une texture en mémoire vidéo, par exemple. Le GPU effectue alors une copie de la mémoire système vers la mémoire vidéo, en utilisant un contrôleur DMA intégré au GPU. Pour résumer, tout se passe comme si la mémoire partagée entre CPU et GPU était coupée en deux : une portion sur le GPU et une autre dans la RAM système.

Interaction du GPU avec la mémoire vidéo et la RAM système sur une carte graphique dédiée

Pour un périphérique PCI-Express, la portion de mémoire vidéo visible est configurée via des registres spécialisés, appelés les Base Address Registers (BARs). La configuration des registres précise quelle portion de mémoire vidéo est adressable par le processeur, quelle est sa taille, sa position en mémoire vidéo, etc. Avant 2008, les BAR permettaient d’accéder à seulement 256 mégaoctets, pas plus. Après 2008, la spécification du PCI-Express ajouta un support de la technologie Resizable Bar', qui permet au processeur d’accéder directement à plus de 256 mégaoctets de mémoire vidéo, voire à la totalité de la mémoire vidéo. De nombreux fabricants de cartes graphiques commencent à incorporer cette technologie, qui demande quelques changements au niveau du système d'exploitation, des pilotes de périphériques et du matériel. Dans ce chapitre, nous allons voir, comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et envoie sur le bus les données et commandes à adéquates. Mais il existe cependant des contraintes temporelles quant à la communication entre périphérique et processeur. Les deux composants ne vont pas à la même vitesse, ce qui impose des méthodes d'accès particulières.

Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé. Mais les transferts dans l'autre sens sont plus problématiques.

Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le pooling, l'usage d'interruptions, et le Direct Memory Access.

Le pooling et les interruptions de type IRQ modifier

La solution la plus simple, appelée Pooling, est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le Pooling est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.

La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les interruptions. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :

  • arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
  • exécute un petit programme nommé routine d'interruption ;
  • restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
Interruption processeur

Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées IRQ, sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit.

Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du pooling, pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.

L'implémentation matérielle des interruptions modifier

Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. On peut distinguer deux cas : soit on fournit une interruption matérielle unique, soit on permet à plusieurs périphériques de déclencher une interruption. Nous allons voir ces deux cas dans cet ordre, histoire de voir le cas le plus simple (et peu utilisé), avant de voir le cas plus complexe.

L'entrée d'interruption modifier

Dans le cas le plus simple, le processeur n'est relié qu'un seul périphérique capable de générer des interruptions. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l'entrée d'interruption, souvent notée INTR ou INT.

L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d'entrée déclenchée par niveau logique et d'entrée déclenchée par front montant/descendant. Les noms sont barbares mais recouvrent des concepts très simples.

Les entrées d'interruption déclenchées par niveau logique modifier

Le plus simple est le cas de l'entrée déclenchée par niveau logique : la mise à 1/0 de cette entrée déclenche une interruption au cycle d'horloge suivant. En général, il faut mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.

Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée.

Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.

Les entrées d'interruption déclenchées par front montant/descendant modifier

A l'opposé, avec une entrée déclenchée par front montant/descendant, on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.

Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.

L'implémentation des IRQ multiples avec une entrée d'interruption modifier

Le cas avec plusieurs périphériques est plus compliqué. Dans une implémentation simple des IRQ, chaque périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher plusieurs entrées d'interruption au processeur, une par périphérique, ce qui fait beaucoup et limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.

Entrées d'interruptions séparées pour chaque périphérique

Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.

Entrée d'interruption partagée

Le contrôleur d'interruption modifier

Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un numéro d'interruption qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de entrées à entrées. Le processeur récupére ce numéro d'interruption, qui est généré à l'extérieur du processeur.

Pour implémenter cette solution, on a inventé le contrôleur d'interruptions. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption.

Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Et cette communication ne peut pas passer par l'entrée d'interruption, mais passe par un mécanisme dédié. Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Avec l'autre solution, le contrôleur d'interruption est mappé en mémoire, il est connecté au bus et est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.

Contrôleur d'interruptions IRQ

L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence a priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.

Les contrôleurs d'interruption en cascade modifier

Il est possible d'utiliser plusieurs contrôleurs d'interruption en cascade. C'était le cas sur les premiers processeurs d'Intel, notamment le 486, où on avait un contrôleur d'interruption maitre et un esclave. Les deux contrôleurs étaient identiques : c'était des Intel 8259, qui géraient 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, et le contrôleur maitre gérait le reste.

Le fonctionnement était le suivant. Si une interruption avait lieu en-dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Lee processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.

Contrôleurs d'interruptions IRQ du 486 d'Intel.
Intel 8259

En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclave et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.

Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 8, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.

Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.

Les Message Signaled Interrupts modifier

Les interruptions précédentes demandent d'ajouter du matériel supplémentaire, relié au processeur : une entrée d'interruption, un contrôleur d'interruption, de quoi récupérer le numéro de l'interruption. Et surtout, il faut ajouter des fils pour que chaque périphérique signale une interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande d'ajouter 5 fils sur la carte mère, pour les connecter au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.

Il existe cependant un type d'interruption qui permet de se passer des fils d'interruptions : les interruptions signalées par message, Message Signaled Interrupts (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens.

Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une adresse réservée. Le périphérique écrit un message qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations. L'adresse réservée est toujours la même, ce qui fait que le processeur sait à quelle adresse lire ou récupérer le message, et donc le numéro d'interruption. Le message est généralement assez court, il faut rarement plus, ce qui fait qu'une simple adresse suffit dans la majorité des cas. Il fait 16 bits pour le port PCI ce qui tient dans une adresse, le bus PCI 3.0 MSI-X alloue une adresse réservée pour chaque interruption.

Le contrôleur d'interruption détecte toute écriture dans l'adresse réservée et déclenche une interruption si c'est le cas. Sauf que le contrôleur n'a pas autant d'entrées d'interruption que de périphériques. A la place, il a une entrée connectée au bus et il monitore en permanence le bus. Si une adresse réservée est envoyée sur le bus d'adresse, le contrôleur de périphérique émet une interruption sur l'entrée d'interruption du processeur. Les gains en termes de fils sont conséquents. Au lieu d'une entrée par périphérique (voire plusieurs si le périphérique peut émettre plusieurs interruptions), on passe à autant d'entrée que de bits sur le bus d'adresse. Et cela permet de supporter un plus grand nombre d'interruptions différentes par périphérique.

Un exemple est le cas du bus PCI, qui possédait 2 fils pour les interruptions. Cela permettait de gérer 4 interruptions maximum. Et ces fils étaient partagés entre tous les périphériques branchés sur les ports PCI, ce qui fait qu'ils se limitaient généralement à un seul fil par périphérique (on pouvait brancher au max 4 ports PCI sur une carte mère). Avec les interruptions par message, on passait à maximum 32 interruptions par périphérique, interruptions qui ne sont pas partagées entre périphérique, chacun ayant les siennes.

Les généralités sur les interruptions matérielles modifier

Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent disponibles. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver. Autant désactiver les interruptions est possible autant pour les IRQ que les interruptions logicielles et exceptions, certaines fonctionnalités sont spécifiques aux interruptions matérielles.

Les priorités d'interruption modifier

Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Et certaines interruptions sont prioritaires sur les autres : par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.

La gestion des priorités est gérée par le contrôleur d'interruption, ou alors par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.

Le masquage d'interruptions modifier

Le masquage d'interruption permet de bloquer des interruptions temporairement, et de les exécuter ultérieurement, une fois le masquage d'interruption levé. Il est pris en charge soit par le contrôleur d'interruption, soit le processeur (surtout si le contrôleur d'interruption est absent). Il est utile quand on veut temporairement supprimer les interruptions, ce qui est utile dans certaines situations assez complexes, notamment quand le système d'exploitation en a besoin.

Le masquage peut être sélectif, à savoir qu'on peut décider d'ignorer certaines interruptions mais pas d'autres. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du watchdog timer. Pour cela, les processeurs disposent d'un registre appelé l'interrupt mask register. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.

Certaines interruptions ne sont pas masquables et sont systématiquement exécutées en priorité. Il s'agit généralement d'erreurs matérielles importantes, qui peuvent se ranger dans deux cas de figures : une défaillance matérielle, et un watchdog timer.

Pour rappel, un watchdog timer est un timer qui sert à détecter les dysfonctionnements matériel, notamment les plantages ou les freezes. Pour cela, le 'watchdog timer génère régulièrement un signal d'interruption non-masquable. La routine d'interruption réinitialise le 'watchdog timer, si tout se passe bien. On part du principe que si l'ordinateur ne réinitialise par le watchdog timer, alors c'est qu'il a planté.

Les défaillances matérielles regroupent des situations très variées : une perte de l'alimentation, une erreur de parité mémoire, une surchauffe du processeur, etc. Le résultat de telles interruptions est que l'ordinateur est arrêté de force, ou alors affiche un écran bleu. Les défaillances matérielles sont généralement détectées par un paquet de circuits dédiés, mais il est généralement difficile de savoir d'où provient l'erreur, quel est le matériel ou périphérique responsable. C'est possible s'il s'agit d'une erreur de mémoire RAM, comme une lecture dont l'ECC détecte une corruption de mémoire, ou d'un problème de surchauffe, d'alimentation. Mais dans les autres cas, difficile de savoir quel est le problème.

Il faut noter que certains processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables.

Le Direct memory access modifier

Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le bus mastering, qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.

Le Direct Memory Access, ou DMA, est une technologie de bus mastering qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.

Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.

Le contrôleur DMA modifier

Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le contrôleur DMA. Il est généralement intégré au périphérique, plus rarement placé sur la carte mère, parfois intégré au processeur, mais est toujours connecté au bus mémoire.

Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.

Le contrôleur DMA contient généralement deux compteurs : un pour l'adresse de la source, un autre pour le nombre de bytes restants à copier. Le compteur d'adresse est initialisé avec l'adresse de départ, celle du bloc à copier, et est incrémenté à chaque envoi de données sur le bus. L'autre compteur est décrémenté à chaque copie d'un mot mémoire sur le bus. Le second compteur, celui pour le nombre de bytes restant, est purement interne au contrôleur mémoire. Par contre, le contenu du compteur d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.

Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.

Controleur DMA

Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le controleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en palce quand le périphérique et la mémoire n'ont pas la même vitesse.

Les modes de transfert DMA modifier

Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode block, le mode cycle stealing, et le mode transparent.

Dans le mode block, le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !

Dans le mode cycle stealing, on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.

Et enfin, on trouve le mode transparent, dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.

Les limitations des controleurs DMA modifier

Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.

Les limitations en terme d'adressage modifier

La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.

Typiquement, le contrôleur DMA ne gére que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.

La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un tampon DMA ou encore un bounce buffer. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.

Les limitations en terme d'alignement modifier

La seconde limitation est que certains contrôleurs DMA ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 15, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.

À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.

Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.

DMA et cohérence des caches modifier

Le contrôleur DMA pose un problème sur les architectures avec une mémoire cache. Le problème est que le contrôleur DMA peut modifier n'importe quelle portion de la RAM, y compris une qui est mise en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Dans ce cas, le cache contient une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.

Cohérence des caches avec DMA.

Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multi-coeurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.

Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.

La technologie Data Direct I/O d'Intel permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.

Le 8237 et son usage dans les PC modifier

Le 8237 d'Intel était un contrôleur DMA présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 8 bits, mais qui avait des adresses de 16 bits. Le 8237 avait les mêmes propriétés : adressage sur 16 bits, copies octet par octet. Le processeur 8088 avait lui des adresses de 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empéché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.

L'usage du 8237 sur les premiers IBM PC modifier

Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (bank switching), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA.

Un défaut de cette solution est que le contrôleur DMA gère 4 pseudo-banques de 64 Kibioctets, au lieu d'un seul de 256 Kibioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une pseudo-banque à un autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une pseudo-banque. Mais si jamais les 3 premiers Kb sont dans une pseudo-banque, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la pseudo-banque, et la copie reprend au tout début de la pseudo-banque de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.

Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 canaux DMA, qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.

Les deux 8237 des bus ISA modifier

Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets, mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 esclave pour gérer le 8237 esclave. le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, ce qui libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :

Premier 8237 (maitre) :

  • Rafraichissement mémoire ;
  • Hardware utilisateur, typiquement une carte son ;
  • Lecteur de disquette ;
  • Disque dur, port parallèle, autres ;

Second 8237 (esclave) :

  • Utilisé pour mise en cascade ;
  • Disque dur (PS/2), hardware utilisateur ;
  • Hardware utilisateur ;
  • Hardware utilisateur.

Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.

L'usage du DMA pour les transferts mémoire-mémoire modifier

Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.

Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs co-processeurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).

Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un local store dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce local store, rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les local stores. Chaque SPE incorporait son propre contrôleur DMA.

Schema du processeur Cell


Dans un ordinateur, les composants sont placés sur un circuit imprimé (la carte mère), un circuit sur lequel on vient connecter les différents composants d'un ordinateur, et qui les relie via divers bus. Si on regarde une carte mère de face, on voit un grand nombre de composants assez divers.

Architecture matérielle d'une carte mère

Les composants les plus visibles sont les connecteurs, là où l'on vient brancher les périphériques, la cartes graphique, le processeur, la mémoire, etc. Dans l'ensemble, toute carte mère contient les connecteurs suivants :

  • Le processeur vient s’enchâsser dans la carte mère sur un connecteur particulier : le socket. Celui-ci varie suivant la carte mère et le processeur, ce qui est source d'incompatibilités.
  • Les barrettes de mémoire RAM s’enchâssent dans un autre type de connecteurs: les slots mémoire.
  • Les mémoires de masse disposent de leurs propres connecteurs : connecteurs P-ATA pour les anciens disques durs, et S-ATA pour les récents.
  • Les périphériques (clavier, souris, USB, Firewire, ...) sont connectés sur un ensemble de connecteurs dédiés, localisés à l'arrière du boitier de l'unité centrale.
  • Les autres périphériques sont placés dans l'unité centrale et sont connectés via des connecteurs spécialisés. Ces périphériques sont des cartes imprimées, d'où leur nom de cartes filles. On peut notamment citer les cartes réseau, les cartes son, ou les cartes vidéo.

Outre les connecteurs, une carte mère contient les différents bus qui connectent les composants entre eux, ainsi que divers circuits électroniques intégrés à la carte mère. Ces derniers n'ont pas de connecteurs, mais sont soudés à la carte mère et sont indispensables à son bon fonctionnement. Un bon exemple est celui du circuit d'alimentation, qui est en charge de la gestion de la tension d'alimentation. Il contient des composants aux noms à coucher dehors pour qui n'est pas électronicien : régulateurs de tension, convertisseurs alternatif vers continu, condensateurs de découplage, et autres joyeusetés.

Dans ce chapitre, nous allons étudier ces composants électroniques, mais aussi comment sont organisés les bus sur une carte mère. Nous allons voir que la façon dont les composants sont connectés entre eux par des bus a beaucoup changé au fil du temps et que l'organisation de la carte mère a évoluée au fil du temps. Les cartes mères se sont d'abord complexifiées, pour faire face à l'intégration de plus en plus de périphériques et de connecteurs. Mais par la suite, les processeurs ayant de plus en plus de transistors, ils ont incorporé des composants autrefois présents sur la carte mère, comme le contrôleur mémoire.

La gestion des fréquences et des durées modifier

Les composants d'un ordinateur sont cadencés à des fréquences très différentes. Par exemple, le processeur fonctionne avec une fréquence plus élevée que l'horloge de la mémoire RAM. Les différents signaux d'horloge sont générés par la carte mère. Intuitivement, on se dit qu'il y a un circuit dédié par fréquence. Mais c'est en fait une erreur : en réalité, il n'y a qu'un seul générateur d'horloge. Ce dernier produit une horloge de base, qui est « transformée » en plusieurs horloges, grâce à des montages électroniques spécialisés. Les avantages de cette méthode sont la simplicité et l'économie de circuits.

La fréquence de base est souvent très petite comparée à la fréquence du processeur ou de la mémoire, ce qui est contre-intuitif. Mais la fréquence de base est multipliée par les circuits transformateurs pour obtenir la fréquence du processeur, de la RAM, etc. Ainsi, la fréquence du processeur et de la RAM sont des multiples de cette fréquence de base. Naturellement, les circuits de conversion de fréquence sont donc appelés des multiplieurs de fréquence.

Génération des signaux d'horloge d'un ordinateur

Le générateur de fréquence : un oscillateur à Quartz modifier

Le générateur de fréquence est le circuit qui génère le signal d'horloge envoyé au processeur, la mémoire RAM, et aux différents bus. Sans lui, le processeur et la mémoire ne peuvent pas fonctionner, vu que ce sont des circuits synchrones dans les PC actuels. Il existe de nombreux circuits générateurs de fréquence, qui sont appelés des oscillateurs en électronique. Ils sont très nombreux, tellement qu'on pourrait écrire un livre entier sur le sujet. Entre les oscillateurs basés sur un circuit RLC (avec une résistance, un condensateur et une bobine), ceux basés sur une résistance négative, ceux avec des amplificateurs opérationnels, ceux avec des lampes à néon, et j'en passe ! Mais nous n'allons pas parler de tous les oscillateurs, la plupart n'étant pas utilisés dans les ordinateurs modernes.

Oscilatteur à Quartz, sur une carte mère.

La quasi-totalité des générateurs de fréquences des ordinateurs modernes sont des dispositifs à quartz, similaires à celui présent dans nos montres électroniques. Les oscillateurs à Quartz sont fabriqués en combinant un amplificateur avec un cristal de Quartz. Ces générateurs de fréquences fournissent une fréquence de base qui varie suivant le modèle considéré, mais qui est souvent de 32 768 Hertz, soit 2^15 cycles d'horloge par seconde. Ils ont une forme facilement reconnaissable, comme montré dans l'image ci-contre. Vous pouvez le repérer assez facilement sur une carte mère si jamais vous en avez l'occasion.

Pour ceux qui voudraient en savoir plus sur le sujet, sachez que le wikilivre d'électronique a un chapitre dédié à ce sujet, , disponible via le lien suivant. Attention cependant : le chapitre n'est compréhensible que si vous avez déjà lu les chapitres précédents du wikilivre sur l'électronique et il est recommandé d'avoir une bonne connaissance des circuits RLC/LC, sans quoi vous ne comprendrez pas grand-chose au chapitre.

Les multiplieurs de fréquence modifier

De nos jours, les ordinateurs font faire la multiplication de fréquence par un composant appelé une PLL (Phase Locked Loop), qui sont des composants assez versatiles et souvent programmables, mais il est aussi possible d'utiliser des circuits à base de portes logiques plus simples mais moins pratiques. Comprendre le fonctionnement des PLLs et des générateurs de fréquence demande des bases assez solides en électronique analogique, ce qui fait que nous n'en parlerons pas en détail dans ce cours.

Les timers intégrés à la carte mère modifier

Le générateur de fréquence est souvent combiné à des timers, des circuits qui comptent des durées bien précises et sont capables de générer des fréquences. Pour rappel, les timers sont des compteurs/décompteurs qui génèrent une interruption ou un signal quand ils atteignent une valeur limite. Ils permettent de compter des durées, exprimées en cycles d’horloge. Les fonctions de Windows ou de certains logiciels se basent là-dessus, comme celles pour baisser la luminosité à une heure précise, passer les couleurs de l'écran en mode nuit, certaines notifications, les tâches planifiées, et j'en passe. Ils permettent aussi d’exécuter une interruption à intervalle régulier, ou après une certaine durée. Par exemple, on peut vouloir générer une interruption à une fréquence de 60 Hz, pour gérer le rafraichissement de l'écran. Une telle interruption s'utilisait autrefois sur les anciens ordinateurs ou sur les anciennes consoles de jeux vidéo et portait le nom de raster interrupt.

Un ordinateur est rempli de timers divers. Dans ce qui va suivre, nous allons voir les principaux timers, qui sont actuellement intégrés dans les PC anciens et modernes. Ils se trouvent sur la carte mère ou dans le processeur, tout dépend du timer. Sur les anciens PC, on trouve deux timers : l'horloge temps réel et le PIT. L'horloge temps réel génère une fréquence de 1024 Hz, alors que le PIT est un Intel 8253 ou un Intel 8254 programmable par l'utilisateur. Les PC récents n'utilisent qu'un seul timer qui remplace les deux précédents : le High Precision Event Timer. Un ordinateur contient d'autres timers, comme le timer ACPI, le timer APIC, ou le Time Stamp Counter, mais ces derniers sont intégrés dans le processeur et non sur la carte mère.

Le plus important est qu'il y a une horloge temps réel cadencée à fréquence de 1 024 Hz, soit près d'un Kilohertz, ou du moins un circuit capable de l'émuler. Dans ce qui suit, nous la noterons RTC, ce qui est l'acronyme du terme anglais Real Time Clock. La RTC est utilisée par le système pour compter les secondes, afin que l'ordinateur soit toujours à l'heure. Vous savez déjà que l'ordinateur sait quelle heure il est (vous pouvez regarder le bureau de Windows dans le coin inférieur droit de votre écran pour vous en convaincre) et il peut le faire avec une précision de l'ordre de la seconde.

Pour savoir quel jour, heure, minute et seconde il est, l'ordinateur utilise la RTC, ainsi qu'un circuit pour mémoriser la date. La CMOS RAM mémorise la date exacte à la seconde près. Son nom nous dit qu'elle est fabriquée avec des transistors CMOS, mais aussi qu'il s'agit d'une mémoire RAM. Mais attention, il s'agit d'une mémoire RAM non-volatile, c'est à dire qu'elle ne perd pas ses données quand on éteint l'ordinateur. Nous expliquerons plus bas comment cette RM fait pour être non-volatile. La CMOS RAM est adressable, mais on y accède indirectement, comme si c'était un périphérique, à savoir que la CMOS RAM est mappée en mémoire. On y accède via les adresses 0x0007 0000 et 0x0007 0001 (ces adresses sont écrites en hexadécimal). Elle mémorise, outre la date et l'heure, des informations annexes, comme les paramètres du BIOS (voir plus bas).

La source d'alimentation de la RTC et de la CMOS RAM modifier

RTC avec pile au lithium intégrée.

L'horloge temps réel, l’oscillateur à Quartz et la CMOS RAM fonctionnent en permanence, même quand l'ordinateur est éteint. Mais cela implique que ces composants doivent être alimenté par une source d'énergie qui fonctionne lorsque l'ordinateur est débranché. Cette source d'énergie est souvent une petite pile au lithium localisée sur la carte mère, plus rarement une petite batterie. Elle alimente les trois composants en même temps, vu que tous les trois doivent fonctionner ordinateur éteint. Elle est facilement visible sur la carte mère, comme n'importe quelle personne qui a déjà ouvert un PC et regardé la carte mère en détail peut en témoigner.

Au passage : plus haut, nous avions dit que la CMOS RAM est une RAM non-volatile, c'est à dire qu'elle ne s'efface pas quand on éteint l’ordinateur. Et bien si elle l'est, c'est en réalité car elle est alimentée en permanence par une source secondaire de courant.

Sur la plupart des cartes mères, la RTC et la CMOS RAM sont fusionnées en un seul circuit qui s'occupe de la gestion de la date et des durées. Il arrive rarement que la pile au lithium soit intégrée dans ce circuit, mais c'est très rare. La plupart des concepteurs de carte mère préfèrent séparer la pile au lithium de la RTC/CMOS RAM pour une raison simple : on peut changer la pile au lithium en cas de problèmes. Ainsi, si la pile au lithium est vide, on peut la remplacer. Enlever la pile au lithium permet aussi de résoudre certains problèmes, en réinitialisant la CMOS RAM. L'enlever et la remettre réinitialise la CMOS RAM, ce qui remet à zéro la date, mais aussi les paramètres du BIOS.

Le Firmware : BIOS et UEFI modifier

La plupart des ordinateurs contiennent une mémoire ROM qui lui permet de fonctionner. Les plus simples stockent le programme à exécuter dans cette ROM, et n'utilisent pas de mémoire de masse. On pourrait citer le cas des appareils photographiques numériques, qui stockent le programme à exécuter dans la ROM. D'autres utilisent cette ROM pour amorcer le système d'exploitation : la ROM contient le programme qui initialise les circuits de l'ordinateur, puis exécute un mini programme qui démarre le système d'exploitation (OS). Cette ROM, et par extension le programme qu'elle contient, est appelée le firmware.

Le firmware est placée sur la carte mère, du moins sur les ordinateurs qui ont une carte mère. Sur les PC modernes, ce firmware s'occupe du démarrage de l'ordinateur et notamment du lancement de l'OS. Il existe quelques standards de firmware, utilisés sur les ordinateurs PC, utilisés pour garantir la compatibilité entre ordinateurs, leur permettre d'accepter divers OS, et ainsi de suite. Il existe deux standard : le BIOS, format ancien pour le firmware qui a eu son heure de gloire, et l'EFI ou UEFI, utilisés sur les ordinateurs récents.

Le BIOS modifier

Sur les PC avec un processeur x86, il existe un programme, lancé automatiquement lors du démarrage, qui se charge du démarrage avant de rendre la main au système d'exploitation. Ce programme s'appelle le BIOS système, communément appelé BIOS. Autrefois, le système d'exploitation déléguait la gestion des périphériques au BIOS. Ce programme est mémorisé dans de la mémoire EEPROM, ce qui permet de mettre à jour le programme de démarrage : on appelle cela flasher le BIOS.

En plus du BIOS système, les cartes graphiques actuelles contiennent toutes un BIOS vidéo, une mémoire ROM ou EEPROM qui contient des programmes capables d'afficher du texte et des graphismes monochromes ou 256 couleurs à l'écran. Lors du démarrage de l'ordinateur, ce sont ces routines qui sont utilisées pour gérer l'affichage avant que le système d'exploitation ne lance les pilotes graphiques.

De même, des cartes d'extension peuvent avoir un BIOS. On peut notamment citer le cas des cartes réseaux, certaines permettant de démarrer un ordinateur sur le réseau. Ces BIOS sont ce qu'on appelle des BIOS d'extension. Si le contenu des BIOS d'extension dépend fortement du périphérique en question, ce n'est pas du tout le cas du BIOS système, dont le contenu est relativement bien standardisé.

Les routines d'interruption du BIOS modifier

Le BIOS fournit des routines d'interruption pour gérer les périphériques et matériels les plus courants. Ce n'est pas pour rien que « BIOS » est l'abréviation de Basic Input Output System, ce qui signifie « programme basique d'entrée-sortie ». Ces routines sont standardisées de façon à assurer la compatibilité des programmes sur tous les BIOS existants. Ce standard, malgré sa simplicité, était extrêmement puissant. Il était possible de créer un OS complet en utilisant juste des appels de routine du BIOS. Par exemple, le DOS, ancêtre de Windows, utilisait exclusivement les routines du BIOS !

Mais une fois le système d'exploitation démarré, ces fonctions de base ne servent plus. Le vecteur d'interruption est mis à jour après le démarrage pour qu'il pointe non pas vers les interruptions du BIOS, mais vers les interruptions fournies par le système d'exploitation et les pilotes. Cette mise à jour est effectuée par le système d'exploitation, une fois que le BIOS lui a laissé les commandes.

L'accès au BIOS modifier

Organisation de la mémoire d'un PC doté d'un BIOS.

Il faut noter que le processeur démarre systématiquement en mode réel, un mode d'exécution spécifique aux processeurs x86, où le processeur n'a accès qu'à 1 mébioctet de mémoire (les adresses font 20 bits maximum). C'est un mode de compatibilité qui existe parce que les premiers processeurs x86 avaient des adresses de 20 bits, ce qui fait 1 mébioctet de mémoire adressable. En mode réel, Le premier mébioctet de mémoire est décomposé en deux portions de mémoire : les premiers 640 kibioctets sont ce qu'on appelle la mémoire conventionnelle, alors que les octets restants forment la mémoire haute.

Les deux premiers kibioctets de la mémoire conventionnelle sont réservés au BIOS, le reste est utilisé par le système d'exploitation (MS-DOS, avant sa version 5.0) et le programme en cours d’exécution. Pour être plus précis, les premiers octets contiennent non pas le BIOS, mais la BIOS Data Area utilisée par le BIOS pour stocker des données diverses, qui commence à l'adresse 0040:0000h, a une taille de 255 octets, et est initialisée lors du démarrage de l'ordinateur.

La mémoire haute est réservée pour communiquer avec les périphériques. On y trouve aussi le BIOS de l'ordinateur, mais aussi les BIOS des périphériques (dont celui de la carte vidéo, s'il existe), qui sont nécessaires pour les initialiser et parfois pour communiquer avec eux. De plus, on y trouve la mémoire de la carte vidéo, et éventuellement la mémoire d'autres périphériques comme la carte son.

Par la suite, le BIOS démarre le système d'exploitation, qui bascule en mode protégé, un mode d'exécution où il peut utiliser des adresses mémoires de 32/64 bits et utiliser la mémoire étendue au-delà du premier mébioctet.

Le démarrage de l'ordinateur modifier

Au démarrage de l'ordinateur, le processeur est initialisé de manière à commencer l'exécution des instructions à partir de l'adresse 0xFFFF:0000h (l'adresse maximale en mémoire réelle moins 16 octets). C'est à cet endroit que se trouve le BIOS. Le BIOS s’exécute et initialise l'ordinateur avant de laisser la main au système d'exploitation. En cas d'erreur à cette étape, le BIOS émet une séquence de bips, la séquence dépendant de l'erreur et de la carte mère. Pour cela, le BIOS est relié à un buzzer placé sur la carte mère. Si vous entendez cette suite de bips, la lecture du manuel de la carte mère vous permettra de savoir quelle est l'erreur qui correspond.

Le démarrage commence avec le POST (Power On Self Test), qui vérifie la stabilité de l'alimentation électrique de l'ordinateur, la stabilité de l'horloge, et l'intégrité des 640 premiers kibioctets de la mémoire. Le BIOS vérifie en premier lieu la stabilité de l'alimentation électrique de l'ordinateur. Il teste les tensions 12 V, 5 V et 3,3 V, et continue son exécution uniquement si celles-ci sont stables et à la bonne valeur. Si les tensions d'alimentation ne marchent pas comme prévu, le BIOS arrête immédiatement le démarrage, pour éviter d'endommager le processeur ou d'autres composants de l'ordinateur. Ensuite, le BIOS vérifie la stabilité de l'horloge et de quelques autres composants : les 640 premiers kibioctets de la mémoire, par exemple.

Ensuite, les périphériques sont détectés, testés et configurés pour garantir leur fonctionnement. Pour cela, le BIOS scanne la mémoire haute à la recherche des périphériques, tout en détectant la présence d'autres BIOS d'extension. Le BIOS est conçu pour lire la mémoire haute, par pas de 2 kibioctets. Par exemple, le BIOS regarde s'il y a un BIOS vidéo aux adresses mémoire 0x000C:0000h et 0x000E:0000h. Pour détecter la présence d'un BIOS d'extension, le BIOS système lit ces adresses et y recherche une signature, une valeur bien précise qui indique qu'une ROM est présente à cet endroit : la valeur en question vaut 0x55AA. Cette valeur est suivie par un octet qui indique la taille de la ROM, lui-même suivi par le code du BIOS d'extension. Si un BIOS d'extension est détecté, le BIOS système lui passe la main, grâce à un branchement vers l'adresse du code du BIOS d'extension. Ce BIOS peut alors faire ce qu'il veut, mais il finit par rendre la main au BIOS (avec un branchement) quand il a terminé son travail.

C'est plus ou moins à ce moment qu'est initialisé le vecteur d'interruption de l'ordinateur. Il se peut que juste avant, le BIOS initialise le vecteur d'interruption au démarrage de l'ordinateur avec les adresses des routines qu'il fournit. Si le vecteur d'interruption est écrit sans erreur dans la mémoire RAM, le BIOS fait émettre un petit bip par l'ordinateur, pour signaler que tout va bien. Par la suite, le système d'exploitation peut remplacer les adresses des interruptions par ses propres routines ou celles des pilotes : on dit qu'il détourne l'interruption. En clair, le vecteur d'interruption ne contiendra plus l'adresse servant à localiser la routine du BIOS, mais celle localisant la routine de l'OS. De fait, les routines du BIOS ne servent à rien avec les systèmes d'exploitation modernes. Si tout fonctionne bien, le BIOS va alors demander l'affichage d'un message à l'écran. Si un problème a lieu durant cette phase de test, le BIOS émet un signal sonore.

À ce stade du démarrage, une interface graphique s'affiche. La majorité des cartes mères permettent d'accéder à une interface pour configurer le BIOS, en appuyant sur F1 ou une autre touche lors du démarrage. Cette interface donne accès à plusieurs options modifiables, qui permettent de configurer le matériel. Ces paramètres sont stockés dans une mémoire flash ou EEPROM séparée du BIOS, généralement fusionnée avec la CMOS, qui est lue par le BIOS à l'allumage de l'ordinateur. Cette mémoire, la mémoire CMOS, est adressable via les adresses 0x0007:0000 et 0x0007:0001.

Par la suite, le BIOS exécute l'interruption 0x19, qui permet de démarrer un système d'exploitation. Pour cela, il lit le premier secteur du disque dur (le master boot record), qui contient toutes les informations pertinentes pour lancer le système d'exploitation. On rentre alors dans un domaine différent, celui du fonctionnement logiciel des systèmes d'exploitation. Je renvoie ceux qui veulent en savoir plus à mon wikilivre sur les systèmes d'exploitation, et plus précisément au chapitre sur Le démarrage d'un ordinateur.

EFI et UEFI modifier

L'EFI (Extensible Firmware Interface) est un nouveau standard de firmware, similaire au BIOS, mais plus récent et plus adapté aux ordinateurs modernes. Le BIOS avait en effet quelques limitations, pour part dues à l'organisation du MBR,pour une autre part au standard du BIOS lui-même. Par exemple, la table des partitions utilisée ne permettait pas de gérer des partitions de plus de 2,1 téraoctets. De plus, le BIOS devait gérer les anciens modes d'adressage mémoire des PC x86 : mémoire étendue, haute, conventionnelle, chose qui est quelque peu inutile de nos jours. Cela forçait le BIOS à utiliser des registres 16 bits lors de l’amorçage, ainsi qu'un ancien jeu d'instruction aujourd’hui obsolète. L'EFI a été conçu sans ces limitations, lui permettant d'utiliser tout l'espace d'adressage 64 bits, et sans limitations de taille de partition.

Cependant, la norme de l'EFI et de l'UEFI (une version plus récente) vont plus loin que simplement modifier le BIOS. Ils ajoutent diverses fonctionnalités supplémentaire, qui ne sont pas censées être un ressort d'un firmware de démarrage. Certains UEFI disposent de programmes de diagnostic mémoire, de programmes de restauration système, de programmes permettant d’accéder à internet et bien d'autres. L'EFI peut ainsi être vu comme un logiciel intermédiaire entre le firmware et l'OS.

Les bus de communication et le chipset modifier

L'organisation des carte mères des ordinateurs personnels a évolué au cours du temps pendant que de nombreux bus apparaissaient. On considère qu'il existe trois générations de cartes mères bien distinctes. La première génération utilise un bus unique pour tous l'ordinateur, les autres utilisent un plus grand nombre de bus.

La première génération : un bus système unique modifier

Pour les bus de première génération, un seul et unique bus reliait tous les composants de l'ordinateur. Ce bus s'appelait le bus système ou backplane bus. Le bus système était généralement connecté à divers composants, comme un contrôleur d'interruption, un contrôleur DMA, et d'autres composants. Le point important est que ces circuits étaient séparés les uns des autres et étaient connectés au bus.

Bus système

Ces bus de première génération avaient le fâcheux désavantage de relier des composants allant à des vitesses très différentes : il arrivait fréquemment qu'un composant rapide doive attendre qu'un composant lent libère le bus. Le processeur était le composant le plus touché par ces temps d'attente. Du fait de l'existence d'un bus unique, les entrées-sorties étaient mappées en mémoire.

La seconde génération : l'apparition du chipset modifier

Par la suite, le processeur et la mémoire sont devenus de plus en plus rapide avec le temps, au point que les périphériques ne pouvaient plus suivre. Il en a été de même avec la carte graphique, quelques temps plus tard. Les différents composants de la carte mère étaient alors séparés en deux catégories : les "composants lents" et les "composants rapides". Les composants rapides regroupent le processeur, la mémoire RAM et la carte graphique, les autres sont regroupés dans les composants lents. Les besoins entre des deux classes de composants étant différents, utiliser un seul bus pour les faire communiquer n'est pas idéal. Les composants rapides demandent des bus rapides, de forte fréquence, avec un gros débit pour communiquer, alors que les composants lents ont besoin de bus moins rapide, de plus faible fréquence.

Chipset séparé en northbridge et southbridge.

Ce problème a été résolu par l'intégration de deux circuits distincts : le northbridge et le southbridge. Le southbridge est le chipset proprement dit, à savoir qu'il regroupe les différents contrôleurs de périphériques, comme nous l'avions vu dans le chapitre "Le contrôleur de périphériques". Le northbridge, quant à lui, sert d'interface entre le processeur, la mémoire et la carte graphique et est connecté à chacun par un bus dédié. Il intègre notamment le contrôleur mémoire externe, ainsi que d'autres circuits importants.

Le bus qui relie le processeur au northbridge est appelé le Front Side Bus, abrévié en FSB. Le bus de transmission qui relie le northbridge au southbridge est un bus dédié, spécifique au chipset considéré, sur lequel on ne peut pas dire de généralités. Le schéma indique ces deux bus, en bleu pour le FSB, en rouge pour le second.

Le southbridge peut intégrer des composants comme le BIOS, la Real Time Clock, un contrôleur DMA, des contrôleurs d'interruption, et pas mal d'autres circuits. Sur la plupart des cartes mères, le BIOS est placé en-dehors du southbridge, mais il arrive que la CMOS RAM soit intégrée au southbridge. Sur les cartes mères des processeurs Intel 5 Series, la Real Time Clock était intégrée au southbridge. Les southbridge actuels intègrent les contrôleurs DMA et des contrôleurs d'interruptions 8259, qui sont soit présents dans leurs circuits, soit émulés avec des circuits équivalents. Ce qui est mis dans le southbridge est très variable selon la carte mère.

La troisième génération : l'intégration du northbridge au processeur modifier

Intel 5 Series architecture

Sur les cartes mères qui datent au moins des années 2000, depuis la sortie de l'Athlon64, le contrôleur mémoire a été intégré au processeur, ce qui est équivalent à dire que le northbridge est intégré au processeur. Concrètement, le processeur est directement connecté à la RAM et au southbridge, le reste de la carte mère ne change pas.

Les raisons derrière cette intégration ne sont pas très nombreuses. La raison principale est qu'elle permet diverses optimisations quant aux transferts avec la mémoire RAM. De sombres histoires de prefetching, d'optimisation des commandes, et j'en passe. La seconde est surtout que cela simplifie la conception des cartes mères, sans pour autant rendre vraiment plus complexe la fabrication du processeur. Les industriels y trouvent leur compte.

Par la suite, la carte graphique fût aussi connectée directement sur le processeur. Le northbridge disparu alors complétement. Sur les cartes mères intel récentes, le chipset est appelé le Platform Controler Hub, ou PCH. l'organisation des bus sur la carte mère qui résulte de cette connexion du processeur à la carte graphique, est illustrée ci-dessous, avec l'exemple du PCH.


Dans ce chapitre, nous allons étudier les périphériques et les cartes d'extension. Pour rappel, les cartes d'extension sont les composants qui se connectent sur la carte mère via un connecteur, comme les cartes sont ou les cartes graphiques. Quant aux périphériques, ce sont les composants connectés sur l'unité centrale. Dans ce chapitre, nous allons voir la carte son et les périphériques d’entrée et de sortie : les claviers, l'écran, etc. De nos jours, les cartes d'extension se font de plus en plus rares. Il y a quelques décennies, tout ordinateur avait une carte son, une carte réseau, une carte graphique, et éventuellement d'autres cartes d'extension. Mais de nos jours, avec les progrés de la miniaturisation, ce n'est plus le cas. La carte son et la carte réseau sont souvent intégrées directement à la carte mère, la carte graphique est intégrée directement dans le processeur. Bref, elles n'existent plus sous forme de cartes d'extension, comme c'était le cas à une époque, la seule exception étant les cartes graphiques à haute performances. Il n'en reste pas moins que ces circuits sont conçus d'une manière assez spécifique et qu'il est intéressant de les étudier à part. Nous regroupons ces cartes d'extension avec les périphériques, car les deux ont en commun d'être des entrées-sorties assez particulières, qui doivent communiquer avec le reste de la carte mère via une interface dédiée.

Le clavier modifier

Le clavier communique avec l'ordinateur dans les deux sens, contrairement à ce qu'on pourrait penser. Le premier sens est la transmission du clavier vers l'ordinateur : le clavier indique à notre ordinateur si une touche a été appuyée et laquelle. La transmission de l'ordinateur vers le clavier sert à configurer les diodes du clavier. Pour mieux comprendre, regardez votre clavier, près du clavier numérique. Vous verrez normalement trois diodes vertes : une pour indiquer si le verrouillage numérique est actif ou non, une autre pour le verrouillage majuscule et une autre pour l'arrêt défil. Et bien c'est l'ordinateur qui configure ces trois diodes, ce n'est pas fait à l'intérieur du clavier ! Un bon moyen de s'en rendre compte est de regarder ce qui se passe quand l'ordinateur plante et se fige : les trois diodes se figent elles aussi. La communication de l'ordinateur vers le clavier ne se limite pas à cela, c'est juste sa conséquence la plus visible. L'ordinateur peut envoyer d'autres informations de configuration au clavier, mais mettons cela de côté.

Pour indiquer quelle touche a été appuyée, le clavier envoie ce qu'on appelle un scancode à l'ordinateur. Ce scancode est un numéro et chaque touche du clavier se voit attribuer un scancode bien particulier, le même sur tous les claviers. Les scancodes des différentes touches sont standardisés, et il existe plusieurs standards de scancodes. Les tout premiers PC utilisaient un jeu de scancode différent de celui utilisé actuellement. Le jeu de scancode utilisé s’appelait le jeu de scancode XT. Le scancode est, sauf exception, un octet dont les 7 bits de poids faible identifient la touche, et le bit de poids fort indique si la touche a été appuyée ou relâchée. De nos jours, les claviers utilisent le jeu de scancode AT s'ils sont relié au PC via le port PS/2. Avec ce jeu de scancode, les numéros des touches sont modifiés. Le relâchement d'une touche est indiqué différemment : avec les scancodes AT, il faut ajouter un octet d'une valeur 0xF0 devant le numéro de la touche. Sans ce préfixe, on considère que la touche a été appuyée.

L'intérieur d'un clavier à petit nombre de touches modifier

À l'intérieur d'un clavier, on trouve un circuit relié aux touches par des fils électriques, qui se charge de convertir l'appui d'une touche en scancode. Naïvement, on pourrait penser que ce contrôleur est relié à chaque touche, mais ce genre d'organisation n'est utilisable que pour de tout petits claviers. Sur les claviers avec un faible nombre de touches, toutes les touches sont reliées à un encodeur combinatoire, ce qui suffit largement pour obtenir un clavier fonctionnel.

Exemple de clavier simple, qui utilise un encodeur combinatoire.

L'intérieur d'un clavier à grand nombre de touches modifier

Avec un clavier à 102 touches, la technique précédente ne fonctionne pas si bien : il nous faudrait utiliser 102 fils, ce qui serait difficile à mettre en œuvre. En fait, les touches sont reliées à des fils électriques, organisés en lignes et en colonnes, avec une touche du clavier à chaque intersection ligne/colonne. Pour simplifier, les touches agissent comme des interrupteurs : elles se comportent comme un interrupteur fermé quand elles sont appuyées, et comme un interrupteur ouvert quand elles sont relâchées.

Matrice clavier

Cette matrice est reliée à un circuit qui déduit les touches appuyées à partir de cette matrice de touches : le Keyboard Encoder. Ce circuit peut aussi bien être un circuit combinatoire, un circuit séquentiel fait sur mesure (rare), ou un microcontrôleur. Dans les premiers claviers de PC, l'encodeur clavier était un microcontrôleur Intel 8048. Les claviers actuels utilisent des microcontrôleurs similaires.

Matrice clavier + controleur

Le fonctionnement des claviers à grand nombre de touches modifier

Pour savoir quelles sont les touches appuyées, ce micro-contrôleur va balayer les colonnes unes par unes, et regarder le résultat sur les lignes. Plus précisément, notre circuit va envoyer une tension sur la colonne à tester. Si une touche est enfoncée, elle connectera la ligne à la colonne, et on trouvera une tension sur la ligne en question. Cette tension est alors interprété comme étant un bit, qui vaut 1. Si la touche à l'intersection entre ligne et colonne n'est pas enfoncée, la ligne sera déconnectée. Grâce à un petit circuit (des résistances de rappels au zéro volt intégrée dans le micro-contrôleur), cette déconnexion de la ligne et de la colonne est interprétée comme un zéro. Le microcontrôleur récupère alors le contenu des différentes lignes dans un octet. À partir de cet octet, il accède à une table stockée dans sa mémoire ROM, et récupère le scancode correspondant. Ce scancode est alors envoyé dans un registre à décalage, et est envoyé sur la liaison qui relie clavier et PC.

Gestion matrice clavier

Le phénomène de Ghosting et sa mitigation modifier

Cette organisation a tout de même un léger problème, qui se manifeste quand trois touches ou plus sont appuyées. Dans une telle situation, il se peut que le courant passe à travers les interrupteurs des touches et active des lignes qui ne devraient pas l'être. Pour donner un exemple, prenons la configuration suivante :

Ghosting - 1

Les problèmes surviennent quand le contrôleur active la troisième colonne. L'appui de la touche de droite active la colonne et la ligne sur laquelle elle se situe. L'appui de la touche immédiatement à gauche permet au courant sur la ligne de traverser la colonne. La touche en-dessous permet au courant de traverser la ligne elle aussi. Le résultat est que le résultat envoyé sur les ports de l'encodeur donnent l'illusion de l'appui d'une touche qui n'est pas appuyée, ici celle située sous la touche de droite.

Ghosting - 2

Une solution pour limiter ce phénomène de ghosting est de coupler chaque touche avec une diode, qui empêche le courant de passer dans les lignes ou colonnes qui ne devraient pas être activées.

Diode - ghosting

La souris modifier

Mécanisme interne d'une souris à boule.

Les anciennes souris fonctionnaient avec une boule, en contact avec la surface/le tapis de souris, que le mouvement de la souris faisait rouler sur elle-même. Ce mouvement de rotation de la boule était capté par deux capteurs, un pour les déplacements sur l'axe gauche-droite, et un pour l'axe haut-bas.

Les souris plus récentes contiennent une source de lumière, qui éclaire la surface sur laquelle est posée la souris (le tapis de souris). Cette source de lumière peut être une diode ou un laser, suivant la souris : on parle de souris optique si c'est une diode, et de souris laser si c'est un laser. Avec la source de lumière, on trouve une caméra qui photographie le tapis de souris intervalles réguliers, pour détecter tout mouvement de souris. Le tapis de souris n'est pas une surface parfaite, et contient des aspérités et des irrégularités à l'échelle microscopique. Quand on bouge la souris, les images successives prises par la caméra ne seront donc pas exactement les mêmes, mais seront en partie décalées à cause du mouvement de la souris. La caméra est reliée à un circuit qui détecte cette différence, et calcule le déplacement en question. Précisément, elle vérifie de combien l'image a bougé en vertical et en horizontal entre deux photographies de la surface.

La sensibilité de la souris modifier

Le déplacement mesuré par le capteur est envoyé à l'ordinateur. Ce qui est envoyé est le nombre de pixels de différence entre deux images de caméra, en horizontal et en vertical. Le système d'exploitation va alors multiplier ce déplacement par un coefficient multiplicateur, la sensibilité souris, ce qui donne le déplacement du curseur sur l'écran en nombre de pixels). Plus celle-ci est faible, meilleure sera la précision.

Les OS actuels utilisent l'accélération souris, à savoir que la sensibilité est variable suivant le déplacement, le calcul du déplacement du curseur étant une fonction non-linéaire du déplacement de la souris. Sur les anciens OS Windows, la sensibilité augmentait par paliers : un déplacement souris faible donnait une sensibilité faible, alors qu'une sensibilité haute était utilisée pour les déplacements plus importants. Cette accélération souris est désactivable, mais cette désactivation n'est conseillée que pour jouer à des jeux vidéos, l'accélération étant utile sur le bureau et dans les autres interfaces graphiques. Elle permet en effet plus de précision lors des mouvements lents (vu que la sensibilité est faible), tout en permettant des mouvements de curseurs rapides quand la souris se déplace vite.

Les performance de la souris modifier

Plus la résolution de la caméra de la souris est élevée, plus celle-ci a tendance à être précise. Cette résolution de la caméra de la souris est mesurée en DPI, Dot Per Inche. Il s'agit du nombre de pixels que la caméra utilise pour capturer une distance de 1 pouce (2,54 cm). Il va de soit que plus le DPI est élevé, plus la souris sera sensible. Pour un même déplacement de souris et à sensibilité souris égale, une souris avec un fort DPI entrainera un déplacement du curseur plus grand qu'une souris à faible DPI. Cela est utile quand on utilise un écran à haute résolution, mais pas dans d'autres cas.

Un autre paramètre important de la souris est la fréquence de rafraichissement déterminant le nombre d'informations envoyé à l'ordinateur par seconde. Suivant le connecteur, celle-ci varie. Une souris connecté au connecteur PS/2 (celui situé l'arrière de l'unité centrale), a une fréquence par défaut de 40 hertz, mais peut monter à 200 Hz si on le configure convenablement dans le panneau de configuration. Un port USB a une fréquence de 125 Hz par défaut, mais peut monter jusqu’à 1000 Hz avec les options de configuration adéquate dans le pilote de la souris. Plus cette fréquence est élevée, meilleure sera la précision et plus le temps de réaction de la souris sera faible, ce qui est utile dans les jeux vidéos. Mais cela entrainera une occupation processeur plus importante pour gérer les interruptions matérielles générées par la souris.

La carte son modifier

Dans un ordinateur, toutes les données sont codées sous forme numérique, c'est à dire sous la forme de nombres entiers/ou avec un nombre de chiffres fixes après la virgule. Les ordinateurs actuels utilisent du binaire. Les informations sonores sont aussi stockées dans notre PC en binaire. Toutes vos applications qui émettent du son sur vos haut-parleurs stockent ainsi du son en binaire/numérique. Seulement, les haut-parleurs et microphone sont des composants dits analogiques, qui codent ou décodent l'information sonore sous la forme d'un tension électrique, qui peut prendre toutes les valeurs comprises dans un intervalle. Dans ces conditions, il faut bien faire l'interface entre les informations sonores numérique du PC, et le haut-parleur ou micro-phone analogique. Ce rôle est dévolu à un composant électronique qu'on appelle la carte son.

L'architecture globale modifier

L'usage des microphones et des haut-parleurs demande de faire des conversions entre analogique et numérique. Les microphones transforment un signal sonore en un signal électrique analogique, alors que les haut-parleurs font l'inverse. Mais ces signaux analogiques ne sont pas compréhensibles par le reste de l'ordinateur, qui est composée de circuits numériques. C'est pour cela que la carte son doit faire la conversion des signaux analogiques en signaux numériques, en binaire.

  • Pour le micro-phone, la conversion doit traduire un signal analogique, proportionnel à l'intensité sonore, en signal numérique. Le convertisseur analogique-numérique, ou CAN, recueille la tension transmise par le micro-phone et la convertit en binaire. Il est relié à un composant qui mesure la tension envoyée par le microphone à intervalles régulier : l'échantillonneur.
  • De même, pour gérer les sorties haut-parleur, la carte son doit traduire le signal numérique provenant de l'ordinateur en un signal analogique utilisable par les haut-parleurs. Le convertisseur numérique-analogique, ou CNA, transforme des informations binaires en tension à envoyer à un haut-parleur.

En plus de tout cela, la carte son contient, au minimum, des circuits chargés de gérer les transferts entre la mémoire et la carte son, des circuits de communication avec le bus.

Architecture d'une carte son.

Il faut signaler que certaines cartes son possèdent un processeur, qui s'occupe du calcul de divers effets sonores. Ce processeur est techniquement un processeur de traitement de signal, aussi appelé DSP, à savoir un type de processeur que nous aborderons en détail d'ici quelques chapitres. Ce DSP est placé en aval du CNA, ce qui est évident quand on sait que le processeur ne peut traiter que des informations numériques.

CARTE SON avec DSP

En aval du CAN (ou du DSP, cas échéant), on trouve une petite mémoire dans laquelle sont stockées les informations qui viennent d'être numérisée. Cette mémoire sert à interfacer le processeur avec le CNA : le processeur est forcément très rapide, mais il n'est pas disponible à chaque fois que la carte son a fini de traduire un son. Les informations numérisées sont donc accumulées dans une petite mémoire FIFO en attendant que le processeur puisse les traiter. La même chose a lieu pour le CNA. Le processeur étant assez pris, il n'est pas forcément capable d'être disponible tous les 10 millisecondes pour envoyer un son à la carte son. Dans ces conditions, le processeur prépare à l'avance une certaine quantité d'information, suffisamment pour tenir durant quelques millisecondes. Une fois qu'il dispose de suffisamment d’informations sonores, il va envoyer le tout en une fois à la carte son. Celle-ci stockera ces informations dans une petite mémoire FIFO qui les conservera dans l'ordre. Elle convertira ces informations au fur et à mesure, à un fréquence régulière.

Architecture d'une carte son avec les mémoires tampons.

L'échantillonneur modifier

La tension transmise par le microphone varie de manière continue, ce qui rend sa transformation en numérique difficile. Pour éviter tout problème, la valeur de la tension est mesurée à intervalle réguliers, tout les 20 millisecondes par exemple. On parle alors d’échantillonnage. Le nombre de fois que notre tension est mesurée par seconde s'appelle la fréquence d'échantillonnage. Pour donner quelques exemples, le signal sonore d'un CD audio a été échantillonné à 44,1 kHZ, c'est à dire 44100 fois par secondes. Plus cette fréquence est élevée, plus le son sera de qualité, proche du signal analogique mesuré. C'est ce qui explique qu'augmenter la fréquence d'échantillonnage augmente la quantité de mémoire nécessaire pour stocker le son. Sur les cartes sons actuelles, il est possible de configurer la fréquence d'échantillonnage.

L’échantillonnage est réalisé par un circuit appelé l’échantillonneur-bloqueur. L'échantillonneur-bloqueur le plus simple ressemble au circuit du schéma ci-dessous. Les triangles de ce schéma sont ce qu'on appelle des amplificateurs opérationnels, mais on n'a pas vraiment à s'en préoccuper. Dans ce montage, ils servent juste à isoler le condensateur du reste du circuit, en ne laissant passer les tensions que dans un sens. L'entrée C est reliée à un signal d'horloge qui ouvre ou ferme l'interrupteur à fréquence régulière. La tension va remplir le condensateur quand l'interrupteur se ferme. Une fois le condensateur remplit, l'interrupteur est déconnecté isolant le condensateur de la tension d'entrée. Celui-ci mémorisera alors la tension d'entrée jusqu'au prochain échantillonnage.

Echantillonneur-bloqueur.

Les convertisseurs entre analogique et numérique modifier

Pour faire les conversions analogiques-numériques, on utilise deux circuits, vus dans les chapitres en début de cours :

  • Le circuit qui convertit un signal analogique en signal numérique cela est un CAN (convertisseur analogique-numérique).
  • Le circuit qui fait la conversion inverse est un CNA (convertisseur numérique-analogique).
CAN & CNA

Le signal numérique de la carte son, utilisé pour coder le son, utilise un certain nombre de bits. Sur les cartes sons actuelles, ce nombre de bits porte un nom : c'est la résolution de la carte son. Celui-ci varie entre 16 et 24 bits sur les cartes sons récentes.

Le multiplexeur analogique modifier

Une carte son peut supporter plusieurs entrées analogiques. Malheureusement, cela coûterait trop cher de mettre plusieurs CAN sur une carte son. À la place, les concepteurs de cartes sons mutualisent le CAN sur plusieurs entrées analogiques grâce à un multiplexeur analogique. Ce multiplexeur récupère les tensions des différentes entrées, et en choisira une qui est recopiée sur la sortie. Ce multiplexeur comporte une entrée de commande qui permet de choisir quelle entrée sera choisie pour être recopiée sur la sortie. Ce multiplexeur est ensuite suivi par un amplificateur,qui fait rentrer la tension fournit en entrée dans un intervalle de tension compatible avec le CAN.

Partage de CAN sur plusieurs entrées.

La carte graphique modifier

Les cartes graphiques sont des cartes qui s'occupent de communiquer avec l'écran, pour y afficher des images. Leur fonctionnement interne est cependant effroyablement complexe et il serait difficile de tout résumer en quelques lignes. Les anciennes versions de ce wikilivres ont tenté de le faire, mais le résultat était peu convainquant. La raison est que les cartes graphiques gèrent beaucoup de choses : elles gèrent l'affichage proprement dit, mais aussi le rendu 2D ou 3D, et ces trois fonctions sont relativement dépendantes les unes des autres. De plus, expliquer comment fonctionne une carte graphique demande de connaitre quelques bases sur le rendu 3D ou sur la couche logicielle de la programmation graphique, sur le pipeline graphique des API 3D modernes, et ce genre de subtilités. Et même quelque chose de très simple, comme l'affichage d'une image à l'écran, sans rendu 2D ou 3D, demande des explications assez longues, au moins un bon chapitre. Sachez cependant que nous reparlerons rapidement des cartes graphiques modernes dans les derniers chapitres de ce cours, dans le chapitre sur les architectures à parallélisme de données. De plus, je peux vous renvoyer vers un wikilivre spécialement dédié sur le sujet écrit par le même auteur que ce cours :


Les périphériques modernes incorporent de la mémoire. Un exemple classique est la mémoire vidéo, intégrée aux cartes graphiques modernes. La mémoire intégrée au périphérique est mappée en mémoire, en détournant suffisamment d'adresses mémoires normales. Elle est ainsi accessible par le processeur. En théorie, elle est censée être mappée en mémoire physique, et donc accessible seulement en espace noyau. Mais dans d'autres cas, elle est mappée dans l'espace d'adressage virtuel du processeur, dans la portion allouée au noyau.

De plus, beaucoup de périphériques modernes intègrent un processeur dans leurs circuits : cartes graphiques, cartes sons, etc. Le processeur intégré exécute des programmes dédiés, souvent fournis par les pilotes du périphérique ou le système d'exploitation, parfois par des applications spécialisées. Cela peut paraitre bizarre, mais sachez que c'est exactement ce qui se passe pour votre carte graphique si celle-ci a moins de vingt ans. Les shaders des jeux vidéos, exécutés par le processeur de la carte graphique, en sont un bon exemple).

Et ces processeurs peuvent gérer la mémoire virtuelle, ils disposent de mécanismes de traduction d'adresse comme la pagination ! L'utilité et le fonctionnement de la mémoire virtuelle liée aux périphériques sera l'objet de ce chapitre. Dans ce qui va suivre, nous allons faire la distinction entre la mémoire virtuelle du processeur et celle des périphériques. Pour bien appuyer cette distinction, nous parlerons de CPU-VM pour la première, d'IO-VM pour la dernière.

L'intérêt de l'IO-VM pour les périphériques modifier

L'avantage principal de l'IO-VM se manifeste sur les périphériques qui incorporent un processeur et une mémoire RAM. Sans mémoire virtuelle, ce processeur n'accède qu'à la mémoire du périphérique, rien de plus. Son espace d'adressage est prévu pour coller parfaitement à la mémoire RAM installée sur le périphérique. Mais avec l'IO-VM, le processeur intégré au périphérique peut aussi utiliser la RAM de l'ordinateur. Pour ne pas confondre les deux, nous allons parler de mémoire locale pour la RAM installée sur le périphérique, de mémoire système pour la RAM de l'ordinateur. L'intérêt de l'IO-VM est que le périphérique peut déborder sur la mémoire système si sa mémoire locale est pleine.

Un bon exemple est celui des cartes graphiques dédiées modernes. L'IO-VM des GPUs dédiés permet d'utiliser plus de RAM qu'il n'y en a d'installé sur la carte graphique. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en rab. Et c'est là qu'intervient la première différence avec la CPU-VM : les 2 gigas fictifs ne sont pas stockés sur le disque dur dans un fichier pagefile, mais sont dans la mémoire système. Pour le dire autrement, ces cartes dédiées peuvent utiliser la mémoire système si jamais la mémoire vidéo est pleine.

Mémoire virtuelle des cartes graphiques dédiées

L'espace d'adressage du processeur et du périphérique peuvent être unfiés ou séparés. Pour les cartes graphiques, des standards comme l'Heterogeneous System Architecture permettent au processeur et à une carte graphique de partager le même espace d'adressage. Une adresse mémoire est donc la même que ce soit pour le processeur ou la carte graphique.

Mémoire vidéo dédiée Mémoire vidéo unifiée
Sans HSA Desktop computer bus bandwidths Integrated graphics with distinct memory allocation
Avec HSA HSA-enabled virtual memory with distinct graphics card HSA-enabled integrated graphics

Le lien entre IO-VM et contrôleur DMA modifier

Le DMA permet aux périphériques modernes, notamment les cartes graphiques, d'accéder à la mémoire RAM et de l'utiliser. Et cela doit prendre en compte la mémoire virtuelle. Les périphériques anciens utilisaient des adresses physiques, à savoir qu'ils adressaient la mémoire RAM directement, sans passer par une traduction d'adresse. Mais les périphériques modernes prennent en compte la mémoire virtuelle et utilisent des adresses virtuelles.

L'usage de l'IO-VM permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues, grâce à des techniques d'extension d'espace d'adressage. Le résultat est que l'on peut se passer de tampons DMA.

Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contiguë dans l'espace d'adressage, mais éclaté en mémoire physique.

Un dernier avantage est que les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.

L'implémentation de la mémoire virtuelle pour les périphériques modifier

Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.

Pour faire la traduction d'adresse, les périphériques incorporent une MMU dédiée, la IOMMU qui s'occupe de traduire les adresses utilisées par les périphériques en adresses physiques à destination de la mémoire. Ils intègrent aussi des page table walkers, des TLBs et tout ce qui va avec l'incorporation de la mémoire virtuelle. Par exemple, les cartes graphiques modernes sont couplés à une MMU apelée la Graphics address remapping table, abrévié en GART. Cela vaut aussi bien pour les cartes graphiques utilisant le bus AGP que pour celles en PCI-Express.

La IOMMU n'est pas dans le périphérique proprement dit, il arrive qu'elle soit intégrée au chipset de la carte mère, voire dans le processeur. La IOMMU est connectée au bus mémoire directement, comme une MMU normale. Il n'est pas rare qu'elle soit partagée entre tous les périphériques, ou du moins avec plusieurs d'entre eux. Typiquement, il y a une IOMMU par bus. Ce faisant, plusieurs périphériques se partagent un même espace d'adressage, s'ils sont reliés au même bus, à la même IOMMU.

Généralement, les périphériques utilisent la pagination, pas la segmentation. Et qui dit pagination dit table des pages. Les périphériques DMA modernes ont leur propre table des pages dédiée, placée en mémoire RAM. Généralement, elle est séparée des autres tables des pages. En théorie, chaque accès à la table des pages demande d'accéder à la RAM, et donc de lancer une requête qui passe par le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès mémoire. Dans les faits, les périphériques incorporent des TLB, des caches qui mémorisent des correspondances adresses logique-physique, pour éviter d'accéder à la RAM fréquemment.

Un accès mémoire se passe comme suit. Le périphérique effectue un accès mémoire et accède à la TLB. Si l'entrée de la table des pages est dans la TLB, la TLB renvoie l'adresse physique à lire et l'accès mémoire se poursuit. Cependant, si la correspondance voulue n'est pas trouvée dans la TLB, le périphérique envoie une requête à l'IOMMU, via le bus PCI-Express. La IOMMU accéde alors à la table des pages, grâce à des page table walkers. En cas de succès de page, elle renvoie l'entrée de la table des pages demandée au périphérique, via le bus. Mais en cas de défaut de page, l'IOMMU échange des informations avec le périphérique PCI-Express, puis déclenche une interruption de défaut de page sur le processeur. Le système d'exploitation gére le défaut de page, puis la IOMMU reprend le relai et gère un succès de page.


Les mémoires de masse modifier

Maintenant que nous savons tout ce qu'il y a à savoir sur les entrées-sorties, ainsi que sur les mémoires, nous allons passer aux mémoires utilisées pour le stockage. Elles regroupent beaucoup de mémoires différentes : disques durs, disques SSD, clés UBS, CD et DVD-ROM, et bien d'autres. Ce sont des mémoires de grande capacité, qui servent à stocker de grosses quantités de données sur un temps assez long. Elles conservent des données qui ne doivent pas être effacés et sont donc des mémoires de stockage permanent (on dit qu'il s'agit de mémoires non-volatiles). Concrètement, elles conservent leurs données mêmes quand l'ordinateur est éteint, ce qui en fait des mémoires non-volatiles. Du fait de leur grande capacité, elles sont très lentes.

Dans ce chapitre, nous allons séparer le tout en deux sections : celle qui explore l’intérieur d'une mémoire de masse, et celle qui explore son interface externe. Par interface externe, on veut dire comment une mémoire de masse communique avec le reste de l'ordinateur. Tout comme pour le processeur ou une mémoire, il y a une séparation entre la micro-architecture et l'architecture externe. L'interface d'une mémoire de masse varie beaucoup suivant le bus utilisé. Par contre, peu importe le bus utilisé, toutes les mémoires de masse ont peu ou prou la même manière de communiquer avec le reste de l'ordinateur. Et c'en est au point où des mémoires de masse très différents peuvent utiliser le même bus pour communiquer avec l'ordinateur. Par exemple, le bus P-ATA était utilisé à la fois par les anciens disques durs, les lecteurs/graveurs de CD/DVD, et les premiers SSD. Il s'agit de mémoires très différentes dans leur fonctionnement interne : les supports de mémorisation ne sont pas les mêmes (magnétique pour les HDD, électronique pour les SSD, optique pour les CD/DVD), pourtant la manière de communiquer avec l'ordinateur était la même.

L'interface externe d'une mémoire de masse modifier

Là où les mémoires normales sont directement connectées sur le bus système, les mémoires de masse sont accédées à travers les ports d'entrée-sorties. Pour simplifier, elles ont la particularité d'être des périphériques, ou d'être connectés à la carte mère comme le sont les cartes d'extension. Leur lenteur pachydermique fait qu'elles n'ont pas besoin de communiquer directement avec le processeur, ce qui fait qu'il est plus pratique d'en faire de véritables périphériques. Elles sont à la fois des mémoires, mais aussi des périphériques/IO. C'est la raison pour laquelle nous allons en parler à ce moment-ci du cours, plutôt que dans la section sur les mémoires : en parler demande de faire appel à la fois aux concepts vus dans les chapitres sur les mémoires, et aux concepts des entrées-sorties. Tout ce qu'il faut retenir, c'est que la communication avec une mémoire de masse n'est pas très différente de la communication avec un périphérique. Il y a un contrôleur de périphérique, des registres d’interfaçage, du Direct Memory Access, des interruptions, etc. Mais les mémoires de masse ont cependant quelques spécificités, notamment la présence de blocs et d'adresses mémoire.

Les blocs et secteurs modifier

Du point de vue de l'ordinateur, une mémoire de masse est assez similaire à une mémoire normale. Elle est composée de plusieurs mots mémoire, chacun avec sa propre adresse. Mais la terminologie est quelque peu différente. Si le terme d'adresse est toujours utilisé, les mots mémoire des mémoires de masse sont appelés des blocs. Ils sont aussi appelés des secteurs pour les disques durs et médias optiques. Il s'agit de bloc de données de taille fixe, dont la taille est bien supérieure aux mots mémoire des RAM/ROM usuelles. Un bloc fait généralement 512 octets et les mémoires de masse récentes ont des blocs pouvant aller jusqu'à plusieurs kibioctets, là où les RAM/ROM ont usuellement des mots de mémoires de quelques octets, grand maximum 128. En soi, rien d'étonnant à avoir des blocs de grande taille, vu que les mémoires de masse ont une grande capacité : avec une grande capacité, avoir une taille de bloc importante permet de garder un nombre raisonnable d'adresses.

La taille d'un bloc est un compromis modifier

L'usage de blocs de grande taille est très adapté au stockage des fichiers, raison principale de l'existence des mémoires de masse. Les fichiers sont découpés en morceaux qui font la même taille qu'un bloc et le système d'exploitation réserve assez de blocs pour stocker tout le fichier. Vous remarquerez qu'en faisant ainsi, la taille prise par un fichier sur une mémoire de masse sera toujours un multiple de la taille d'un bloc. Elle sera égale à la taille d'un bloc, multipliée par le nombre de blocs utilisés. Mais cela pose un léger problème pour les petits fichiers. Par exemple, imaginons un fichier de 50 octets, enregistré sur une mémoire de masse dont le bloc fait 4 kibioctets : on gâchera une bonne partie du bloc à rien. La même chose a lieu quand les fichiers prennent un faible nombre de blocs. Par exemple, pour un fichier de 9 kibioctets et des blocs de 4 Ko, trois secteurs seront utilisés, dont le dernier ne contiendra qu'1 Ko de données utiles, 3 Ko étant gâchés. Ce phénomène de gâchis de mémoire, appelé la fragmentation interne, est d'autant plus important que la taille des blocs est importante. Utiliser des blocs de grande taille est donc une mauvaise idée.

Mais utiliser beaucoup de blocs de petite taille n'est pas sans défaut non plus. La première raison à cela est que, à capacité égale, des plus petits blocs signifie plus de blocs. Et diviser une mémoire de masse en un grand nombre de blocs rendra l'adressage plus compliqué. Chaque bloc a en effet une adresse, un numéro, et plus de blocs signifie plus d'adresses à gérer, avec tous les défauts que cela peut avoir : des circuits d'adressages plus compliqués et plus gourmands en transistors, des adresses plus longues et donc des trames plus longues, etc. Un autre défaut tient dans les performances : lire de grandes quantités de données est plus rapide quand on a des blocs assez gros. Si un gros fichier est répartit sur 1000 blocs, sa lecture totale demandera plus de temps que s'il est sur seulement 100 blocs, car il faudra faire moins d'opérations de lecture/écriture : 1000 dans le premier cas, seulement 100 dans le second. Moins de commandes de lecture/écriture à envoyer signifie que le bus de communication sera mieux utilisé et que son débit sera surtout consacré à transférer des données. Le débit du bus est important dans les performances des mémoires de masse. Pour résumer, la taille des blocs est un compromis entre débit de la mémoire et gâchis de mémoire.

L'advanced format modifier

Pour les disques durs anciens, les secteurs font une taille de 512 octets. Les CD et DVD avaient des secteurs de 2048 octets, 4 fois plus grands que ceux des disques durs. De nos jours, disques durs et SSD utilisent des secteurs plus gros, généralement de 4096 octets. La taille de 512 octets est appelée la taille standard, alors que secteurs plus gros sont appelés des secteurs de type advanced format. Ce format autorise des secteurs de 4096, 4112, 4160, et 4224 octets. La raison derrière ce changement est en grande partie liée à une question d'efficacité du stockage sur le support de mémorisation, comme nous allons immédiatement le voir dans la section sur la microarchitecture des mémoires de masse.

Mais utiliser des secteurs de 4 kibioctets posait des problèmes de compatibilité, avec les systèmes d'exploitation ou les cartes mères conçues pour des secteurs standards. Pour éviter tout problème, les premiers disques durs en advanced format utilisaient des mécanismes matériels de compatibilité. Ils émulaient un disque dur en secteur standard, alors que leurs secteurs étaient en advanced format. Pour les lectures, cela ne faisait pas vraiment de différences en termes de performance. Le disque dur lisait un secteur de 4 kibioctets, et sélectionnait les 512 octets demandés. Le secteur 4k entier pouvait être placé dans une mémoire cache, ce qui accélérait les lectures quand on accédait à des secteurs 512o consécutifs : les secteurs suivant étaient lus depuis ce cache et non depuis le disque dur. Pour les écritures, les choses étaient plus compliquées. Au lieu de simplement écrire un secteur 512 octets, il fallait : copier le secteur 4K dans le cache, modifier les 512 octets écrit dans ce cache, et réécrire tout le secteur 4k sur le disque dur. En soi, diverses optimisations au niveau de cache permettaient de limiter la casse, mais cela ralentissait les écritures.

Un autre point est qu'il fallait éviter des problèmes d'alignement. La taille de 4 kibioctets avait était choisie, entre autres, car c'est la taille standard des clusters du système d'exploitation. Celui-ci découpe les fichiers en clutsers de 4 kibioctets, et gérait des clusters au lieu de secteurs. Utiliser une taille de 4k pour les clusters et les secteurs parait être une très bonne idée. Mais il faut que les secteurs et les clusters soient alignés, c’est-à-dire qu'un secteur doit correspondre à un cluster. Il ne faut pas qu'un cluster soit à cheval sur deux secteurs, comme illustré dans le schéma ci-dessous. Sans ce genre de précautions, la perte de performance est drastique.

Secteurs et clusters non-alignés.

De nos jours, cette émulation a disparu et les systèmes d'exploitation modernes, ainsi que les BIOS, gèrent parfaitement des secteurs advanced format natifs.

Les commandes d'entrée-sortie modifier

La communication avec une mémoire de masse est la même qu'avec tout périphérique : l'ordinateur envoie une commande, qui demande à la mémoire de masse de faire quelque chose, et celle-ci l’exécute. La commande en question est soit une demande de lecture ou écriture, soit autre chose. Il y a beaucoup de commandes de configuration : pour configurer la mémoire de masse, pour déterminer l'état de la mémoire de masse en interrogeant le registre d'état du contrôleur de périphérique, etc. Mais les principales sont les commandes de lecture et d'écriture, qui permettent de lire ou d'écrire un bloc. Toute commande de lecture/écriture manipule un bloc entier. Il est impossible d'écrire seulement une portion de bloc, ou d'en lire seulement quelques octets.

Il existe des commandes de lecture/écriture qui permettent de lire/écrire plusieurs blocs consécutifs en mémoire, sur le même modèle que les accès en rafales des mémoires RAM.

Une commande de lecture demande de lire un bloc depuis la mémoire de masse : elle envoie l'adresse du bloc à lire, éventuellement quelques autres informations. La commande est encodée sur un certain nombre de bits, dont des bits de commande qui indiquent si la commande est une lecture/écriture/autre. Une commande d'écriture envoie la donnée à écrire, l'adresse du bloc à écrire, et éventuellement d'autres informations. Toutes les mémoires gèrent au minimum les commandes de lecture, et la plupart gèrent les commandes en écriture (sauf pour les CD/DVD non-réinscriptibles). Dans certains cas, la commande d'écriture ne contient pas la donnée à écrire, mais l'adresse de cette donnée dans la RAM principale. La donnée à écrire est alors stockée en mémoire RAM, séparée de la commande, et le transfert s'effectue par Direct Memory Acess, ou autre.

Évidemment, les mémoires de masse sont connectées au reste de l'ordinateur par un bus, tout comme pour les mémoires RAM/ROM. Sauf que si les RAM/ROM ont des mots mémoire qui ont la même taille que celle du bus, la taille d'un bloc est largement supérieure à la taille du bus pour une mémoire de masse. Les données d'un bloc sont donc transférées en plusieurs fois, morceau par morceau. Et c'est là que ce qu'on a vu sur les entrées-sorties rentre en jeu. Les transferts de données avec une mémoire de masse se font par une communication sur le bus, sous la forme d'une trame. C'est tout l'opposé de ce qu'on avait pour les RAM/ROM parallèles, où tout transfert de donnée se faisait en une fois, en un cycle d'horloge.

La mémoire de masse reçoit une commande et l'exécute. Une commande demande de faire des transferts entre mémoire RAM et mémoire de masse, qui passent souvent par DMA. Pour une lecture, le bloc lu ou est copié en mémoire RAM, morceau par morceau, généralement via DMA. Pour l'écriture, la donnée à écrire est en RAM et est transmise à la mémoire de masse morceau par morceau, là encore via DMA. Une fois la commande exécutée, la mémoire de masse prévient le processeur qu'elle est terminée, soit avec une interruption, soit en mettant à jour le registre d'état du contrôleur de périphérique.

La micro-architecture d'une mémoire de masse modifier

L'intérieur d'une mémoire de masse ressemble beaucoup à celui d'une mémoire normale : on trouve un support de mémorisation et éventuellement des circuits annexes.

Le support de mémorisation : supports magnétiques et optiques modifier

Toute mémoire de masse contient un support de mémorisation, à savoir quelque chose de physique qui stocke les données enregistrées dessus. Celui peut être :

  • magnétique, comme dans les disques durs ou les fameuses disquettes (totalement obsolètes de nos jours) ;
  • électroniques, comme dans les mémoires Flash utilisées dans les clés USB et disques durs SSD ;
  • optiques, comme dans les CD-ROM, DVD-ROM, et autres CD du genre ;
  • mécanique ou acoustique, dans quelques mémoires très anciennes et rarement utilisées de nos jours, comme les rubans perforés et quelques autres.

Les blocs sont mémorisés sur le support de mémorisation, qui est soit magnétique (HDD), soit optique (CD/DVD), soit électronique (SSD). Le support de mémorisation est généralement découpé en blocs, mais il arrive rarement que ce ne soit pas le cas. En fait, sur les SSD, le découpage en blocs n'est pas si évident quand on regarde le support de mémorisation. Dans ce cas, les blocs sont une vue de l'esprit, un moyen de transférer des données entre l'ordinateur et une mémoire de masse, mais pas une réalité. Ce sont des abstractions qui permettent de communiquer avec la mémoire de masse, pas quelque chose de physique. Mais sur les disques durs et les médias optiques, le support de mémorisation est bel et bien découpé en blocs. Dans ce cas, on parle alors de secteur, bien que la terminologie ne soit pas forcément très stricte.

Un secteur contient les données du secteur, mais aussi d'autres informations utiles pour la mémoire de masse. En premier lieu, on trouve des informations qui permettent d'indiquer le début du secteur. Les disques durs et médias optiques sont formés d'un support magnétique continu, qu'il faut découper en secteur en indiquant physiquement là où commence le secteur. Avant les données, on trouve donc un préambule que le contrôleur utilise pour déterminer la position des secteurs. Après les données, on trouve des bits d'ECC, à savoir des bits de correction/détection d'erreur, qui permettent de vérifier si les données sont valides ou corrompues. Il faut noter que les bits d'ECC sont présents sur les SSD, mais pas le préambule.

Secteur.

Le préambule et les bits d'ECC sont généralement une petite perte sèche. Une partie du support magnétique/optique est gâchée pour stocker les préambules et les bits d'ECC. Et la taille d'un secteur impacte cette perte. Plus les secteurs sont gros, moins on a de secteurs à capacité égale, donc on a moins de préambules et moins de pertes. L'espace mémoire pris par les bits d'ECC n'est pas censé changer en fonction du nombre de secteurs. Mais augmenter la taille des secteurs permet d'utiliser des techniques d'ECC plus efficaces, qui prennent moins de bits pour une qualité équivalent. En clair, plus la taille d'un secteur est grande, moins on a de pertes et plus la capacité effective est importante. C'est d'ailleurs la raison qui a poussé les fabricants de disques durs et de CD/DVD à dépasser la taille standard de 512 octets. Par exemple, le passage à des secteurs de 4 kibictets pour les disques durs modernes visait à réduire cette perte sèche.

Comparaison de la perte sèche entre des secteurs standard et des secteurs de type Advanced format (4Kib).

Le contrôleur de périphérique d'une mémoire de masse modifier

Sur certaines mémoires de masse, on n'a rien d'autre qu'un support de mémorisation. C'est le cas des mémoires optiques : un CD ou DVD est un simple support de mémorisation, sans contrôleur électronique. Mais les autres mémoires intègrent de l'électronique.

Pour les CD et DVD, cette électronique est présente dans le lecteur/graveur de CD/DVD.

Cette électronique peut être découpée en deux circuits séparés : un contrôleur mémoire et un circuit d'interface avec le bus. Les seules différences tiennent dans la complexité de ces différents composants. Le circuit d'interface avec le bus est beaucoup plus complexe, car il doit communiquer avec des bus comme le PCI-Express, l'USB, ou autres, qui sont beaucoup plus complexes que les bus mémoires normaux. Le contrôleur mémoire n'est en rien un décodeur, mais est un circuit beaucoup plus complexe et bourré d'électronique. Dans la plupart des cas, ce contrôleur est un véritable processeur couplé à une RAM et une ROM. La ROM contient un firmware qui peut être mis à jour si besoin.

Microarchitecture d'une mémoire de masse

Le disk buffer modifier

Une mémoire de masse est beaucoup plus lente que le reste de l'ordinateur, sans compter que les transferts de données doivent se faire quand le bus est libre. Il arrive que le disque dur reçoive une commande de lecture, l’exécute, mais que le bus soit occupé alors que la lecture a lieu dans le support de mémorisation. Idem pour les écritures : le support de mémorisation peut être occupé, alors qu'une commande d'écriture soit en cours de transfert sur le bus. Pour éviter tout problème, les mémoires de masse incorporent une mémoire tampon appelée le disk buffer, ou encore le cache disque intégré.

Dans sa version la plus simple, le disk buffer contient un seul bloc. Il est alors une simple mémoire tampon qui sert d'intermédiaire entre le support de mémorisation et le bus. Lors d'une lecture, les données lues depuis le support de mémorisation sont copiées dans le disk buffer, puis attendent dedans que le bus soit libre. Pareil lors d'une écriture : les données à écrire sont accumulées dans le disk buffer, et sont copiées sur le support de mémorisation quand celui-ci est libre.

Disk buffer fusionné avec un cache d'écriture.

En clair, avec un disk buffer, une écriture se fait en deux temps : le bloc est écrit d'abord dans le disk buffer, puis transféré sur le support de mémorisation quand celui-ci est libre. Et cela permet une optimisation assez intéressante : le disque dur prévient le processeur quand le bloc est écrit dans le disk buffer, pas dans le support de mémorisation. L'écriture est donc plus rapide, même si elle est en réalité différée quand le contrôleur de disque l'a décidé. Mais cette optimisation pose un problème en cas de coupure de courant. Les données à écrire vont attendre durant un moment avant que les plateaux soient libres pour démarrer l'écriture. Si jamais une coupure de courant se produit, les données présentes dans la mémoire tampon, mais pas encore écrites sur le disque dur, sont perdues.

En théorie, le disk buffer ne contient qu'un seul bloc. Mais sur les mémoires de masse à haute performance, le disk buffer est modifié de manière à pouvoir contenir plusieurs blocs. Cela permet diverses optimisations pour améliorer les performances. Une première possibilité d'optimisation demande de séparer le disk buffer en deux : un disk buffer spécialisé dans les écritures, et un autre pour les lectures.

Le disk buffer pour les lectures est appelé improprement le cache de lecture, mais le terme le plus adapté serait plutôt tampon de prélecture. Il ne s'agit pas d'un cache, car il est rare qu'un bloc soit relu sous peu. Son rôle est d'utiliser la technique dite du préchargement. Avec cette technique, le support de mémorisation accède successivement non pas à un seul bloc, mais à plusieurs blocs consécutifs. Ceux-ci sont alors chargés dans le disk buffer, et sont disponibles à l'avance pour de futures lectures. Le préchargement marche très bien si jamais on précharge les blocs suivants, pendant que la mémoire de masse attend que le bus soit libre, ou que le bloc lu est transféré.

Disk buffer avec cache de lecture et préchargement

Le disk buffer spécialisé dans les écritures est aussi modifié de manière à pouvoir gérer plusieurs blocs en même temps. Il porte alors le nom de write buffer, ou encore de tampon d'écriture. Le tampon d'écriture est une mémoire FIFO, qui mémorise plusieurs écritures en attente. L'idée est de mettre en attente un grand nombre d'écriture, de manière à prioriser les lectures. Ainsi, si on doit effectuer un grand nombre de lectures/écritures, les écritures sont mises en attente dans le tampon d'écriture, alors que le support de mémorisation est utilisé pour les lectures en priorité. Les écritures ont lieu soit quand le support de mémorisation est libre, soit quand il est plein et que les écritures doivent avoir lieu. A noter que si on veut lire un bloc récemment écrit, il se peut que la donnée écrite soit en réalité dans le tampon d'écriture, mais pas encore sur le support de mémorisation. Dans ce cas, la donnée doit être lue depuis le tampon d'écriture.

Diverses optimisations permettent d'optimiser au mieux les écritures sur le support de mémorisation, avec l'aide du tampon d'écriture. L'optimisation principale permet d'accélérer certaines écritures successives. Elle porte le nom de combinaison d'écriture. Comme son nom l'indique, elle consiste en la fusion de plusieurs écritures en une seule. Si plusieurs écritures dans un même bloc sont dans le tampon d'écriture, alors seule la toute dernière est envoyée au support de mémorisation.

Tampon d'écriture d'une mémoire de masse

Il est possible de fusionner le tampon de prélecture et le tampon d'écriture, ce qui donne un véritable cache, capable de mémoriser plusieurs blocs. Il peut ainsi servir de cache pour les accès en lecture : si jamais une donnée est lue plusieurs fois de suite, le premier accès charge le bloc lu dans le cache, les accès ultérieurs relisent le bloc depuis le cache, ce qui est plus rapide. Mais une telle situation est assez rare. On peut aussi précharger les blocs proches de celui lu, afin de faciliter des lectures ultérieures à ces blocs préchargés. Un tel cache facilite l'implémentation de la combinaison d'écriture : si jamais un bloc est modifié plusieurs fois de suite, ces modifications se font dans le cache et seul le résultat final est ensuite écrit dans le support de mémorisation : on a en quelque sorte fusionné plusieurs écritures en une seule. Des écritures dans le support de mémorisation ont été évitées, ce qui laisse la place pour d'autres commandes.

La power loss protection modifier

Une mémoire de masse peut perdre des données quand on coupe le courant, en raison de la présence du disk buffer. Le disk buffer est totalement effacé lors d'une coupure de courant, vu qu'il est fait avec des SRAM/DRAM. Le contenu du disk buffer doit idéalement être totalement transféré sur le support de mémorisation avant une coupure de courant, sans quoi des données peuvent être perdues. Par exemple, une écriture dans un fichier peut être mise en attente dans le disk buffer et perdue lors d'une coupure de courant, alors que le système d'exploitation a déjà mis à jour ses données internes après une écriture (listes de secteurs, taille du fichier, etc). La donnée est alors perdue, même si l'OS ne voit rien.

Pour éviter ce genre de problème, la solution idéale est de désactiver la mise en attente des écritures dans le disk buffer. Toute écriture a alors lieu immédiatement sur le support de mémorisation. Une option du système d'exploitation permet de faire ce genre de choses, mais sa désactivation engendre de grosses pertes de performance. Une autre solution est purement matérielle : concevoir la mémoire de masse de manière à gérer le cas d'une perte de courant. Pour cela, on ajoute des condensateurs à la mémoire de masse, qui servent de réservoirs à électricité. En cas de coupure de courant, les condensateurs fournissent du courant assez longtemps pour que la mémoire de masse ait le temps de sauvegarder ce qu'il faut. La mémoire de masse monitore en permanence le statut de l’alimentation. Si elle est coupée, la mémoire de masse stoppe immédiatement tout ce qu'elle faisait et démarre immédiatement la procédure de sauvegarde. Mais toutes les mémoires de masse n'ont pas cette technique de protection, appelé la power loss protection. Seule une minorité marketée pour le fait.

La performance des mémoires de masse modifier

Les caractéristiques d'une mémoire de masse sont plus ou moins les mêmes que pour une mémoire normale : elle a une capacité mémoire, un temps d'accès et un débit binaire. Nous allons mettre de côté la capacité mémoire, sur laquelle il n'y a pas grand-chose à dire, et nous concentrer sur les deux derniers points. Nous avions déjà abordé tout cela dans le chapitre sur "La performance d'un ordinateur", mais quelques rappels ne font pas de mal : le temps d'accès est le temps que met la mémoire pour répondre à une demande de lecture/écriture. Le débit binaire est la quantité de données pouvant être lue/écrite par seconde, exprimée en octets. Idéalement, une mémoire doit avoir un temps d'accès faible, et un débit binaire élevé. Les mémoires de masse ont un temps d'accès élevé, du fait de leur grande capacité (hiérarchie mémoire). Par contre, elles ont généralement un bon débit binaire. La plupart des optimisations des mémoires de masse visent à augmenter leur débit, et non à réduire un temps d'accès de toute façon trop élevé.

Le nombre d'IOPS modifier

Le débit binaire d'un périphérique est le produit de deux facteurs : le nombre d'opérations de commandes par secondes, et la taille des blocs (qui est la quantité de données échangée par commande). Cela peut se résumer par la formule suivante :

, avec D le débit binaire, IOPS le nombre d'opérations disque par secondes, et T la taille d'un bloc/secteur.

Néanmoins, il s'agit d'une équation théorique, qui ne dit rien sur le nombre d'IOPS. Celui-ci est extrêmement variable et dépend de beaucoup de paramètres. Le plus important est la différence entre les accès séquentiels et aléatoires. Le terme accès séquentiel est assez parlant : il s'agit d'accès où on accède successivement à des données contiguës. Typiquement, il s'agit de balayer une portion de mémoire, en partant d'une adresse, puis en accédant à la suivante, puis à celle qui suit, etc. On accède donc à la mémoire adresse par adresse, dans l'ordre des adresses croissantes ou décroissantes. Par contre, les accès aléatoires sont mal nommés. Ils correspondent à des accès mémoire où des lectures/écritures consécutives accèdent à des adresses éloignées les unes des autres.

Random vs sequential access

Toutes les mémoires de masse ne gèrent pas les accès séquentiels ou aléatoires de la même manière. Prenons le cas des SSD, les fameux Solid State Drive : ils ont de bonnes performances pour les deux type d'accès, avec un léger avantage pour les accès séquentiels. Les disques durs, disquettes et CD/DVD sont différents. Ils ont des performances absolument déplorables pour les accès aléatoires, mais excellents pour des accès séquentiels. Les raisons à cela sont liées à leur fonctionnement interne et à la répartition des données sur le support de mémorisation, et nous verrons cela en détail dans le chapitre sur le disque dur. Toujours est-il que ce détail a son importance pour le système d'exploitation : il a tout intérêt à placer un fichier dans des secteurs contigus, plutôt que dans des secteurs éloignés sur le disque dur. Les accès à des fichiers de grande taille donnent généralement des accès séquentiels, si le système d'exploitation s'est bien débrouillé pour utiliser des secteurs contigus. Par contre, l'accès à de nombreux fichiers de petite taille donne des accès aléatoires, ces fichiers n'étant pas forcément placés les uns après les autres sur le disque dur.

Ensuite, il faut savoir que les performances en lecture et écriture ne sont pas forcément les mêmes. Au final, cela donne cinq possibilités :

IOPS total Nombre de commandes par seconde
Random read IOPS Nombre de commandes de lecture par seconde en accès aléatoire
Random write IOPS Nombre de commandes d'écriture par seconde en accès aléatoire
Sequential read IOPS Nombre de commandes de lecture par seconde en accès séquentiel
Sequential write IOPS Nombre de commandes d'écriture par seconde en accès séquentiel

Le tampon de commande et les optimisations associées modifier

Augmenter la taille des blocs augmente le débit, toutes choses égales par ailleurs, mais cette technique est rarement utilisée. La taille des blocs influence non seulement le débit, mais aussi le gâchis d'espace par fragmentation interne et quelques autres paramètres. Difficile d'augmenter la taille des blocs, celle-ci étant un compromis entre de nombreux paramètres. À la place, la majorité des périphériques incorporent diverses optimisations pour augmenter l'IOPS, ce qui permet un meilleur débit pour une taille de bloc identique.

Si on met de côté les optimisations liées au disk buffer, dont nous avons parlé plus haut, la plus importante est celle dite du tampon de commande. Sans lui, la mémoire de masse exécute chaque commande de lecture/écriture l'une après l'autre. Elle doit attendre qu'une commande soit terminée avant d'en envoyer une autre. Dans ces conditions, le support de mémorisation est sous-utilisé. En effet, l’exécution d'une commande demande une phase de transmission de la commande, son interprétation par le contrôleur de mémoire de masse, l'accès au support de mémorisation, puis une phase d'ACK ou de transfert DMA. Durant les phases d'accès au bus et d’interprétation, le support de mémorisation est inutilisé, alors qu'on pourrait y accéder.

Avec le tampon de commande, la mémoire de masse est capable de recevoir des commandes même si les précédentes ne sont pas terminées. Ainsi, quand une commande a terminé d'accéder au support de mémorisation, la suivante est déjà prête et peut accéder immédiatement au support de mémorisation. Pas besoin d'attendre la transmission et l'interprétation de la commande suivante : elle est déjà là. Évidemment, les commandes anticipées sont mises en attente dans une mémoire tampon et traitées quand le support de mémorisation est libre. La mémoire tampon qui met en attente les commandes est appelée le tampon de commandes.

Tampon de commande d'une mémoire de masse

Avec cette optimisation, le nombre d'IOPS augmente, mais le temps d'accès fait de même. Le temps d'accès augmente car le temps d'attente dans le tampon de commande est techniquement compté dans le temps d'accès. Le temps d'accès total augmente donc, au prix d'un débit plus élevé. Plus la file d'attente sera longue (peut mettre en attente un grand nombre de requêtes), plus le débit sera important, de même que le temps d'accès. Il est possible de formaliser l'amélioration induite par ce tampon de requêtes sur le débit et le temps d'accès avec quelques principes mathématiques relativement simples.


Fonctionnement d'un ordinateur/Les disques durs Fonctionnement d'un ordinateur/Les solid-state drives Les mémoires vues précédemment étaient des mémoires fabriquées à base de semi-conducteurs ou de matériaux magnétiques. Les mémoires à semi-conducteurs sont généralement des mémoires assez rapides, peu d'entre elles servant de mémoires de masse. À l'inverse, les mémoires magnétiques sont systématiquement des mémoires de masse. Mais il existe un second type de mémoires de masse, basées sur des supports dits optiques. Les CD-ROM, DVD et autres Blue-Ray font partie de cette catégorie. Nous allons les étudier dans ce chapitre.

Le support de mémorisation modifier

Couches d'un disque optique. La couche A est la couche de polycarbonate, la seconde est la couche réfléchissante, la troisième est la couche de vernis, et la quatrième la couche d'illustration.

Un disque optique est composé d'un plateau circulaire, composé de plastique (du polycarbonate), qui fait environ 1,2 millimètre d’épaisseur. Ce plateau est recouverte d’une fine couche d’aluminium réfléchissant, qui lui donne son aspect luisant, iridescent. C'est cette couche qui sert de support de stockage proprement dit. Enfin, le tout est recouvert d'une couche de vernis, afin de prévenir le CD de l'oxydation. Parfois, une quatrième couche est ajoutée, afin de placer un dessin sur le disque optique (pensez à vos CD musicaux).

Vu du dessous un disque optique est composé de plusieurs parties. La partie centrale est une portion plastique transparente qui ne contient pas de données. Elle sert juste à s'accrocher au lecteur de CD/DVD. La portion réfléchissante est celle qui contient les données proprement dite. Elle est aussi appelée la portion programmée. Elle est encerclée par deux bande circulaires en plastique, qui contiennent des données spécifiques : des informations sur le support pour la bande intérieure, et des données spéciales pour la bande extérieure (du silence pour un CD audio). Les tailles de chaque bande, de la portion programmée et de la partie centrale sont standardisées.

Illustration des différentes portions d'un CD-ROM.

Pistes, sillons et bits modifier

Le disque est organisé en pistes circulaires, chaque piste comprenant un grand nombre de bits consécutifs. Ces pistes sont délimitées par des sillons, qui permettent à la tête de lecture de se positionner sur une piste.

Surface d'un disque optique (ici, un DVD), vierge de toute donnée. On voit que les pistes sont séparées par des sillons relativement profonds.
Comparaison de la taille des creux entre CD et DVD.

L'écriture des données va créer des irrégularités à la surface du disque plastique. Précisément, les bits sont stockés sous la forme de creux sur la surface du disque. Suivant le support, le codage des bits peut être relativement différent. Les anciens disques optiques utilisaient un codage simple : un creux code un 1, tandis qu'une absence de creux code un zéro. Mais sur les disques récents, c'est la transition d'un creux vers une bosse qui code un 1, l'absence de transition codant un zéro. Ces creux sont creusés dans la surface réfléchissantes : ils n'atteignent pas le disque de polycarbonate. Ils sont placés les uns à côté des autres, d'une manière similaire à celle utilisée sur les disques durs, sur une même piste.

La taille des creux varie suivant que l'on a affaire à un CD-ROM, un DVD-ROM, un Blue-Ray, ou tout autre type de disque optique. Plus ces creux sont petits, plus on peut en mettre à surface équivalente. Cela permet donc de coder plus de données pour une surface égale. Le passage du CD au DVD, puis au BR s'est accompagné d'une réduction de la taille des creux, ce qui s'est traduit par une augmentation de la capacité mémoire des disques optiques.

Le lecteur de disques optiques modifier

Lentille d'un lecteur CD.

La méthode de lecture des disques optiques est relativement similaire à celle utilisée sur les disques durs. Le disque optique est balayé par une tête de lecture, qui parcours les pistes du disque optique. Lorsqu'on place le disque optique dans son lecteur, celui-ci entre en rotation sous l'influence d'un moteur. En tournant, le disque optique va être balayé par la tête de lecture, qui lira les bits d'une piste les uns après les autres.

Comme pour les disques durs, la tête de lecture est placée au bout d'un bras de lecture, qui lui permet de passer d'une piste à l'autre.

Le disque optique contient un détrompeur, qui lui permet de s'insérer dans les sillons qui délimitent les pistes, ainsi que de quoi lire le contenu de la piste.

Tête de lecture optique.

Le servomoteur modifier

Le disque optique est entraîné par un moteur, qui le fait tourner à une vitesse bien précise. Selon le lecteur, la vitesse de rotation peut varier : certains vont plus vite que d'autres. Les premiers lecteurs utilisaient une vitesse de rotation qui permettait d'obtenir un débit de 150 Ko/s, débit qui est devenu une sorte de norme à laquelle il est encore fait référence aujourd'hui. De nos jours, les lecteurs ont un débit qui est un multiple de débit de 150 Ko/s : on parle ainsi de lecteur 4x pour un lecteur qui a un débit de 4 fois 150 Ko/s. De nos jours, les lecteurs les plus rapides font jusqu’à 48 fois cette valeur, ce qui donne du 7200 Ko/S.

La tête de lecture modifier

La tête de lecture contient un laser, qui émet de la lumière en direction de la surface du disque optique. La lumière émise est réfléchie par le disque, sa fraction reflétée étant alors captée par un capteur intégré à la tête de lecture. Ce capteur est une cellule photoélectrique, un composant électronique qui transforme la présence ou absence de lumière en signal électrique. Suivant la présence d'un creux ou d'une bosse, la lumière ne sera pas réfléchie de la même manière. L’électronique du lecteur peut alors détecter la présence d'un creux ou d'une bosse selon la qualité de la lumière réfléchie.

Précisons que le capteur de lumière et le laser ne sont pas placés l'un à côté de l'autre, par manque de place. Si on plaçait le laser et le capteur l'un à côté de l'autre, il y aurait un décalage assez problématique à gérer. La lumière devrait être émise avec un angle pour rebondir en direction du capteur. Pour éviter ce genre de problème, les lecteurs actuels utilisent une ruse basée sur l'optique élémentaire. L'idée est d'utiliser un prisme droit, qui laisse passer la lumière tout droit lors du trajet aller, mais qui dévie la lumière lors du retour.

Pour résumer, voici comment se déroule la lecture d'un bit sur un disque optique. La lumière est émise par le laser, traverse le prisme en ligne droite, avant d'être concentrée par diverses lentilles, histoire d'obtenir un fin faisceau de lumière qui frappera le disque en un point. La lumière frappe la surface réfléchissante et est renvoyée vers la tête de lecture. Mais elle est alors déviée par le prisme, en direction du capteur de luminosité.

Fonctionnement d'une tête de lecture de lecteur CD-DVD-BR.

Lorsque la lumière est réfléchie par le disque, elle va interférer avec la lumière émise par le laser, causant ou non des interférences et de la diffraction. Lors du passage d'un creux à un plat (ou l'inverse), la lumière réfléchie sera minime. Par contre, la lumière réfléchie par une surface plane (hors creux ou bosse) sera maximale. On peut ainsi détecter le passage d'un creux à un plat en regardant l'intensité de la lumière émise. Vu que les creux n'ont pas la même taille entre un CD, un DVD ou un Blu-ray la lumière utilisée n'a pas la même longueur d'onde. Les CD-ROM sont lues avec une lumière grise, dont la taille est de 780 nm. Les DVD utilisent une longueur d'onde plus courte, de 650 nm, qui a une couleur rouge. Les Blu-ray utilisent quand à eux une lumière de 405 nm, tout comme les HD-DVD.

Comparaison CD, DVD, BD.

Voir aussi modifier

Pour ceux qui veulent aller plus loin, je conseille vivement la lecture des liens suivants :


Fonctionnement d'un ordinateur/Les mémoires historiques Les technologies RAID (Redundant Array of Inexpensives Disks) sont des technologies qui permettent d'utiliser plusieurs disques durs ou SSD afin de gagner en performances, en espace disque ou en fiabilité. Ces technologies sont apparues dans les années 70, sur des ordinateurs devant supporter des pannes de disque dur et sont toujours utilisées dans ce cas de figure à l'heure actuelle, notamment sur les serveurs dits à haute disponibilité. Il existe plusieurs types de RAID, qui ont des avantages et leurs inconvénients bien précis. Dans les grandes lignes, on peut classer les différentes techniques de RAID dans deux catégories : les RAID standards et les RAID combinés.

Ces techniques peuvent être prises en charge aussi bien par le logiciel que par le matériel. Dans le premier cas, c'est le système d'exploitation (et plus précisément le système de fichiers) qui se charge de la gestion des disques durs et de la répartition des données sur ceux-ci. Dans l'autre cas, le RAID est pris en charge par un contrôleur spécialisé, intégré à la carte mère ou présent sous la forme d'une carte d'extension. La dernière méthode est évidemment plus rapide, les calculs étant déportés sur une carte spécialisée au lieu d'être pris en charge par le processeur.

Les technologies RAID standards (RAID 0 à 6) modifier

Les techniques de RAID dites standards ont toutes étés inventées en premier. Il s'agit des techniques de JBOD, RAID 0, RAID 1, RAID2, RAID3, RAID 4, RAID5 et RAID6. Les trois premières sont les plus simples, dans le sens où elles ne font pas appel à des bits de parité.

Le JBOD (Just a Bunch Of Disks) modifier

La technologie JBOD permet de regrouper plusieurs disques durs en une seule partition. JBOD est l'acronyme de l'expression Just a Bunch Of Disks. Avec cette technique, on attend qu'un disque dur soit rempli pour commencer à entamer le suivant. Les capacités totales des disques durs s'additionnent. Il est parfaitement possible d'utiliser des disques durs de taille différentes.

JBOD.

Le RAID 0 et le RAID 1 modifier

Avec le RAID 0, des données consécutives sont réparties sur des disques durs différents. Là encore, le système RAID contient plusieurs disques durs, mais est reconnu comme une seule et unique partition par le système d'exploitation. La capacité totale du RAID 0 est égale à la somme des capacités de chaque disque dur, comme avec le JBOD. La différence avec le JBOD tient dans le fait que les données d'un même fichier étant systématiquement réparties sur plusieurs HDDs, ce que ne faisait pas le JBOD. Cela permet une amélioration des performances lors de l'accès à des données consécutives, celles-ci étant lues/écrites depuis plusieurs disques durs en parallèle.

Avec le RAID 1, les données sont copiées à l'identique sur tous les disques durs. Chaque disque dur est ainsi une copie de tous les autres, chaque disque dur ayant exactement le même contenu. L'avantage de cette technique réside dans la résistance aux pannes : cela permet de résister à une panne qui touche un grand nombre de disque dur, tant qu'au moins un disque dur est épargné. Par contre, cette technique ne permet pas de gagner en espace disque : l'ensemble des disques durs est vu comme un unique disque de même capacité qu'un disque individuel. Des gains en performances sont possibles, vu que des données consécutives peuvent être lues depuis plusieurs disques durs. Mais cette optimisation entraine une baisse des performances en écriture, ce qui fait que peu de contrôleurs RAID l'utilisent.

RAID 0.
RAID 1.

Le RAID classique avec bit de parité modifier

Les RAID 2, 3, 4, 5 et 6 font appel à des bits de parité. Cela leur permet d'obtenir une résilience aux pannes plus attractive que le RAID1. Ces techniques ont tendance à utiliser un seul HDD pour stocker des données redondantes, soit nettement moins que le RAID1. En contrepartie, la récupération suite à une panne est plus lente, vu qu'il faut reconstituer les données originelles via un calcul qui fait intervenir les données de tous les disques durs, ainsi que les données de parité.

Les RAID 2, 3 et 4 peuvent être vu comme une sorte de RAID 0 amélioré. Il est amélioré dans le sens où on ajoute un disque pour les données de parité à un RAID 0. L'idée est simplement de calculer, pour tous les secteurs ayant la même adresse, un secteur de parité. Les octets des différents disques du RAID 0 seront utilisés pour calculer un octet de parité, qui sera enregistré sur un disque de parité à part. Il faut noter que certaines formes améliorées du RAID 4 dupliquent les disques de parité, ce qui permet de résister à plus d'une panne de disque dur (autant qu'il y a de disques de parité). Mais ces améliorations ne font pas partie des niveaux de RAID standard.

RAID 3.
RAID 4.

Le RAID 5 est similaire au RAID 4, sauf que les secteurs de parité sont répartis sur les différents disques dur, afin de gagner en performances.

Le RAID 6 est une amélioration du RAID 5 où les données de parité sont elles-mêmes dupliquées en plusieurs exemplaires. Cela permet de résister à la défaillance de plus d'un HDD.

RAID 5.
RAID 6.
Type de RAID JBOD RAID 0 RAID 1 RAID 2, 3 et 4 RAID 5 RAID 6
Performances Pas d'amélioration Amélioration en lecture et écriture Amélioration en lecture, sous conditions Amélioration en lecture et écriture, inférieure au RAID 0.

Avantage au RAID 5 et 6

Espace disque Somme des capacités des HDD Pas d'amélioration Somme des capacités de tous les HDD, sauf un
Résilience aux pannes Pas d'amélioration Excellente : survit à autant de pannes qu'il y a de HDD, sauf un Minimale : survit à la panne d'un seul HDD Intermédiaire, dépend des données de parité

Les technologies de RAID combiné modifier

Il est possible de combiner des disques durs en utilisant diverses techniques de RAID.

Par exemple, on peut utiliser un RAID1 de disques en RAID 0 : on parle alors de RAID 01. Il est aussi possible d'utiliser un RAID 0 de disques en RAID 1 : on parle alors de RAID 10.

RAID 01.
RAID 10.

De même, utiliser un RAID 0 de RAID 3, 4, 5 ou 6 est possible : on parle respectivement de RAID 3+0, 4+0, 5+0, 6+0.

RAID 5+0.

On peut aussi faire un RAID de plusieurs disques en RAID 0, ce qui donne du RAID 0+1, 0+2, 0+3, 0+4, ...

RAID 0+3

, +, +,

La mémoire cache modifier

Fonctionnement d'un ordinateur/Les mémoires cache Fonctionnement d'un ordinateur/Le préchargement Fonctionnement d'un ordinateur/Le cache d'instructions Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer

Le parallélisme d’instructions modifier

Fonctionnement d'un ordinateur/Le pipeline

La gestion des dépendances par le pipeline modifier

Fonctionnement d'un ordinateur/Interruptions et pipeline Fonctionnement d'un ordinateur/Dépendances de contrôle Fonctionnement d'un ordinateur/Dépendances de données Fonctionnement d'un ordinateur/Dépendances structurelles

L'éxécution dans le désordre modifier

Fonctionnement d'un ordinateur/Exécution dans le désordre Fonctionnement d'un ordinateur/Fenêtres d’instruction et stations de réservation Fonctionnement d'un ordinateur/Le renommage de registres Fonctionnement d'un ordinateur/Désambigüisation de la mémoire Fonctionnement d'un ordinateur/Processeurs à émissions multiples

Le parallélisme d'instruction exposé au niveau du jeu d'instruction modifier

Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC Fonctionnement d'un ordinateur/Les architectures dataflow

Les architectures parallèles modifier

Fonctionnement d'un ordinateur/Les architectures parallèles

Le parallélisme de tâches modifier

Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading Fonctionnement d'un ordinateur/Architectures distribuées, NUMA et COMA

Le parallélisme de données modifier

Fonctionnement d'un ordinateur/Les architectures à parallélisme de données

Les architectures parallèles exotiques modifier

Fonctionnement d'un ordinateur/Les architectures parallèles exotiques

Annexes modifier

Fonctionnement d'un ordinateur/Le matériel réseau Fonctionnement d'un ordinateur/Les architectures découplées Fonctionnement d'un ordinateur/Les architectures tolérantes aux pannes Fonctionnement d'un ordinateur/Les architectures neuromorphiques Fonctionnement d'un ordinateur/Les circuits réversibles

Modèle:Autocat