Aller au contenu

Fonctionnement d'un ordinateur/Version imprimable 2

Un livre de Wikilivres.

Pour commencer, nous allons voir qu'il existe de nombreux types d'ordinateurs. Le plus connu est certainement le PC, l'ordinateur personnel, que vous avez sans doute dans votre salon. Les ordinateurs portables sont un deuxième type d'ordinateur assez intuitif, que vous avez peut-être. Mais il y a aussi d'autres types d'ordinateurs auxquels vous n'avez jamais été confrontés.

Les différents types d'ordinateurs

[modifier | modifier le wikicode]

Dans cette section, nous allons décrire rapidement les ordinateurs les plus courant, mais surtout voir ce qu'il y a à l'intérieur d'un ordinateur.

L'ordinateur de type PC fixe

[modifier | modifier le wikicode]

De l'extérieur, l'ordinateur est composé d'une unité centrale sur laquelle on branche des périphériques.

Exemples de périphériques, connectés à une unité centrale (ici appelée à tort operating system).

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. Voici ce à quoi ressemble l'intérieur de l'unité centrale. Trois sections sont visibles : l'alimentation électrique en haut à gauche, la carte mère en bas à gauche et en jaune/orange, les disques et lecteurs à droite des câbles. Et on voit aussi beaucoup de câbles.

Exemple d'unité centrale.

Tout en haut à gauche, se trouve l'alimentation électrique, qui convertit le courant de la prise électrique en un courant plus faible, utilisable par les autres composants. Elle émet de nombreux câbles assez gros, qui sont reliés au reste. Les PC actuels utilisent des alimentations électriques standardisées, qui fournissent trois tensions au reste de l'ordinateur : une tension de 12 Volts, une de 5 Volts, une de 3,3 Volts.

À droite, caché par la partie "métallique" du boitier, se trouvent les lecteurs de CD/DVD, et les disques durs. Les lecteurs CD-ROM ou DVD-ROM permettaient de lire des CD ou des DVD. Je parle au passé, car ils ont aujourd'hui disparu des ordinateurs modernes. Les disques durs sont des mémoires de stockage, qui mémorisent vos données de manière permanente. De nos jours, ils sont remplacés par des SSD, qui sont des disques durs électroniques, là où les disques durs proprement dit sont des mémoires magnétiques. Nous verrons la différence dans deux chapitres dédiés, un sur les disques durs, l'autre sur les SSD.

En bas à droite, se trouve la carte mère un support plat, en plastique ou en céramique, sur laquelle sont soudés/connectés les autres composants. Sur celle-ci, de nombreux composants sont soudés, d'autres sont branchés dessus avec des câbles, d'autres sont connectés dessus avec des connecteurs. 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 électronique, dans des creux du plastique. Ils portent le nom de pistes.

Avant, les disques durs, SSD et lecteurs de CD/DVD sont branchés dessus via des connecteurs dédiés, appelés des connecteurs S-ATA. De nos jours, ils sont branchés sur un autre connecteur, grâce à l'interface NVME. Toujours est-il qu'ils sont reliés à la carte mère par des câbles, ce qui montre que ce sont des sortes de périphériques interne. Ils sont placés dans le boitier, mais on aurait tout aussi bien pu les mettre à l'extérieur, vu qu'ils communiquent avec l'ordinateur par l'intermédiaire d'un câble ou d'un connecteur dédié.

Mais deux autres composants très importants sont eux placés sur la carte mère, directement. Ils ont chacun un connecteur dédié, sur lequel ils sont branchés directement, sans passer par l'intermédiaire d'un câble, mais sans être soudés non plus. Il s'agit du processeur et de la mémoire RAM, deux composants centraux des PC modernes.

Processeur Pentium III.

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é. Il est souvent caché sous un radiateur, lui-même surmonté par un ventilateur. En effet, un processeur chauffe beaucoup, ce qui demande d'évacuer cette chaleur. Un chapitre entier expliquera pourquoi les processeurs chauffent, ainsi que les techniques utilisées pour limiter leur production de chaleur et leur consommation électrique.

La mémoire vive conserve des informations/données temporairement, tant que le processeur en a besoin. Elle prend la forme de barrettes de RAM, comme montré ci-dessous.

Barrette de RAM.

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. Il s'agit des 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.

É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.

Si on regarde une carte mère de face, on voit un grand nombre de connecteurs, mais aussi des circuits électroniques soudés à la carte mère.

Architecture matérielle d'une carte mère

Les connecteurs sont là où on branche les périphériques, la carte 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.

Les ordinateurs portables

[modifier | modifier le wikicode]
Ordinateur portable Samsung QX-511 (2), pour illustration.

Un ordinateur portable est identique à un ordinateur de type PC. Si vous ouvrez votre ordinateur portable (attention à la garantie), vous retrouverez les mêmes composants qu'un PC à l'intérieur. Un point important est la présence d'une batterie à l'intérieur du PC portable, absente des PC fixes.

LG gram 14Z90Q avec sa batterie.

Une fois la batterie et les circuits associés retiré, la seule différence notable est que tous les composants sont soudés sur la carte mère, au lieu d'être branchés sur des connecteurs, à part éventuellement la RAM et le disque dur.

LG gram 14Z90Q sans sa batterie.

La RAM est parfois soudée sur la carte mère, d'autres fois sous la forme de barrettes de mémoire vive semblables à celles des PC. Dans le dernier cas, elle est connectée à des connecteurs spécifiques, ce qui permet de les changer/upgrader. Les PC portables sont parfois construits de manière à pouvoir changer la RAM facilement, en ouvrant un cache spécial, concu pour.

Lenovo G555.

La conception d'un ordinateur portable est légèrement différente de celle d'un PC, pour des raisons thermiques. Un PC fixe a plus de place pour évacuer sa chaleur. Les ventilateurs peuvent plus facilement déplacer l'air dans le boitier, même si le flux d'air dans le boitier est éventuellement gêné par la carte graphique. Mais l'évacuation de la chaleur est assez efficace. Sur les PC portables, leur finesse fait que le trajet de l'air est beaucoup plus contraint. Évacuer la chaleur produite par l'ordinateur est plus complexe, ce qui demande d'utiliser des systèmes de refroidissement et de ventilation spécifiques.

Là où les PC fixes se débrouillent avec des radiateurs et des ventilateurs posés sur le processeur, les PC portables font autrement. Ils ne peuvent pas forcément mettre le ventilateur sur le processeur. A la place, ils mettent le ventilateur à côté, mais le processeur est surmonté par un mécanisme de transmission de la chaleur, qui transfère la chaleur du processeur vers le ventilateur.

Comment fonctionne le système de ventilation d'un ordinateur portable, en anglais.

Les difficultés pour dissiper la chaleur font que les ordinateurs portables utilisent souvent des composants peu performants, mais qui chauffent peu. En effet, nous verrons que plus un composant est puissant, plus il chauffe. La relation n'est pas parfaite, mais les processeurs haute performance chauffent plus que les modèles d'entrée ou milieu de gamme moins puissants. En conséquence, les PC portables sont souvent moins puissants que les PC fixes, même pour les modèles dit gaming.

Les serveurs et mainframes

[modifier | modifier le wikicode]

Les grandes entreprises utilisent des ordinateurs de grande taille, très puissants, appelés des mainframes. Ils sont souvent confondus avec les serveurs par le grand public, il y a une différence entre les deux. Un serveur est une fonction logicielle/réseau, pas un type d'ordinateur. Mais laissons cette différence de côté, ce qui est dit pour les mainframes sert pour les gros serveurs haute performance.

Exemple de mainframe' de type IBM Z15.

Les anciens mainframes des années 50-80 avaient une taille pouvant être impressionnante, au point de prendre une pièce complète d'un bâtiment. Mais de nos jours, les mainframes tiennent dans une grosse armoire un peu haute. Les mainframes doivent rester allumé en permanence et ne doivent pas être éteint, sauf éventuellement quelques jours par an pour des opérations de maintenance. Ils utilisent pour cela des techniques spécifiques, avec beaucoup de redondance interne.

Mainframe ACONIT.

Les mainframes sont aussi conçus pour mutualiser au maximum leur utilisation, ils peuvent être utilisés par plusieurs utilisateurs à la suite, voire en même temps. Pour cela, les anciens mainframes étaient capables de faire tourner plusieurs applications distinctes l'une à la suite de l'autre, avec des commutations fréquentes entre logiciels. De nos jours, ils sont capables d'exécuter un grand nombre de tâches en même temps, grâce à la présence de plusieurs processeurs. Les mainframes modernes sont même capables de faire tourner plusieurs systèmes d'exploitation en même temps si besoin.

Les anciens mainframes étaient des ordinateurs assez simples, avec un processeur, de la mémoire RAM, un ou plusieurs disques durs, pas plus. Le mainframe était tellement énorme et cher que les entreprises n'en avaient qu'un seul pour toute l'entreprise. Les employés avaient sur leur bureau des terminaux, qui permettaient d'accéder à l'ordinateur central, mais n'étaient pas des ordinateurs. Les terminaux étaient des composants électroniques simples, avec un écran, un clavier et une souris, mais sans processeur. Ils envoyaient des données au mainframe, ils en recevaient de sa part, mais tout traitement était réalisé sur le mainframe. L'arrivée sur le marché des ordinateurs personnels, de type PC fixe/portable, a entrainé l'abandon de ce genre de pratique, tous les employés ont maintenant un ordinateur rien que pour eux.

Exemples de terminaux
Exemple de terminal. Notez la présence d'un écran et d'un clavier, mais l'asbence d'une unité centrale.
Autre exemple de terminal. Le clavier est couplé à un stylo CRT, qui remplacait la souris et agissait sur l'écran comme s'il était tactile.
Intérieur d'un terminal
Terminal CT1024, sans le boitier extérieur.
Terminal CT1024, description des composants interne.

De nos jours, les mainframes contiennent en réalité plusieurs ordinateurs simples interconnectés via un réseau local. Un mainframe qui tient dans une armoire contient facilement une dizaine ou centaine de processeurs, plein de RAM, des disques durs séparés dans une armoire distincte, etc. Vu qu'ils communiquent uniquement via le réseau, ils n'ont pas d'interface, pas d'écran, 'entrée via clavier/souris. Le tout est commandé avec une console de commande, typiquement un ordinateur portable ou un terminal assez simple séparé de l'armoire, qui permet à un administrateur de faire des opérations de surveillance, configuration et maintenance. Il peut y avoir une console de commande pour plusieurs armoires séparées, la console peut même être dans un autre bâtiment et accéder au mainframe par le réseau.

Mainframe de type IBM System Z9. L'ordinateur portable sert de console pour qu'un administrateur fasse des opérations de surveillance, configuration et maintenance.

Les processeurs et la RAM sont typiquement installés sur des cartes amovibles, qui sont connectées au fond de l'armoire lors de l'installation. Il est même possible de retirer ou d'ajouter des cartes en fonctionnement, afin d'ajouter/retirer des processeurs, des disques durs, etc. Ce qui est très utile si un composant est en panne et qu'il faut le remplacer.

IBM TotalStorage Exp400

Un peu d'abstraction : qu'est-ce qu'un ordinateur ?

[modifier | modifier le wikicode]

Un ordinateur comprend donc un processeur, plusieurs mémoires, des cartes d'extension et une carte mère pour connecter le tout. Mais il s'agit là d'une description assez terre-à-terre de ce qu'est un ordinateur. Une autre description, plus abstraite, se base sur le fait qu'un ordinateur est une énorme calculatrice programmable. Elle permet d'expliquer pourquoi il y a une distinction entre processeur et mémoire, entre unité centrale et périphériques. Tout ce qui va suivre pourra vous paraitre assez abstrait, mais rassurez-vous : les prochains chapitres rendront tout cela plus concrets.

La séparation entre entrées-sorties et traitement

[modifier | modifier le wikicode]

L'unité centrale peut être vue comme une énorme calculatrice ultra-puissante, qui exécute des commandes/opérations. Mais à elle seule, elle ne servirait à rien, il faut interagir avec par l'intermédiaire de plusieurs périphériques. Il existe deux types de périphériques, qui sont conceptuellement différents. Les périphériques comme le clavier ou la souris permettent d'envoyer des informations à l'ordinateur, d'agir sur celui-ci. A l'inverse, les écrans transmettent des informations dans le sens inverse : de l'ordinateur vers l'utilisateur. Les premiers sont appelés des entrées, les seconds des sorties.

Dans un ordinateur, les informations sont représentées sous la forme de nombres. Toute donnée dans un ordinateur est codée avec un ou plusieurs nombres regroupés dans une donnée, un fichier, ou autre. Et la transmissions avec les entrées et sorties se fait là-encore avec des nombres. Par exemple, quand vous appuyez sur votre clavier, le clavier envoie un numéro de touche à l'unité centrale, qui indique quelle touche a été appuyée. L'image à afficher à l'écran est codée sous la forme d'une suite de nombres (un par pixel). L'image est envoyée à l'écran, qui traduit la suite de nombre en image à afficher. Les entrées traduisent des actions utilisateurs en nombres, on dit que les entrées encodent les actions utilisateur. Les sorties font la traduction inverse, elles transforment des suites de nombres en une action physique. On dit qu'elles décodent des informations.

Pour résumer, toute appareil électronique est composé par :

  • Des entrées sur lesquelles l'utilisateur agit sur l'ordinateur. Les entrées transforment les actions de l'utilisateur en nombres, qui sont interprétés par l'ordinateur.
  • Une unité de traitement, qui manipule des nombres et fait des calculs/opérations dessus. Les nombres proviennent des entrées, du disque dur, ou d'autres sources, peu importe.
  • Des sorties, qui va récupèrent le résultat calculé par l'unité de traitement pour en faire quelque chose : écrire sur une imprimante ou sur un moniteur, émettre du son,...
Ordinateur théorique Simple 1

L'unité de traitement proprement dite, celle qui fait les calculs, est couplée à une mémoire qui mémorise les opérandes et résultats des calculs. La séparation entre processeur et mémoire est nécessaire pour qu'un appareil électronique soit qualifié d'ordinateur. De nombreux appareils n'ont pas de séparation entre unité de traitement et mémoire, comme certaines vielles radios AM/FM.

Schéma de principe d'un ordinateur

Un ordinateur est un appareil programmable

[modifier | modifier le wikicode]

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.

Un programme est une suite de d'instructions machines, chaque instruction effectuant une action dans l'ordinateur. Sur les ordinateurs modernes, les instructions les plus communes effectuent une opération arithmétique : une addition, une multiplication, une soustraction, etc. Les ordinateurs modernes sont donc de grosses calculettes très puissantes, capables d'effectuer des millions d'opérations par secondes.

Et qui dit opération dit nombres : un ordinateur gère nativement des nombres, qui sont codés en binaire sur la quasi-totalité des ordinateurs modernes. Les ordinateurs modernes sont capables de faire des calculs sur des nombres entiers, mais aussi des nombres à virgule. Il existe plusieurs manières de représenter des nombres en binaire, certaines représentant des nombres entiers dits naturels (0 et plus), d'autres des nombres signés (positifs ou négatifs), d'autres des nombres à virgule. Un chapitre entier détaillera comment sont encodés ces nombres.

Une instruction est représentée dans un ordinateur par une série de nombres entiers : un nombre qui indique quelle opération/commande effectuer et des autres nombres pour coder les données (ou de quoi les retrouver dans l'ordinateur). L'ordinateur récupére les commandes une par une, les traduit en opération à effectuer, exécuter l'opération, et enregistre le résultat. Puis il recommence avec la commande suivante.

Ordinateur Théorique Complexe. En jaune, le programme informatique, la liste de commande. Le rectangle gris est l'ordinateur proprement dit.

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 un processeur séparé de la mémoire. De plus, il doit utiliser un codage numérique, chose que nous allons aborder dans le chapitre suivant.

Les ordinateurs à programme mémorisé ou programme externe

[modifier | modifier le wikicode]
Carte Perforée.

Au tout début de l'informatique, les programmes étaient enregistrés sur des cartes perforées, à savoir des cartes en papier/plastique percées par endroits. Un 1 est codé par un trou dans le papier et un 0 par une absence de trou. Les programmes étaient exécutés par l'ordinateur quand on insérait la carte perforée dans le lecteur et qu'on appuyait sur un bouton.

Par la suite, les premiers ordinateurs grand public, comme les Amstrad ou les Commodore, utilisaient des cassettes audio magnétiques pour stocker les programmes. Dans le même style, les consoles de jeu utilisaient autrefois des cartouches de jeu, qui contenaient le programme du jeu à exécuter. Pour résumer, les anciens ordinateurs mémorisaient les programmes sur un support externe, lu par un périphérique. De tels ordinateurs sont dits à programme externe.

D’anciens ordinateurs personnels, comme l’Amstrad ou le Commodore, permettaient de taper les programmes à exécuter à la main, au clavier, avant d’appuyer une touche pour les exécuter. Nul besoin de vous dire que les cassettes audio étaient plus pratiques.

Mais de nos jours, les programmes sont enregistrés sur le disque dur de l'ordinateur ou dans une mémoire intégrée à l'ordinateur. On dit qu'ils sont installés, ce qui est un mot bien compliqué pour dire que le programme est enregistré sur le disque dur de l’ordinateur. A défaut de disque dur, le programme/logiciel est enregistré dans une mémoire spécialisée pour le stockage. Par exemple, sur les cartes électroniques grand public de marque Arduino, les programmes sont envoyés à la carte via le port USB, mais les programmes sont enregistrés dans la carte Arduino, dans une mémoire FLASH dédiée. Disque dur ou non, le programme est mémorisé dans la mémoire de l'ordinateur. Il est possible d'installer ou de désinstaller les programmes en modifiant le contenu de la mémoire. Le terme utilisé est alors celui de programme stocké en mémoire.

Les programmes à installer sont disponibles soit sur un périphérique, comme un DVD ou une clé USB, soit sont téléchargés depuis internet. Les premiers PC fournissaient les logiciels sur des disquettes, qui contenaient un programme d'installation pour enregistrer le programme sur le disque dur de l'ordinateur. Par la suite, le support des logiciels a migré vers les CD et DVD, les logiciels devenant de plus en plus gros. De nos jours, la majorité des applications sont téléchargées depuis le net, l'usage de périphériques est devenu obsolète, même les consoles de jeu abandonnent cette méthode de distribution.

Les générations d'ordinateurs : un historique rapide

[modifier | modifier le wikicode]

L'informatique a beaucoup évolué dans le temps. Et une bonne partie de cette évolution tient à l'évolution de l’électronique sous-jacente. Les ordinateurs actuels sont fabriqués avec des transistors, des petits composants électroniques qui servent d'interrupteurs programmables, qu'on détaillera dans ce qui suit. Mais ça n'a pas toujours été le cas. Et cela permet de distinguer plusieurs générations d'ordinateurs.

Les ordinateurs électro-mécaniques : les premiers ordinateurs

[modifier | modifier le wikicode]

Le tout premier ordinateur est le Z1, un modèle unique inventé par Konrad Suze entre 1936 et 1938. Son processeur gérait non pas des nombres entiers, mais des nombres à virgule (des nombres dits flottants, pour être précis), encodés sur 22 bits. Le processeur du Z1 était capable de faire des additions et des soustractions, car il intégrait un circuit additionneur-soustracteur basique. Des circuits annexes permettaient de faire des multiplications et des divisions en enchainant des additions/soustractions successives. Il fonctionnait à une fréquence de 1 Hz, ce qui fait qu'il pouvait faire une addition/soustraction par seconde maximum.

Niveau mémoire, la RAM de l'ordinateur était très limitée : seulement 16 nombres de 22 bits chacun. S'il s'agissait d'une mémoire électronique, les programmes étaient mémorisés sur une mémoire mécanique et précisémment sur des cartes perforées, à savoir des cartes en papier/plastique dans lesquelles un 1 est codé par un trou dans le papier et un 0 par une absence de trou. Les programmes étaient exécutés par l'ordinateur quand on insérait la carte perforée dans le lecteur et qu'on appuyait sur un bouton.

Réplique du Z1 au German Museum of Technology de Berlin, 2017.

Après sa destruction dans un bombardement, le Z1 a été reconstruit et amélioré, ce qui donné les deux ordinateurs Z2 et Z3. Quelques années plus tard, quelques laboratoires de recherche ont fabriqués des ordinateurs dans leur coin, comme le Harvard Mark I et le Colossus Mark 1 (UK) qui ont été mis en service en 1944. Ils ont servi pour les cryptanalystes pour casser les codes utilisés par les Allemands pour leurs transmissions, pendant la seconde guerre mondiale.

Les ordinateurs cités dans le précédent paragraphe étaient tous des ordinateurs dit électro-mécaniques, car ils mélangeaient des composants électroniques et des composants mécaniques (tout ce qui a trait aux cartes perforées, notamment). Les composants électroniques étaient généralement des relais, à savoir des interrupteurs commandables électriquement, qui sont plus ou moins équivalents aux transistors modernes. La majeure partie des ordinateurs des années 40 étaient de ce type, mais leur nombre n'a pas dépassé 30. Il faut dire qu'ils ont rapidement laissé la place à des ordinateurs purement électroniques, que nous allons voir dans ce qui suit.

La première génération : les tubes à vide

[modifier | modifier le wikicode]

Les ordinateurs qui ont suivi les ordinateurs électromécaniques sont regroupés dans la première génération d'ordinateur. Ils étaient conçus avec des tubes à vide, les ancêtres des transistors. Les tubes à vide sont les ancêtres des transistors. Ils fonctionnent eux aussi comme des interrupteurs commandés en tension, ou comme amplificateur. Mais les comparaisons s'arrêtent là. Les tubes à vides n'étaient pas conçu avec des semi-conducteurs, mais étaient en réalité des ampoules à filament améliorées.

Les tubes à vide avaient beaucoup de défauts. Ils étaient assez encombrants, dans le sens où une ampoule prend beaucoup de place. Et il est difficile de miniaturiser une ampoule, personne n'imagine une ampoule de quelques microns de côté, alors que les transistors actuels sont bien plus petits que ça. De plus, les tubes à vide consommaient beaucoup de courant et chauffaient. Faire chauffer un filament d'ampoule demande un courant important et une bonne partie de ce courant part en chaleur. De plus, ils étaient assez peu fiables et tombaient en panne assez souvent. Et s'ils étaient assez simples à fabriquer, ils restaient malgré tout chers.

Tubes à vides.

Les tubes à vides étaient utilisés pour le processeur et quelques circuits annexes, mais les mémoires utilisaient d'autres technologies. Les mémoires de l'époque n'étaient pas du tout des mémoires électroniques. Il existait déjà des ancêtres des disques durs, à savoir des mémoires magnétiques appelées tambours magnétiques, bandes magnétiques, mémoires à tore de ferrite, etc. Les mémoires RAM, quant à elles, utilisaient des technologies totalement abandonnées : les mémoires à lignes de délai étaient des mémoires acoustiques, les tubes Williams étaient des écrans CRT modifiés, etc. Un chapitre entier sera dédié aux technologies des mémoires de l'époque.

L'usage de tubes à vide les place entre les premiers ordinateurs électro-mécaniques et les ordinateurs utilisant des transistors. Au vu de la taille des tubes à vide, les ordinateurs de l'époque étaient composés de plusieurs milliers de ces tubes, guère plus. Et malgré cela, un ordinateur prenait facilement un étage de bâtiment entier, ou au moins plusieurs pièces. La conséquence est que les ordinateurs de première génération étaient très simples, rudimentaires quand on les compare aux ordinateurs modernes. Ils ne géraient que quelques opérations simples : l'addition, la soustraction, pas grand-chose de plus. À quelques exceptions près, ils ne géraient pas la multiplication et encore moins la division, cela aurait demandé trop de tubes à vides.

Une des toutes premières machines construite avec des tubes à vide était l'Atanasoff–Berry. Ce n'était pas un ordinateur, car elle ne pouvait pas exécuter de programme sans intervention humaine pour séquencer des opérations, mais c'était la première machine de calcul purement électronique, sans aucune pièce mécanique. Elle utilisait une mémoire RAM construite avec des condensateurs, ceux-ci étant placés sur des cylindres tournants. Les circuits de calcul étaient construits avec des tubes à vide et faisaient leur travail un bit à la fois. Elle fonctionnait à une fréquence de 60 Hz et pouvait faire 30 additions/soustractions par seconde. Les nombres étaient encodés sur 50 bits.

Atanasoff–Berry.

L'arrivée de l'ENIAC en 1945 a changé la donne. Cet ordinateur a introduit plusieurs innovations majeures. La première est assez technique, il s'agit de la turing-complétude, elle a trait aux programmes qui peuvent être écrits pour un ordinateur, nous ne la détaillerons pas ici.

Architecture de la Manchester Baby.

En 1948, le Manchester Baby a introduit une innovation extrêmement importante : le programme n'était plus mémorisé sur des cartes perforées, mais dans une mémoire électronique à l’intérieur de l'ordinateur lui-même. En clair, le Manchester Baby était le premier ordinateur à programme stocké en mémoire en français. Au-delà de ça, le Manchester Baby restait quand même très simple, c'était limite un prototype.

Niveau interface, l'ordinateur avait une console avec 32 boutons, des interrupteurs et un écran de sortie sur lequel afficher les résultats. À l'intérieur, il y avait un processeur et une RAM de 32 nombres de 32 bits, fabriquées avec des tubes Williams (abordés dans une annexe à la fin du cours). Une instruction prenant autant de place qu'un nombre en RAM, ce qui limitait les programmes à 32 instructions maximum. Le processeur gérait 8 instructions en tout, mais la seule instruction de calcul qu'il gérait était la soustraction. L'addition n'était pas supportée. Il pouvait cependant émuler l'addition à partir d'une soustraction et inversant l'opérande soustrait.

Le Manchester Mark 1 était une amélioration du Manchester Baby. Il utilisait 4500 tubes à vide. L'ordinateur gérait des nombres de 40 bits, mais les instructions étaient codées sur 20 bits. La RAM avait la même taille, à savoir 32 nombres, leur taille était simplement augmentée. Par contre, la RAM était complémentée par l’ancêtre du disque dur magnétique, à savoir un tambour magnétique, l’ancêtre du disque dur magnétique. Il contenait 40 pages, chacune ayant la même taille que la RAM. Contrairement à la Manchester Baby, son processeur gérait l'addition. Les multiplications étaient réalisées par des additions successives, mais le processeur intégrait des registres pour mémoriser les opérandes à multiplier.

Par la suite, les ordinateurs commerciaux ont vu le jour. Les ordinateurs précédents provenaient de projets de recherches, souvent publics. Mais les entreprises privées se sont mises à fabriquer des ordinateurs elle-mêmes, pour les vendre à d'autres entreprises. On peut notamment citer les ordinateurs comme le UNIVAC I, le Bull Gamma 3 ou les premiers ordinateurs IBM. La performance de ces ordinateurs était nettement plus importante que leurs prédécesseurs, avec des fréquences de l'ordre de la centaine de kilo-hertz. Par exemple, le Bull Gamma 3 allait à 281 kHz, l'IBM 650 allait à 50 Khz.

La seconde génération : le remplacement des tubes à vides par des transistors

[modifier | modifier le wikicode]
Cartes imprimées de l'IBM 704, avec des transistors et résistances dessus.

La seconde génération est celle des ordinateurs fabriqués avec des transistors isolés, reliés entre avec des fils électriques. Les transistors en question possèdent trois broches, des pattes métalliques sur lesquelles on connecte des fils électriques. 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.

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

Les ordinateurs de seconde génération avaient entre 1000 et 500 000 transistors. Les capacités des ordinateurs étaient donc nettement supérieures à celles des ordinateurs de première génération. Une part non négligeable des ordinateurs de seconde génération supportait l'opération de multiplication, absente sur les ordinateurs de première génération. Néanmoins, cela variait beaucoup selon les ordinateurs.

Un exemple assez hors du commun est celui du premier modèle de l'IBM 1620. Le tout premier modèle n'avait aucun circuit de calcul, il n'était même pas capable de faire une addition ou une multiplication. A la place, il utilisait une table d'addition et une table de multiplication en mémoire RAM. Les deux tables devaient être initialisées par le logiciel lors du démarrage de la machine. La table d'addition faisait une centaine de chiffres, la table de multiplication faisait le double. Le processeur de l'ordinateur gérait des instructions pour faire des additions ou des multiplications, mais ces opérations lisaient le résultat dans la table d'addition. Les calculs étaient faits chiffre par chiffre, avec un encodage partiellement décimal, partiellement binaire (des chiffres décimaux étaient encodés en binaire, nous en reparlerons quand nous parlerons du BCD) !

Mémoire à tore de ferrite.

Les transistors n'étaient utilisés que pour le processeur et les circuits annexes. La mémoire n'était pas faite en transistors, pas à cette époque. La mémoire était une mémoire aujourd'hui abandonnée, appelée une mémoire à tores de ferrite. Elle sera détaillée dans un chapitre ultérieur. Mais sachez pour le moment qu'il s'agit d'une mémoire au support magnétique, les données étant stockées sur un support magnétique.

Les transistors étaient au départ de la même taille que les tubes à vide, mais ils ont rapidement rétrécit. Ils prenaient encore beaucoup de place dans les 60-70, ce qui fait que les ordinateurs étaient assez gros. Concrètement, ils prenaient une pièce de bâtiment entière dans le meilleur des cas. C'était l'époque des gros mainframes, reliés à des terminaux, peu puissants comparé aux standards actuels. Seules les grandes entreprises et les grands instituts de recherche pouvaient se payer les services de ce genre d'ordinateurs. Seuls les professionnels avaient accès à ce genre d'équipement.

La troisième génération : le circuit intégré

[modifier | modifier le wikicode]
Exemple de circuit intégré.

La troisième génération est toujours fabriquée avec des transistors, mais dont la taille a été réduite. Avec l'évolution de la technologie, les transistors ont diminué en taille, de plus en plus. Ils sont devenus tellement petits qu'ils ont fini par être regroupés dans des circuits intégrés, des circuits regroupent plusieurs transistors sur la même puce de silicium. 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.

Lors de cette génération, 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 séparés, impossible de mettre un processeur dans un seul boitier. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés aux noms à coucher dehors, mais dont vous comprendrez ce qu'ils veulent dire dans les chapitres adéquats : l'Intel 3001 est un séquenceur, l'Intel 3002 regroupe unité de calcul et registres, le 3003 est un circuit d'anticipation de retenue complémentaire du 3002, 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.

Même si le processeur était en pièces détachées, utiliser une dizaine de circuits intégrés était nettement mieux que d'utiliser plusieurs milliers de transistors individuels. En conséquence, les ordinateurs ont vu leur taille grandement réduite, passant de pièces entières à une simple armoire, voire à quelques cartes intégrées superposées. De plus, le prix des circuits intégré était abordable, du moins comparé aux ordinateurs d'avant. Le prix des ordinateurs a alors grandement décru, permettant à des entreprises et administrations de taille modeste de s'équiper d'ordinateurs.

La quatrième génération : le microprocesseur

[modifier | modifier le wikicode]
Microprocesseur, ici un processeur MOS6502, de la quatrième génération.

Lors de la troisième génération, les circuits intégrés regroupaient plusieurs dizaines ou centaines de transistors, mais sont rapidement montés en gamme. De centaines de transistors, ils sont passés au millier de transistors, puis au million et maintenant au-delà du milliard. Les transistors étaient devenus tellement petits, tellement miniaturisés, qu'il était devenu possible de mettre un processeur entier dans un circuit intégré. C'est ainsi qu'est né le microprocesseur, à savoir un processeur qui tient tout entier dans un seul circuit imprimé. La quatrième génération d’ordinateur est celle des ordinateurs basés autour d'un microprocesseur.

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. 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. 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.

Les microprocesseurs permirent encore uen fois de réduire la taille des ordinateurs, qui pouvaient tenir dans un boitier de PC, ou un boitier de console de jeu. Le prix des ordinateurs a aussi chuté en même temps que les transistors étaient miniaturisés. C'est cette invention qui permis l'invention des consoles de jeux et des mini-ordinateurs, à savoir les ordinateurs personnels. Sans miniaturisation, on n'aurait pas d'ordinateur personnel, pas de PC fixe ou portable.

L'organisation du cours : les différents niveaux d'explication

[modifier | modifier le wikicode]

Le fonctionnement d'un ordinateur est assez complexe à expliquer car les explications peuvent se faire sur plusieurs niveaux. Par plusieurs niveaux, on veut dire qu'un ordinateur est composé de composants très simples, qui sont assemblés pour donner des composants eux-même plus complexes, qui sont eux-même regrouper, etc. Étudier tout cela demande de voir plusieurs niveaux, allant de transistors très petits à des processeurs multicœurs. Les niveaux les plus bas sont de l'électronique pur et dure, alors que ceux plus haut sont à mi-chemin entre électronique et informatique.

Les niveaux d'abstraction en architecture des ordinateurs

[modifier | modifier le wikicode]

Les trois premiers niveaux sont de l'électronique pur et dure. Ils correspondent aux premiers chapitres du cours, qui porteront sur les circuits électroniques en général.

  • Le premier niveau est celui des transistors, des circuits intégrés, des wafer et autres circuits de très petite taille.
  • Le second niveau est celui des portes logiques, des circuits très basiques, très simples, à la base de tous les autres.
  • Le troisième niveau est celui dit de la Register Transfer Level, où un circuit électronique est construit à partir de circuits basiques, dont des registres et autres circuits dits combinatoires.

Les deux niveaux suivants sont de l'informatique proprement dit. C'est dans ces deux niveaux qu'on étudie les ordinateurs proprement dit, les circuits qu'il y a dedans et non l'électronique en général.

  • Le quatrième niveau est celui de la microarchitecture, qui étudie ce qu'il y a à l'intérieur d'un processeur, d'une mémoire, des périphériques et autres.
  • Le cinquième niveau est celui de l'architecture externe, qui décrit l'interface du processeur, de la mémoire, d'un périphérique ou autre. Par décrire l'interface, on veut dire : comment un programmeur voit le processeur et la mémoire, comment il peut les manipuler. Une architecture externe unique peut avoir plusieurs microarchitecture, ce qui fait qu'on sépare les deux. Tout cela sera plus clair quand on passera aux chapitres sur le processeur et les mémoires.

Nous n'allons pas voir les 5 niveaux dans l'ordre, des transistors vers l'architecture externe. En réalité, nous allons procéder autrement. La première partie du cours portera sur les trois premiers niveaux, le reste sur les deux autres. Les 5 niveaux seront vus dans des chapitres séparés, du moins le plus possible. Au niveau pédagogique, tout est plus simple si on scinde les 5 niveaux. Bien sûr, il y a quelques explications qui demandent de voir plusieurs niveaux à la fois. Par exemple, dans le chapitre sur les mémoires caches, nous auront des explications portant sur la RTL, la microarchitecture du cache et son architecture externe. Mais le gros du cours tentera de séparer le plus possible les 5 niveaux.

Les trois parties principales du cours

[modifier | modifier le wikicode]

Nous allons commencer par parler du binaire, avant de voir les portes logiques. Avec ces portes logiques, nous allons voir comment fabriquer des circuits basiques qui reviendront très souvent dans la suite du cours. Nous verrons les registres, les décodeurs, les additionneurs et plein d'autres circuits. Puis, nous reviendrons au niveau des transistors pour finir la première partie. La raison est que c'est plus simple de faire comme cela. Tout ce qui a trait aux transistors sert à expliquer comment fabriquer des portes logiques, et il faut expliquer les portes logiques pour voir le niveau de la RTL.

Une fois la première partie finie, nous allons voir les différents composants d'un ordinateur. Une première partie expliquera ce qu'il y a dans un ordinateur, quels sont ses composants. Nous y parlerons de l'architecture de base, de la hiérarchie mémoire, des tendances technologiques, et d'autres généralités qui serviront de base pour la suite. Puis, nous verrons dans l'ordre les bus électroniques, les mémoires RAM/ROM, le processeur, les périphériques, les mémoires de stockage (SSD et disques durs) et les mémoires caches. Pour chaque composant, nous allons voir leur architecture externe, avant de voir leur microarchitecture. La raison est que la microarchitecture ne peut se comprendre que quand on sait quelle architecture externe elle implémente.

Enfin, dans une troisième partie, nous allons voir les optimisations majeures présentes dans tous les ordinateurs modernes, avec une partie sur le pipeline et le parallélisme d'instruction, et une autre sur les architectures parallèles. Pour finir, les annexes de fin parlerons de sujets un peu à part.


Le codage des informations

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Caractères ASCII imprimables.

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.

ASCII-Table

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, des codages ASCII étendus ont rajouté des caractères à la table ASCII de base. 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 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.

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 | modifier le wikicode]

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 | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 ordinateurs 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. C'est le même principe que l'écriture des nombres en base décimale : le chiffre le plus significatif à gauche, et le moins significatif à droite (l'unité ou les décimales après la virgule).

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 | modifier le wikicode]

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 .

Valeur des chffres dans le système de numération binaire.

La traduction décimal→binaire

[modifier | modifier le wikicode]

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.

Quelques opérations en binaire

[modifier | modifier le wikicode]

Maintenant que l'on sait coder des nombres en binaire normal, il est utile de savoir comment faire quelques opérations usuelles en binaire. Nous utiliserons les acquis de cette section dans la suite du chapitre, bien que de manière assez marginale.

La première opération est assez spécifique au binaire. Il s'agit d'une opération qui inverse les bits d'un nombre : les 0 deviennent des 1 et réciproquement. Par exemple, le nombre 0001 1001 devient 1110 0110. Elle porte plusieurs noms : opération NOT, opération NON, complémentation, etc. Nous parlerons de complémentation ou d'opération NOT dans ce qui suit. Beaucoup d'ordinateurs gèrent cette opération, ils savent la faire en un seul calcul. Il faut dire que c'est une opération assez utile, bien que nous ne pouvons pas encore expliquer pourquoi.

Exemple d'addition en binaire.

La seconde opération à aborder est l'addition. Elle se fait en binaire de la même manière qu'en décimal : 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é. 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.

La troisième opération est une variante de l'addition appelée l'opération XOR, notée . Il s'agit d'une addition dans laquelle on ne propage pas les retenues. L'addition des deux bits des opérandes se fait normalement, mais les retenues sont simplement oubliées, on n'en tient pas compte. Le résultat est que l'addition se résume à appliquer la table d'addition précédente :

  • 0 0 = 0 ;
  • 0 1 = 1 ;
  • 1 0 = 1 ;
  • 1 1 = 0.

Pour résumer, le résultat vaut 1 si les deux bits sont différents, 0 s'ils sont identiques. L'opération XOR sera utilisée rapidement dans le chapitre suivant, quand nous parlerons rapidement du mot de parité. Et elle sera beaucoup utilisée dans la suite du cours, nous en feront fortement usage. Pour le moment, mémorisez juste cette opération, elle n'a rien de compliqué.

Une dernière opération est l'opération de population count. Il s'agit ni plus ni moins que de compter le nombre de bits qui sont à 1 dans un nombre. Par exemple, pour le nombre 0110 0010 1101 1110, elle donne pour résultat 9. Elle est utilisée dans certaines applications, comme le calcul de certains codes correcteurs d'erreur, comme on le verra dans le chapitre suivant. Elle est supportée sur de nombreux ordinateurs, encore que cela dépende du processeur considéré. Il s'agit cependant d'une opération assez courante, supportée par les processeurs ARM, les processeurs x86 modernes (ceux qui gèrent le SSE), et quelques autres.

L'hexadécimal

[modifier | modifier le wikicode]

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.

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 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.

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

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

É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 | modifier le wikicode]

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 | modifier le wikicode]

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 comprises 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 | modifier le wikicode]

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. 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).

L'avantage est que les calculs sont beaucoup plus simples avec cette méthode qu'avec les autres. L'ordinateur a juste à ne pas calculer les bits de poids fort. Pas besoin de faire une division pour calculer un modulo, pas besoin de corriger le résultat pour faire de l'arithmétique saturée.

Aparté : quelques valeurs particulières en binaire

[modifier | modifier le wikicode]

Plus haut, on a dit qu'avec n bits, on peut encoder toutes les valeurs allant de à . Ce simple fait permet de déterminer quelle est la valeur de certains entiers à vue d’œil. Dans ce qui va suivre, nous allons poser quelques bases que nous réutiliserons dans la suite du chapitre. Il s'agit de quelques trivias qui sont cependant assez utiles.

Le premier trivia concerne la valeur maximale  : elle est encodée par un nombre dont tous les bits sont à 1.

Le deuxième trivia concerne les nombres de la forme 000...000 1111 1111, à savoir des nombres dont les x bits de poids faible sont à 1 et tous les autres valent 0. Par définition, de tels nombres codent la plus grande valeur possible sur x bits, ce qui fait qu'ils valent .

Le troisième trivia concerne les nombres de la forme 11111...0000. En clair, des nombres où on a une suite consécutive de 1 dans les bits de poids fort et les x bits de poids faibles à 0. De tels nombres valent : . La preuve est assez simple : ils s'obtiennent en prenant la valeur maximale , et en soustrayant un nombre de la forme .

Valeurs particulières en binaire

Si on utilise l'arithmétique modulaire, la valeur n'est autre que la valeur de débordement haute pour les nombres stockés sur n bits, et se code comme le zéro (il faudrait 1 bit de plus pour stocker le 1 de poids fort de ). Les 1er et 3ème nombres évoqués dans les paragraphes précédents peuvent donc se passer de dans leur expression :

  • est codé 1111111111111111...111 (n bits à 1)
  • est codé 11111...111000..000000 (x bits à 0 précédés de n-x bits à 1)

Les nombres entiers négatifs

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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. On parle alors de représentation en signe-valeur absolue, aussi appelée représentation en signe-magnitude.

Avec cette technique, il y autant de nombres positifs que négatifs. Mieux : 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 en complément à un

[modifier | modifier le wikicode]

La représentation en complément à un peut être vue, en première approximation, comme une variante de la représentation en signe-magnitude. Et je dis bien : "en première approximation", car il y a beaucoup à dire sur la représentation en complément à un, mais nous verrons cela dans la section suivante sur le complément à deux. Conceptuellement, la représentation en signe-magnitude et celle du complément à un sont drastiquement différentes et sont basées sur des concepts mathématiques totalement différents. Mais elles ont des ressemblances de surface, qui font que faire la comparaison entre les deux est assez utile pédagogiquement parlant.

En complément à un, les nombres sont codés en utilisant un bit de signe qui indique si le nombre de positif ou négatif, couplé à une valeur absolue. Par contre, la valeur absolue est codée différemment en binaire. Pour les nombres positifs, la valeur absolue est codée en binaire normal, pas de changement comparé aux autres représentations. Mais pour les valeurs négatives, la valeur absolue est codée en binaire normal, puis tous les bits sont inversés : les 0 deviennent des 1 et réciproquement. En clair, on utilise une opération de complémentation pour les nombres négatifs.

Prenons un exemple avec un nombre codé sur 4 bits, avec un cinquième bit de signe. La valeur 5 est codée comme suit : 0 pour le bit de signe, 5 donne 0101 en binaire, le résultat est 0 0101. Pour la valeur -5, le bit de signe est 1, 5 donne 0101 en binaire, on inverse les bits ce qui donne 1010 : cela donne 11010. Notez qu'on peut passer d'un résultat à l'autre avec une opération de complémentation, à savoir en inversant les bits de l'autre et réciproquement.

Il s'agit là d'une propriété générale avec le complément à 1 : l'opposé d'un nombre, à savoir passer de sa valeur positive à sa valeur négative ou inversement, se calcule avec une opération NOT, une opération de complémentation (d'où son nom). L'avantage est que les ordinateurs gèrent naturellement d'opération de complémentation. Par contre, en signe-magnitude, inverser le bit de signe est une opération spécifique et ne sert qu'à ça. Il faut donc rajouter une opération en plus pour calculer l'opposé d'un nombre.

La représentation en complément à un garde les défauts de la représentation en signe-magnitude. Le zéro est codé deux fois, avec un zéro positif et un zéro négatif. La différence est que les valeurs négatives sont dans l'ordre inverse, il y a une symétrie un peu meilleure. Les deux zéros sont d'ailleurs totalement éloignés, ils correspondent aux valeurs extrêmes encodables : le zéro positif a tous ses bits à 0, le zéro négatif a tous ses bits à 1. Le résultat est que l'implémentation des comparaisons et de certains calculs est plus complexe qu'avec le signe-magnitude, mais de peu.

Codage sur 4 bits en complément à 1.

La représentation par excès

[modifier | modifier le wikicode]

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, le zéro sera encodé par X, 1, par X+1, etc. 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

La représentation en complément à deux

[modifier | modifier le wikicode]

La représentation en complément à deux est basée sur les mêmes mathématiques que le complément à un, mais fonctionne très différemment en pratique. Si on regarde de loin, son principe est assez différent : il n'y a pas de bit de signe ni quoi que ce soit d'autre. À la place, un nombre en complément à deux est encodé comme en binaire normal, à un point près : le bit de poids fort est soustrait, et non additionné aux autres. Il a une valeur négative : on soustrait la puissance de deux associée s'il vaut 1, on ne la tient pas en compte s'il vaut 0.

Par exemple, la valeur du nombre noté 111001 en complément à deux s'obtient comme suit :

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

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

Avec le complément à deux, comme avec le complément à un, le bit de poids fort vaut 0 pour les nombres positifs et 1 pour les négatifs.

L'avantage de cette représentation est qu'elle n'a pas de double zéro : le zéro n'est encodé que par une seule valeur. Par contre, la valeur autrefois prise par le zéro négatif est réutilisée pour encoder une valeur négative. La conséquence est que l'on a un nombre négatif en plus d'encodé, il n'y a plus le même nombre de valeurs strictement positives et de valeurs négatives encodées. Le nombre négatif en question est appelé le nombre le plus négatif, ce nom trahit le fait que c'est celui qui a la plus petite valeur (la plus grande valeur absolue). Il est impossible de coder l'entier positif associé, sa valeur absolue.

Codage sur 4 bits en complément à 2.

Le calcul du complément à deux : première méthode

[modifier | modifier le wikicode]

Les représentations en complément à un et en complément à deux sont basées sur le même principe mathématique. 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 tout calcul donne le même résultat si on remplace un nombre négatif par son complément (idem avec un nombre positif, son complément aura un signe inverse). 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 : l'ordinateur ne conserve que les bits de poids faible du résultat, les autres bits sont oubliés. Par exemple, prenons l'addition 15 + 2, 1111 + 0010 en binaire : le résultat ne sera pas 17 (10001), vu qu'on n'a pas assez de bits pour encoder le résultat, mais 1 (0001). 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.

Prenons un exemple, qui permettra d'introduire la suite. Encore une fois, on utilise 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 dont un veut calculer le complément à deux K. Dans le cas général, on a :

, vu que en utilisant le modulo.

En réorganisant très légèrement les termes pour isoler K, on a :

La formule précédent permet de calculer la complément à deux assez simplement, en faisant le calcul à la main.

Avant de poursuivre, prenons un exemple très intéressant : le cas où N = -1. Son complément à deux vaut donc :

Le terme devrait vous rappeler quelque chose : il s'agit d'un nombre dont tous les bits sont à 1. En clair, le complément à deux de -1 est un nombre de la forme 1111...111. Et cela vaut quel que soit le nombre de bits n. La représentation de -1 est similaire, peu importe que l'on utilise des nombres de 16 bits, 32 bits, 64 bits, etc.

Voyons maintenant le cas des puissance de deux, par exemple 2, 4, 8, 16, etc. Leur complément à deux vaut :

Nous avions vu précédemment dans le chapitre que ces nombres sont de la forme 11111...0000. En clair, des nombres dont les x bits de poids faible sont à 0, et tous les autres bits sont à 1. Par exemple, le complément à deux de -2 est un nombre dont les bits sont à 1, sauf le bit de poids faible. De même, le complément à deux de -4 a tous ses bits à 1, sauf les deux bits de poids faible. Et le complément à deux de -8 a tous ses bits à 1, sauf les trois bits de poids faible. Pour résumer, tout nombre de la forme a tous ses bits à 1, sauf les x bits de poids faible qui sont à 0.

Exemple avec des nombres de 8 bits
-1 1111 1111
-2 1111 1110
-4 1111 1100
-8 1111 1000
-16 1111 0000
-32 1110 0000
-64 1100 0000
-128 1000 0000
0 / - 256 0000 0000

Le calcul du complément à deux : seconde méthode

[modifier | modifier le wikicode]

Il existe une seconde méthode pour calculer le complément à deux d'un nombre, la voici. Pour les nombres positifs, encodez-les comme en binaire normale. Pour les nombres négatifs, faites pareil, puis inversez tous les bits, avant d'ajouter un. La procédure est identique à celle du complément à un, sauf que l'on incrémente le résultat final. Et ce n'est pas une coïncidence, comme nous allons le voir immédiatement.

Pour comprendre pourquoi la méthode marche, repartons de la formule précédente . Elle peut se reformuler comme suit :

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.

Le terme est donc le complément à un du nombre N, un nombre égal à sa représentation en binaire dont tous les bits sont inversés. Notons le nombre formé en inversant tous les bits de N. On a alors :

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.

Notons que tout ce qui a été dit plus haut marche aussi pour le complément à un, avec cependant une petite différence : la valeur haute de débordement n'est pas la même, ce qui change les calculs. 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.

L'extension de signe

[modifier | modifier le wikicode]

Dans les 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 le 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.

L'explication plus simple tient dans la manière de coder le bit de poids fort. Prenons l'exemple de la conversion d'un entier de 5 bits en un entier de 8bits. Les 4 bits de poids faible ont un poids positif (on les additionne), alors que le bit de poids fort a un poids négatif. Le nombre encodé vaut : . La valeur encodée sur 4 bits reste la même après extension de signe, car les poids des bits de poids faible ne changent pas. Par contre, le bit de poids fort change. Sur 8 bits, la valeur -16 est encodée par un nombre de la forme 1111 0000. En remplaçant le bit de poids fort par sa valeur calculée sur plus de bits, on remarque que les bits de poids fort ont été remplacés par des 1.

La représentation négabinaire

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 en vue de la taille de la mantisse;
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 | modifier le wikicode]

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. Un nombre logarithmique est donc composé d'un bit de signe et d'un exposant, sans mantisse. La mantisse est totalement implicite : tous les flottants logarithmiques ont la même mantisse, qui vaut 1.

Pour résumer, il ne reste que l'exposant, qui est tout simplement le logarithme en base 2 du nombre encodé, d'où le nom de codage flottant logarithmique donné à cette méthode. Attention toutefois : l'exposant est ici un nombre fractionnaire, 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 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. Donc, la représentation logarithmique permet de remplacer les multiplications/divisions par des additions/soustractions, 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 nombres à virgule non-représentables en binaire

[modifier | modifier le wikicode]

Quel que soit la façon de représenter les nombres à virgules, il existe des nombres qui ne peuvent être représentés de manière exacte, à savoir avec un nombre fini de décimales après la virgule. En soi, ce n'est pas spécifique au binaire, on a la même chose en décimal. Par exemple, la fraction 1/3 en décimal s'écrit 0.3333333..., avec une infinité de 3. La même chose existe en binaire, mais pour des nombres différents.

Déjà, évacuons le cas des nombres irrationnels, à savoir les nombres qui ne peuvent pas s'écrire sous la forme d'une fraction, comme ou . Ils ont une infinité de décimales que ce soit en binaire, en décimal, en hexadécimal, ou autre. Ils ne sont pas représentables avec un nombre fini de décimales quelle que soit la base utilisée. Concentrons-nous sur des nombres qui ne sont pas dans ce cas, et qui ont un nombre fini ou infini de décimales.

Le passage de la base 10 à la base 2 change le nombre de décimales, et peut faire passer d'un nombre fini de décimales à un nombre infini. Par exemple, est représenté en binaire avec une séquence infinie de bits : . Les nombres étant en base binaire et représenté avec un nombre limité de bits, il existe certains nombres décimaux triviaux qui ne sont pas représentables avec un nombre fini de décimales.

Et la réciproque n'est pas vraie : tout nombre binaire avec un nombre fini de décimales en binaire est représentable avec un nombre fini de décimales en base 10. Ceci est lié à la décomposition en facteur premier des bases utilisées :

Le nombre 10 possède tous les facteurs premiers de 2, mais 2 n'a pas de 5 dans sa décomposition.

Les encodages binaires alternatifs

[modifier | modifier le wikicode]

Outre le binaire que nous venons de voir, il existe d'autres manières de coder des nombres avec des bits. Et nous allons les aborder dans cette section. Parmi celle-ci, nous parlerons du code Gray, de la représentation one hot, du unaire et du ternaire. 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 utilisées dans des cas assez spécifiques. Et elles sont plus courantes que vous ne le pensez.

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

Les utilisations du code Gray sont assez nombreuses, bien qu'on n'en croise pas tous les jours. Un exemple : le code Gray est 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. Il est aussi utilisé dans des scénarios difficiles à expliquer ici (des codes correcteurs d'erreur ou des histoires de passages de domaines d'horloge). Mais le point important est que ce code sera absolument nécessaire dans quelques chapitres, 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.

Les représentations one-hot et unaire

[modifier | modifier le wikicode]

La représentation one-hot et la représentation unaire sont deux représentations assez liées, mais légèrement différentes.

La représentation unaire est la représentation en base 1. Avec elle, le zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0011, le trois avec 000...0111, etc. Pour résumer, le nombre N est encodé en mettant les N bits de poids faible à 1, à l'exception du zéro qui est encodé...par un zéro.

Décimal Binaire Unaire
0 000 0000 0000
1 001 0000 0001
2 010 0000 0011
3 011 0000 0111
4 100 0000 1111
5 101 0001 1111
6 110 0011 1111
7 111 0111 1111
8 1000 1111 1111

Pour convertir un nombre codé en unaire vers le binaire, il suffit de compter le nombre de 1. C'est ni plus ni moins ce que fait l'opération dite de population count dont nous avons parlé plus haut. La représentation unaire permet d'encoder N+1 valeurs sur N bits, car le zéro est encodé à part. Une valeur pour le zéro, plus une par bit.

Avec la représentation one-hot, un nombre est codé sur N bits, dont un seul bit est à 1. La position du 1 dans le nombre indique sa valeur. Précisément, la valeur encodée est égale au poids du bit à 1. En faisant cela, le zéro est codé en mettant le bit de poids faible à 1, la valeur 1 est codée avec les bits 00...00010, la valeur 2 est codée par les bits 00...000100, etc.

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

Elle permet d'encoder N valeurs différentes avec N bits, zéro inclus. On perd une valeur par rapport à l'unaire, car le zéro n'et pas encodé à part du reste. La traduction du binaire vers le one-hot est réalisé par un circuit nommé décodeur, que nous verrons d'ici quelques chapitres. De même, la traduction d'un nombre encodé en one-hot vers le binaire est réalisée par un circuit électronique appelé l'encodeur, qui est en quelque sorte l'inverse de l'encodeur. Les deux circuits sont très utilisés en électronique, ce sont des circuits de base, que nous n'aurons de cesse d'utiliser pour fabriquer d'autres circuits.

L'utilité de ces deux représentations 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. Elles sont très utilisées dans des circuits appelés des machines à état, qui doivent incorporer des circuits compteurs efficients. Et ces représentations permettent d'avoir des circuits pour compter qui sont très simples, efficaces, rapides et économes en circuits électroniques. La représentation unaire sera aussi utile à la toute fin du cours, dans les chapitres liés à l'exécution dans le désordre. Il en sera fait référence quand nous parlerons de fenêtres d'instruction, d'émission dans l'ordre, de scoreboarding, etc.

Il faut aussi noter qu'il a existé des ordinateur qui manipulaient des nombres codés en unaire. Enfin presque, la représentation utilisée était proche de l'unaire, mais ressemblait fortement. De tels ordinateurs étaient appelés des ordinateurs stochastiques et nous les aborderons à la toute fin de ce wikilivre, dans un chapitre annexe portant sur les ordinateurs ternaires (qui comptent en base 3) et unaires.

Les encodages hybrides entre décimal et binaire

[modifier | modifier le wikicode]

Pour terminer, nous allons voir les représentations qui mixent binaire et décimal. Il s'agit en réalité d'encodage qui permettent de manipuler des nombres codés en décimal, mais les chiffres sont codés en utilisant des bits. L'encodage Binary Coded Decimal est le plus connu de cette catégorie, mais il y en a quelques autres, qui sont moins connus. De telles représentations étaient très utilisées au début de l'informatique, mais sont aujourd'hui tombées en désuétude. Pour désigner ces encodages, nous parlerons d'encodages bit-décimaux : décimaux pour préciser qu'ils encodent des nombres codés en décimal, bit pour préciser que les chiffres sont codés avec des bits.

De tels encodages étaient utilisés sur les tous premiers ordinateurs pour faciliter le travail des programmeurs, mais aussi sur les premières calculettes. Ils sont utiles 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. Ils ont aussi l'avantage de bien se marier avec les nombres à virgule. Les représentations des nombres à virgule fixe ou flottante 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 les encodages bit-décimaux, on n'a pas ce problème, 0,2 étant codé 0000 , 0010.

Il a existé des ordinateurs qui travaillaient uniquement avec de tels encodages, appelés des ordinateurs décimaux. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Avec eux, la mémoire n'était pas organisée en octets, mais elle stockait des chiffres décimaux codés sur 5-6 bits. Le processeur faisait des calculs sur des chiffres bit-décimaux directement.

Leur grand avantage est leur très bonne performance dans les taches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en bit-décimal de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversions décimal-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications.

Les ordinateurs décimaux se classent en deux sous-types bien précis. Les premiers gèrent des entiers qui ont un nombre de chiffres fixes. Par exemple, l'IBM 7070 gérait des entiers de 10 chiffres, plus un signe +/- pour les entiers signés. Le processeur faisait les calculs directement en bit-décimal, il gérait des entiers faisant environ 10-15 chiffres décimaux et savait faire des calculs avec de tels nombres. Le second sous-type effectue les calculs chiffre par chiffre et géraient des nombres de taille variable, sans limite de chiffres ! Nous reparlerons de ces derniers dans un chapitre ultérieur, quand nous parlerons de la différence entre byte et mot. Pour le moment, ne gardez à l'esprit que les processeurs gérant un nombre fixe de chiffres décimaux, plus simples à comprendre.

Le Binary Coded Decimal

[modifier | modifier le wikicode]

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 ne sont tout simplement pas prises en compte. Sur quelques ordinateurs, ces combinaisons codent des chiffres décimaux en double : certains chiffres pouvaient être codés de deux manières différentes. Il est aussi possible d'utiliser ces valeurs pour coder quelque chose d'autre que des chiffres. Par exemple, il est possible de les utiliser pour coder un signe + ou -, afin de gérer les entiers relatifs. Une autre possibilité, complémentaire de la précédente, utilise ces valeurs en trop pour coder une virgule, afin de gérer les nombres non-entiers. Les possibilités sont nombreuses.

Le support du BCD implique souvent que le processeur supporte des opérations BCD, à savoir des opérations capables de travailler sur des opérandes en BCD et de donner un résultat en BCD. Il faut bien faire la différence entre les opérations en binaire et les opérations en BCD. Par exemple, on n'effectue pas une addition de la même manière en binaire et en décimal/BCD, même si les grandes lignes sont presque identiques. Les différences font que les processeurs doivent avoir des opérations différentes pour les deux encodages, de la même manière que les processeurs gèrent les opérations sur les flottants et les entiers séparément.

Le support du codage BCD est abandonné de nos jours, c'est surtout quelque chose qu'on trouve sur les anciens processeurs. Les architectures 8 et 16 bits supportaient à la fois des opérations binaires et des opérations BCD. Mais il a aussi existé une méthode intermédiaire, qui utilisait des additions binaires normales sur des opérandes BCD. L'idée est que le résultat de l'addition est incorrect, car les valeurs 10 à 15 peuvent apparaître comme chiffre dans le résultat. Mais on peut le corriger pour obtenir le résultat exact en BCD. Pour résumé, les processeurs faisaient des additions en binaire, et corrigeaient le résultat avec une opération spécifique pour obtenir un résultat en BCD. Nous en reparlerons dans le chapitre sur le langage machine et l'assembleur, dans lequel nous étudierons les différentes opérations que supporte un processeur.

Les encodages compacts du BCD

[modifier | modifier le wikicode]

Des variantes du BCD visent à réduire le nombre de bits utilisés pour encoder un nombre décimal. Nous allons les appeler les encodages BCD compacts. 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.

Les encodages BCD compacts sont utilisés par les programmeurs pour stocker des données dans un fichier, en mémoire, mais guère plus. Ils ne sont pas gérés par le processeur directement, on ne peut pas faire de calculs avec, le processeur ne gére pas d'opération BCD supportant de tels encodages. C'est théoriquement possible, mais ça n'a jamais été implémenté dans un processeur, le cout en circuit n'en valait pas la chandelle. Pour faire des calculs sur des nombres en BCD compact, on doit les décompresser et les convertir en BCD normal ou en binaire, puis faire les calculs avec des opérations BCD/binaire usuelles.

L'encodage Excess-3

[modifier | modifier le wikicode]

La représentation Excess-3 (XS-3) est une variante du BCD, qui a autrefois était utilisée sur d'anciens ordinateurs décimaux. Il s'agit d'une sorte d'hybride entre une représentation par excès et BCD. Chaque chiffre décimal est codé sur plusieurs bits en utilisant une représentation binaire par excès. Le biais est de 3 pour la représentation XS-3, il en existe des variantes avec un excès de 4, 5, mais elles sont moins utilisées. 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.

Codage en Excess-3
Décimal Binaire
-3 0 0 0 0
-2 0 0 0 1
-1 0 0 1 0
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
10 1 1 0 1
11 1 1 1 0
12 1 1 1 1

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.

L'XS-3 a été utilisé sur quelques ordinateurs décimaux assez anciens, notamment sur l'UNIVAC I et II.

Les codes bi-quinaires

[modifier | modifier le wikicode]

Les codes bi-quinaires utilisent un mélange entre encodage binaire, encodage one-hot et encodage décimal. L'idée de base derrière ces codes est d'utiliser la représentation one-hot pour coder un chiffre BCD. Le problème est que cela demande d'utiliser 10 bits pour encoder un chiffre BCD, ce qui est beaucoup. L'encodage bi-quinaire optimise le tout de manière à diviser par deux le nombre de bits requis pour l'encodage.

Pour simplifier, un chiffre encodé en bi-quinaire est composé de deux parties : une partie binaire de un bit, suivie par d'une partie quinaire encodée en représentation one-hot. La partie quinaire encode un nombre allant de 0 à 4, ce qui prend 5 bits pour encoder les valeurs 0, 1, 2, 3 et 4. La partie binaire indique s'il faut ou non ajouter 5 à la valeur encodée par la partie quinaire. Ainsi, on peut coder tous les nombres de 0 à 9 : 0 à 4 si la partie binaire vaut 0, 5 à 9 si elle vaut 1. La valeur du chiffre se calcule comme suit :

  • si la partie binaire vaut 0, elle est égale à la partie quinaire ;
  • si la partie binaire vaut 1, on ajoute 5 à la partie quinaire.

L'originalité de ce codage est qu'il permet de facilement compter sur ses doigts : on a deux mains, cinq doigts. De nombreuses langues utilisent ce système pour coder des nombres, comme le Wolof. Et ce système est utilisé dans certains pays pour compter sur ses doigts, voire dans la vie de tous les jours.

Code bi-quinaire où partie binaire et quinaire sont encodées en représentation one-hot.

L'encodage abordé au-dessus est une version simplifiée de l'encodage bi-quinaire, il en existe d'autres variantes. Le bi-quinaire vu juste au-dessus encode la partie binaire en binaire, et la partie quinaire en one-hot. Mais il est aussi possible d'inverser l'encodage, à savoir encoder la partie binaire avec une représentation one-hot, alors que la partie bi-quinaire est encodée en binaire normal.

Cependant, la version la plus intéressante utilise l'encodage one-hot pour les deux ! Elle est illustrée ci-contre. Avec cette dernière, un chiffre BCD est encodé sur 7 bits, dont 2 sont obligatoirement à 1 et les 5 restants sont à 0. Cette propriété permet de détecter qu'un bit a été inversé suite à une erreur de transmission ou un problème de stockage. Si un bit est inversé, le résultat aura 1 bit à 1 et 6 à 0, ou alors 3 bits à 1 et 4 à 0, ce qui ne colle pas. Le résultat après inversion ne donnera pas un chiffre bi-quinaire valide.

Une autre variante du bi-quinaire a été utilisée sur l'IBM 1401. Avec elle, la partie binaire disait si le chiffre était pair ou non, la partie quinaire encodait les valeurs 0, 2, 4, 6 et 8. Le chiffre se calculait en additionnant la partie binaire (0 ou 1) au nombre encodé par la partie quinaire. L'avantage de ce système est que la gestion des retenues lors des additions était grandement simplifiée.

Le bi-quinaire et ses variantes ont été utilisés sur d'anciens ordinateurs, dont l'IBM 650, l'UNIVAC Solid State et le UNIVAC LARC. La plupart utilisaient des variantes de l'encodage bi-quinaire proprement dit. Pour rentrer vraiment dans le détail, voici les encodages utilisés sur ces machines. La lecture est facultative, il est possible de passer directement à la section suivante :

IBM 650 Remington Rand 409 UNIVAC Solid State UNIVAC LARC
Chiffre 1357-9 bits 05-01234 p-5-421 bits p-5-qqq bits
0 10-10000 0000-0 1-0-000 1-0-000
1 10-01000 1000-0 0-0-001 0-0-001
2 10-00100 1000-1 0-0-010 1-0-011
3 10-00010 0100-0 1-0-011 0-0-111
4 10-00001 0100-1 0-0-100 1-0-110
5 01-10000 0010-0 0-1-000 0-1-000
6 01-01000 0010-1 1-1-001 1-1-001
7 01-00100 0001-0 1-1-010 0-1-011
8 01-00010 0001-1 0-1-011 1-1-111
9 01-00001 0000-1 1-1-100 0-1-110

Annexe : pourquoi l'octet existe

[modifier | modifier le wikicode]

Nous avons vu plus haut que les nombres encodés en binaire sont découpés en octets, des paquets de 8 bits. Et les octets sont présents partout dans la conception d'un ordinateur. Par exemple, la mémoire RAM d'un ordinateur moderne est organisée en octet. Elle contient des cases mémoire, chacune contenant un octet. Les octets, les cases mémoire, sont numérotées, ce qui permet de sélectionner un octet parmi tous les autres. La capacité de la mémoire est le nombre d'octets qu'elle contient, elle s'exprime en kilo-octets (1024 octets), mébi-octets (1024 kilo-octets), gibi-octets (1024 mébi-octets). Les octets sont donc présents partout dans l'informatique moderne.

Contenu de la RAM, simplifié
Numéro Octet
0 0000 1111
1 0101 0101
2 0111 0110
3 0011 1010
4 1001 1001
... ...
1023 0110 1010
1024 0001 1110

Mais pourquoi est-ce que les nombres encodés en binaire sont souvent découpés en octets ? Pourquoi ne pas utiliser des groupes de 3, 5, 6, 7, 9, 10 bits ? La question est d'autant plus légitime que d'anciens ordinateurs faisaient ainsi. Leurs mémoires étaient découpé en cases mémoire, mais chacune faisait 7 ou 9 bits, rien d'impossible à cela. À vrai dire, l'usage des octets au tout début de l'informatique était assez rare. Il y a eu un basculement progressif vers l'octet pendant les années 80-90. Reste à expliquer pourquoi. Si on devait résumer, les deux raisons principales sont le support du BCD et le support des caractères pour le texte. Voyons d'abord l'encodage des caractères en binaire.

L'encodage du texte : ASCII et autres encodages

[modifier | modifier le wikicode]

Pour les caractères, les encodages se sont concentrés sur l'encodage des alphabets occidentaux. Ils demandaient d'encoder au minimum toutes les lettres, en majuscules et minuscules, plus les chiffres, une dizaine de caractères pour la ponctuation. À cela, il faut ajouter des symboles utiles comme %, µ, $, £, #, {} et bien d'autres. Au minimum, cela fait au minimum 52 lettres (26 lettres doublées pour les majuscules) + 10 chiffres + une dizaine de caractères pour la ponctuation : cela fait plus de 72 valeurs à encoder. Pour encoder cela en binaire, le minimum est de 7 bits, ce qui permet d'encoder 128 caractères : les 72 caractères minimum, plus quelques autres. L'encodage ASCII utilisait d'ailleurs 7 bits pour encoder des nombres.

On pourrait croire que c'était une raison suffisante pour utiliser des groupes de 7 bits. Mais il y a quelques problèmes avec cela. Premièrement, les encodages 7 bits pour le texte marchent bien pour gérer la langue anglaise et la plupart des langues occidentales, mais d'autres langues ont besoin d'encoder des caractères locaux. Les 128 caractères de l'ASCII ne sont pas suffisant, ce qui demande de passer de 7 à 8 bits. On retombe donc sur un octet ! D'ailleurs, les encodages ASCII améliorés utilisaient les 127 premières valeurs pour l'ASCII normal, mais les 128 suivantes encodaient d'autres caractères. Les encodages en question varient d'un pays ou d'une langue à l'autre, mais ils sont terriblement importants, ce qui est un argument pour l'usage des octets.

Le support des données encodées sur un nibble

[modifier | modifier le wikicode]

Une autre raison pour l'usage des octets est qu'il est facile de les découper en deux nibbles. Pour rappel, les nibbles sont des groupes de 4 bits. Et il se trouve que de nombreuses données sont encodées sur 4 bits, ni plus ni moins. Les nibbles sont très important pour le support du BCD et de l’hexadécimal. Et c'était un avantage important au début de l'informatique, moins maintenant. Un octet regroupe deux nibbles : un nibble de poids faible, un nibble de poids fort.

Nibbles dans un octet.

Un chiffre BCD occupe un nibble, ce qui fait que la taille idéale de l'unité mémoire doit être un multiple de 4 bits : 4 bits, 8 bits, 12 bits, 16 bits, etc. Un nibble de 4 bits est insuffisant pour encoder des caractères, 8 bits fonctionne parfaitement, pas besoin d'aller plus loin. De plus, l'usage des nibbles permet de gérer la représentation hexadécimale très facilement. Un chiffre hexadécimal est encodé sur exactement un nibble, un octet regroupe deux chiffres hexadécimaux, deux octets 4 chiffres hexadécimaux, etc. Enfin, 8 bits est une puissance de deux, ce qui simplifie grandement l'encodage des entiers en binaire est est une source de nombreuses simplifications. Les circuits de l'ordinateur sont plus simple à concevoir quand ils manipulent des opérandes de 2, 4, 8, 16, 32, 64, 128 bits.

L'influence sur la taille des opérandes

[modifier | modifier le wikicode]

Les anciens ordinateurs étaient conçus pour le calcul scientifique, ou du moins des applications similaires. Ils n'avaient pas besoin de supporter le BCD, ni même de gérer du texte. Ils ne faisaient que des calculs et utilisaient des opérandes flottantes. La taille des opérandes était dictée par des considérations calculatoires. La norme à l'époque était que les calculs scientifiques devaient gérer des opérandes pouvant aller jusqu'à 10 chiffres décimaux. Et cela demande d'utiliser des opérandes entiers de 35 bits, avec un bit en plus pour encoder des opérandes positives ou négatives. Les 36 bits étaient parfois découpés en 6 groupes de 6 bits chacun, au lieu des octets de 8 bits.

Mais ces groupes de 6 bits étaient souvent peu utilisés. Prenons l'exemple des mémoires RAM. Plus haut, j'ai dit qu'une mémoire RAM normale est composée de plusieurs octets, tous numérotés. Intuitivement, vous vous dites que les anciens mainframes remplaçaient ces octets par des groupes de 6 bits dans la RAM. Mais en réalité, l'équivalent des RAM de l'époque utilisait des groupes de 36 bits. les groupes de 36 bits étaient aussi numérotés, les numéros étant appelés des adresses mémoire. Par contre, les adresses/numéros étaient codés sur 12 ou 18 bits, c'est à dire sur 2 ou 3 groupes de 6 bits. La raison est que ces numéros sont manipulés par le processeur, qui gère des opérandes de 36 bits. L'idée est que le numéro ne prend que la moitié ou un tiers des 36 bits gérés par le processeur.

Avec l'apparition de l'informatique commerciale, les ordinateurs ont du s'adapter aux besoins des entreprises, pour gérer la comptabilité, des applications financières, des documents texte, etc. Et cela demandait de supporter à la fois le BCD et les caractères/lettres. Il a fallu trouver une unité mémoire capable de gérer à la fois nombres entiers, chiffres BCD et caractères. L'octet était parfait pour ça. Les mainframes sont adaptés en supportant des caractères codés sur 6, 7 ou 8 bits. Mais le support demandait d'utiliser des circuits qui découpaient un groupe de 36 bits en plusieurs caractères ou en plusieurs chiffres BCD. Le tout demandait des circuits de décalage et de masquage assez complexes. Les ordinateurs 8 et 16 bits manipulaient des octets nativement : leur mémoire RAM gérait des octets, le processeur traitait des opérandes de 8, 16 ou 32 bits. Ils n'avaient pas besoin de matériel complexe pour extraire un caractère ou un chiffre BCD d'un octet/doublet/multiplet. C'était un avantage que les anciens mainframes n'avaient pas.


Dans le chapitre précédent, nous avons vu comment l'ordinateur faisait pour coder des nombres. Les nombres en question sont mémorisés dans des mémoires plus ou moins complexes. Et ces mémoires ne sont pas des dispositifs parfaits, elles peuvent subir des corruptions. Les corruptions en question se traduisent le plus souvent par l'inversion d'un bit : un bit censé être à 0 passe à 1, ou inversement. Le terme anglais pour ce genre de corruption est un bitflip, mais nous utiliserons le terme général "erreur", pour désigner ces bitflips .

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 : certains codages des nombres permettent de détecter et corriger ces bitflips. Pour cela, les codes de détection et de correction d'erreur ajoutent des bits de correction/détection d'erreur aux données. Les bits en question sont calculés à partir des données à transmettre/stocker et 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. Ils sont peu utilisées dans les ordinateurs grand public, mais elles sont très importantes dans les domaines demandant des ordinateurs fiables, comme dans l'automobile, l'aviation, le spatial, l'industrie, etc. Et ce chapitre va expliquer ce qu'elles sont, et aussi comment les circuits élaborés permettent de s'en protéger.

Dans ce qui suit, nous parlerons parfois de codes ECC, bien que ce soit un abus de langage : ECC est l'abréviation de Error Correction Code, mais certains de ces codes se contentent de détecter qu'une erreur a eu lieu, sans la corriger. Ceci étant dit, les codes ECC sont utilisés sur les mémoires comme les mémoires RAM, parfois sur les disques durs ou les SSDs, afin d'éviter des corruptions de données. Ils sont aussi utilisés quand on doit transmettre des données, que ce soit sur les bus de communication ou sur un support réseau. Par exemple, les données transmises via internet incorporent un code ECC pour détecter les erreurs de transmission, idem pour les transmissions sur un réseau local.

Le bit de parité

[modifier | modifier le wikicode]

Nous allons commercer par aborder le bit de parité/imparité. Le bit de parité est un bit ajouté à la donnée à mémoriser/transmettre. Sa valeur est telle que 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 au 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.

Valeurs valides et invalides avec un bit de parité. Les valeurs valides sont en vert, les autres en noir.

Il est maintenant temps de parler de si un bit de parité est efficace ou non. Que ce soit avec un bit de parité ou d'imparité, environ la moitié des valeurs encodées sont invalides. En effet, si on prend un nombre codé sur N bits, bit de parité, inclut, on pourra encoder 2^n valeurs différentes. La moitié d'entre elle aura un bit de parité à 0, l'autre un bit de parité à 1. Et la moitié aura un nombre de bit à 1 qui soit pair, l'autre un nombre impair. En faisant les compte, seules la moitié des valeurs seront valides. Le diagramme ci-contre montre le cas pour trois bits, avec deux bits de données et un bit de parité.

L'octet/mot de parité et ses variantes

[modifier | modifier le wikicode]

L'octet de parité est une extension de la technique du bit de parité, qui s'applique à plusieurs octets. L'idée de base est de calculer un bit de parité par octet, et c'est plus ou moins ce que fait le mot de parité, mais avec quelques subtilités dans les détails.

Illustration du mot de parité.

La technique s'applique en général sur toute donnée qu'on peut découper en blocs d'une taille fixe. Dans les exemples qui vont suivre, les blocs en question seront des octets, pour simplifier les explications, mais il est parfaitement possible de prendre des blocs plus grands, de plusieurs octets. La méthode fonctionne de la même manière. On parle alors de mot de parité et non d'octet de parité.

Le calcul du mot de parité

[modifier | modifier le wikicode]

Le calcul du mot de parité se calcule en disposant chaque octet l'un au-dessus des autres, le tout donnant un tableau dont les lignes sont des octets. 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é.

  • 1100 0010 : nombre ;
  • 1000 1000 : nombre ;
  • 0100 1010 : nombre ;
  • 1001 0000 : nombre ;
  • 1000 1001 : nombre ;
  • 1001 0001 : nombre ;
  • 0100 0001 : nombre ;
  • 0110 0101 : nombre ;
  • ------------------------------------
  • 1010 1100 : octet de parité.

Le calcul de l'octet de parité se fait en utilisant des opérations XOR. Pour rappel, une opération XOR est équivalente à une addition binaire dans laquelle on ne tiendrait pas compte des retenues. L'opération prend deux bits et effectue le calcul suivant :

  • 0 0 = 0 ;
  • 0 1 = 1 ;
  • 1 0 = 1 ;
  • 1 1 = 0.

En faisant un XOR entre deux octets, on obtient l'octet de parité des deux octets opérandes. Et cela se généralise à N opérandes : il suffit de faire un XOR entre les opérandes pour obtenir l'octet de parité de ces N opérandes. Il suffit de faire un XOR entre les deux premières opérandes, puis de faire un XOR entre le résultat et la troisième opérande, puis de refaire un XOR entre le nouveau résultat et le quatrième opérande, et ainsi de suite. Le calcul du mot de parité se fait aussi avec des opérations XOR, cela marche au-delà de l'octet.

La récupération des données manquantes/effacées

[modifier | modifier le wikicode]

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é. Il suffit de déterminer, pour chaque colonne, quel valeur 0/1 est compatible avec la valeur du bit de parité associé. C'est d'ailleurs pour cette raison que le mot de parité est utilisé 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 retirer le disque dur endommagé et reconstituer ses données.

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 savoir deux choses :

  • faire un XOR entre un nombre et lui-même donne 0 ;
  • faire un XOR entre une opérande et zéro redonne l'opérande comme résultat.

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. Un exemple sera certainement plus parlant.

Prenons le cas où on calcule l'octet de parité de quatre octets nommés O1, O2, O3 et O4, et notant le résultat . On a alors :

Maintenant, imaginons que l'on veuille retrouver la valeur du second octet O2, qui a été corrompu ou perdu. Dans ce cas, on fait un XOR avec O1, O3 et enfin O4 :

On injecte alors l'équation .

On réorganise les termes :

On se rappelle que A XOR A = 0, ce qui simplifie grandement le tout :

On retrouve bien l'octet manquant.

La combinaison d'un mot de parité avec plusieurs bits de parité

[modifier | modifier le wikicode]

Avec un octet/mot de parité, on peut détecter qu'une erreur a eu lieu, mais aussi récupérer un octet/mot effacé. Mais si un bit est modifié, on ne peut pas corriger l'erreur. En effet, on ne sait pas détecter quel octet a été modifié par l'erreur. Maintenant, ajoutons un bit de parité à chaque octet, en plus de l'octet de parité.

Exemple avec quatre octets
Octets Bit de parité de chaque octet
Octet 1 0 0 1 1 1 1 1 1 0
Octet 2 0 0 1 0 1 0 1 1 0
Octet 3 1 1 0 1 1 0 0 1 1
Octet 4 0 1 1 0 1 0 0 0 1
Octet de parité 1 0 1 0 0 1 0 1

En faisant cela, on peut détecter qu'un bit a été modifié, mais aussi corriger l'erreur assez simplement. En cas d'erreur, deux bits de parité seront faussés : celui associé à l'octet, celui dans l'octet de parité. On peut alors détecter le bit erroné. Une autre méthode est de regarder les bits de parité associés aux octets, pour détecter l'octet erroné. Reste alors à corriger l'erreur, en supprimant l'octet invalide et en récupérant l'octet initial en utilisant le mot de parité.

Exemple avec quatre octets
Octets Bit de parité de chaque octet
Octet 1 0 0 1 1 1 1 1 1 0
Octet 2 0 0 1 0 1 0 1 1 0
Octet 3 1 1 0 0 1 0 0 1 1
Octet 4 0 1 1 0 1 0 0 0 1
Octet de parité 1 0 1 0 0 1 0 1

Le nombre de bits de parité total est élevé

[modifier | modifier le wikicode]

Le tout demande d'utiliser beaucoup de bits de parité. Pour N octets, il faut un bit de parité par octet et un octet de parité, ce qui donne N + 8 bits de parité. Pour 64 bits, soit 8 octets, cela fait 16 bits de parité nécessaires, soit 25% de bits en plus. Maintenant, prenons le cas général où on n'utilise pas des octets, mais des mots de M bits, plus longs ou plus courts qu'un octet. Dans ce cas, on a N + M bits de parité.

Notons que la technique peut s'appliquer avec des octets ou des nibbles, si on organise les bits correctement. Par exemple, prenons un nibble (4 bits). On peut l'organiser en un carré de deux bits de côté et ajouter : un bit de parité par colonne, un bit de parité par colonne (le mot de parité). Le tout donne 4 bits de parité, pour 4 bits de données : on double la taille de la donnée. Il est aussi possible de faire pareil avec un octet, l'organisant en deux lignes de 4 bits. Le résultat est de 6 bits de parité, ce qui est un petit peu mieux qu'avec un nibble : on passe à 3/4 de bits de plus.

Exemple d'octet avec parité ajoutée
0 0 1 1 0
1 0 0 1 0
1 0 1 0

Il est possible de faire la même chose pour des données de plusieurs octets. Pour un nombre de 16 bits, l'idéal est de faire 4 lignes de 4 bits chacune, ce qui fait 8 bits de parité au total. Pour 32 bits, on passe à 12 bits de parité, etc. Au total, voici la quantité de bits de parité nécessaires suivant la longueur de la donnée :

Exemple d'octet avec parité ajoutée
4 8 16 32 64 128 256 ...
4 6 8 12 16 24 32 ...

Il existe cependant des techniques plus économes, que nous allons voir dans ce qui suit. Par plus économes, il faut comprendre qu'elles utilisent moins de bits de parité, pour une fiabilité identique, voire meilleure. C'est là un défaut de la technique précédente : elle utilise beaucoup de bits de parités pour pas grand chose.

Les capacités de correction de la technique

[modifier | modifier le wikicode]

Notons que cette solution permet de corriger plus d'une erreur. Dans le pire des cas, on peut détecter et corriger une erreur. Si toutes les erreurs sont toutes dans le même mot/octet, alors on peut récupérer l'octet manquant. Le bit de parité permet de détecter un nombre impair d'erreur, soit 1, 3, 5, 7, ... erreurs. Il faut donc que le nombre d'erreurs dans l'octet soit impair.

Exemple avec quatre octets
Octets Bit de parité de chaque octet
Octet 1 0 0 1 1 1 1 1 1 0
Octet 2 0 0 1 0 1 0 1 1 0
Octet 3 1 1 0 0 1 0 0 1 1
Octet 4 0 1 1 0 1 0 0 0 1
Octet de parité 1 0 1 0 0 1 0 1

Par contre, si le nombre d'erreurs dans un octet est pair, alors le bit de parité associé à l'octet ne remarque pas l'erreur. On sait sur quelles colonnes sont les erreurs, pas la ligne.

Exemple avec quatre octets
Octets Bit de parité de chaque octet
Octet 1 0 0 1 1 1 1 1 1 0
Octet 2 0 0 1 0 1 0 1 1 0
Octet 3 1 1 0 0 1 0 0 1 1
Octet 4 0 1 1 0 1 0 0 0 1
Octet de parité 1 0 1 0 0 1 0 1

De même, si les erreurs touchent deux octets, alors on ne peut rien corriger. On peut détecter les erreurs, mais pas les corriger.

Exemple avec quatre octets
Octets Bit de parité de chaque octet
Octet 1 0 0 1 1 1 1 1 1 0
Octet 2 0 0 1 0 1 0 1 1 0
Octet 3 1 1 0 0 1 0 0 1 1
Octet 4 0 1 1 0 1 0 0 0 1
Octet de parité 1 0 1 0 0 1 0 1

Pour résumer, pour des mots de M bits, on peut corriger entre 1 et M/2 erreurs. Pour un octet, cela permet de détecter 1 erreur, 3/5/7 erreurs si elles ont lieu dans le même octet.

La protection des bits de parité

[modifier | modifier le wikicode]

Avec la méthode précédente, les bits de parité ne sont pas protégés : la moindre corruption des bits de parité fait que la méthode ne marche plus. Pour cela, il y a une solution toute simple : calculer un bit de parité qui tient compte de tous les bits de parité. Ainsi, si un bit de parité est corrompu, alors ce super-bit de parité détecterait l'erreur.

Dans les tableaux précédents, cela revient à ajouter un bit de parité dans la case tout en bas à droite. Le bit de parité calculé à partie des autres bits de parité est en rouge dans le tableau suivant. Il peut se calculer de plusieurs manières. La plus simple est calculer le bit de parité des 6 bits de parité, ceux de l'octet de parité et les autres. En faisant ainsi, on ganratit que tous les bits de parités sont protégés.

Exemple avec un octet
0 0 1 1 0
1 0 0 1 0
1 0 1 0 0

Avec cette technique, il faut faire la différence entre les bits de parité primaire, qui calculent la parité de tout ou partie des données, et le bit de parité secondaire, qui est calculé à partie des bits de parité primaire. Généralement, les codes correcteurs/détecteurs d'erreur avec des bits de parité secondaires sont assez peu efficaces, que ce soit en termes de fiabilité ou d'économies de bits. Ils utilisent beaucoup de bits et ne protègent que peu contre les erreurs. De plus, les bits de parité secondaires ne font que repousser le problème : le bit de parité secondaire peut être modifié lui aussi ! Les bits de parité secondaires ne protègent que contre la modification des bits de parité primaire, mais pas de leurs modifications propres. Mais qu'on se rassure : on peut protéger les bits de parité primaire sans recourir à des bits de parité secondaires, avec le codage que nous allons voir dans ce qui suit.

Les codes de Hamming

[modifier | modifier le wikicode]

Le code de Hamming se base sur l'usage de plusieurs bits de parité pour un seul nombre. Chaque bit de parité est calculé à partir d'un sous-ensemble des bits. 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 bit 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é. Mais cela est aussi vrai pour la technique précédente. Un point important est que si un bit de parité est corrompu et change de valeur, les autres bits de parité ne le seront pas et c'est ce qui permettra de détecter l'erreur. Si un bit de données est inversé, plusieurs bits de parité sont touchés, systématiquement. Donc si un seul bit de parité est incompatible avec les bits de données, alors on sait qu'il a été inversé et qu'il est l'erreur. Pas besoin de faire comme avec la technique précédente, avec un mot de parité complété avec des bits de parité, avec un bit de parité secondaire.

Hamming(7,4)

Le code de Hamming le plus connu est certainement le code 7-4-3, un code de Hamming parmi les plus simples à 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, mais aussi des autres bits de parité. 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.

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 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)

É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 codes de Hamming sont généralement plus économes que la technique précédente, avec un mot de parité combiné à plusieurs bits de parité. Par exemple, pour 4 bits, le code de Hamming 7-4-3 n'utilise que 3 bits de parité, contre 4 avec l'autre technique. Pour 7 bits, elle n'en utilise que 4, contre 6. Voici un tableau qui donne combien on peut protéger avec N bits de parité en utilisant un code de Hamming. On voit que les codes de Hamming sont bien plus économes que le mot de parité, tout en étant tout aussi puissant (ou presque).

Bits de parité 2 3 4 5 6 7 8 9
Données 1 4 11 26 57 120 247 502

Les sommes de contrôle

[modifier | modifier le wikicode]

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.

La première 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éaires ou encore les codes polynomiaux cycliques.


Les circuits électroniques

[modifier | modifier le wikicode]

Le chapitre précédent nous a appris à encoder des nombres en binaire, ce qui est suffisant pour encoder n'importe quelle donnée. Reste à savoir comment un ordinateur fait des opérations sur ces bits. Dans ce chapitre, nous allons voir qu'un ordinateur effectue des opérations très simples sur des bits, opérations qui sont implémentées avec des portes logiques, elles-mêmes fabriquées avec des transistors. Nous allons voir les portes logiques dans ce chapitre, puis comment faire des circuits plus complexes dans les chapitres suivants.

Les portes logiques de base

[modifier | modifier le wikicode]

Les portes logiques sont des circuits qui prennent un ou plusieurs bits en entrée et fournissent un bit en guise de résultat. Elles possèdent des entrées sur lesquelles on va placer des bits, et une sortie sur laquelle se trouve le bit de résultat. Les entrées ne sont rien d'autre que des morceaux de « fil » conducteur sur lesquels on envoie un bit (une tension). La sortie est similaire, si ce n'est qu'on récupère le bit de résultat.

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 | modifier le wikicode]

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 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 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 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 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 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

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 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

Interlude propédeutique : combien il y a-t-il de portes logiques différentes ?

[modifier | modifier le wikicode]

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 le bit de sortie pour chaque combinaison possible des entrées. Le raisonnement se fait en deux étapes. La première détermine, pour n bits d'entrée, combien il y a de lignes dans la table de vérité. La seconde détermine combien de tables de vérité à c lignes existent.

Les 16 portes logiques à deux entrées possibles.

Le nombre de lignes de la table de vérité se calcule facilement quand on se rend compte qu'une porte logique reçoit en entrée un "nombre" codé sur n bits, et fournit un bit de résultat qui dépend du "nombre" envoyé en entrée. Chaque ligne de la table de vérité correspond à une valeur possible pour le "nombre" envoyé en entrée. Pour n bits en entrée, la table de vérité fait donc lignes.

Ensuite, calculons combien de portes logiques en tout on peut créer c lignes. 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, ce qui fait :

.

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. Pour les portes logiques à 3 bits d’entrée, cela fait 256 portes logiques.

Les portes logiques à un bit d'entrée

[modifier | modifier le wikicode]

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 | modifier le wikicode]

Les portes logiques à 2 bits d'entrée sont au nombre de 16. Et dans ces 16 portes, sont inclues les portes logiques à une entrée, à savoir les portes logiques FALSE, TRUE, OUI et NON. La porte OUI est en double, avec une porte qui recopie l'entrée A, une autre qui recopie l'entrée . De même, on trouve deux portes NON : une qui inverse l'entrée A et une autre qui inverse l'entrée B. En clair, sur les 16 portes logiques à deux entrées, 6 d'entre elles sont des portes à une entrée. Elles ont deux entrées, mais l'une d'entre elle n'est pas prise en compte. Seules 10 sont de vraies portes à deux entrées. Dans le tableau ci-dessous, avec les portes à une entrée illustrées en bleu.

Entrée FALSE NOR NCONVERSE NON (A) NIMPLY 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

Les 10 portes logiques restantes 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. De fait, la moitié des portes logiques sont l'inverse de l'autre.

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

Mais dans ce qui suit, nous allons voir que certaines portes logiques sont des dérivées des portes ET et des portes OU, formées en combinant une porte ET/OU avec une ou plusieurs portes NON. Les portes logiques dérivées de la porte ET sont illustrées en rouge dans le tableau suivant, celles dérivées de la porte OU sont en vert, les portes XOR/NXOR sont en jaune, le reste est les portes à une entrée.

Entrée FALSE NOR NCONVERSE NON A NIMPLY 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

Les portes dérivées de la porte OU

[modifier | modifier le wikicode]

Les portes dérivées de la porte OU regroupent les deux portes OU et NAND, ainsi que deux nouvelles portes : IMPLY et CONVERSE. Elles sont équivalentes à une porte OU dont on aurait inversé une des entrées. 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. Un bon moyen de s'en rendre compte serait d'écrire la table de vérité de ce petit circuit, et le résultat que vous retomberez exactement sur la table de vérité d'une porte NAND. En clair, une porte NAND est équivalente à une porte OU dont on aurait inversé les deux entrées.

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

Les portes dérivées d'un OU mettent leur sortie à 1 pour trois lignes de la table de vérité, pour trois entrées possibles. Aussi, on les appellera des portes 3-combinaisons.

Entrée NAND OR CONVERSE IMPLY
00 1 0 1 1
01 1 1 0 1
10 1 1 1 0
11 0 1 1 1

Les portes dérivées de la porte ET

[modifier | modifier le wikicode]

Les portes dérivées de la porte ET regroupent les deux portes NOR et ET, ainsi que deux nouvelles portes : NCONVERSE et NIMPLY, qui sont respectivement l'inverse des portes CONVERSE et IMPLY. 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 en ajoutant une porte NON sur l'entrée adéquate. 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. Encore une fois, il faut pour cela écrire la table de vérité du circuit. Et le résultat 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.

Les portes dérivées d'un ET mettent leur sortie à 1 pour une seule combinaison d'entrée, une seule ligne de la table de vérité. Aussi, nous allons les appeler des portes 1-combinaison.

Entrée NOR NCONVERSE NIMPLY ET
00 1 0 0 0
01 0 1 0 0
10 0 0 1 0
11 0 0 0 1

Les liens entre portes 1 et 3-combinaisons

[modifier | modifier le wikicode]

Il y a un lien assez fort entre les ports dérivées de la porte ET et celles dérivées de la porte OU. Pour comprendre pourquoi, regardez le tableau suivant, qui liste les portes 1 et 3-combinaison en paires. On voit que chaque porte 1-combinaison est l'exact inverse d'une porte 3-combinaison ! La conséquence est que l'on peut créer n'importe quelle porte 3-combinaison à partie d'une porte 1-combinaison, et réciproquement.

Entrée NOR OU NCONVERSE CONVERSE IMPLY NIMPLY ET NAND
00 1 0 0 1 0 1 0 1
01 0 1 1 0 0 1 0 1
10 0 1 0 1 1 0 0 1
11 0 1 0 1 0 1 1 0

Par exemple, nous avions vu plus haut que la porte NOR est une porte dérivée d'une porte ET. On peut créer une porte OU en ajoutant une porte NON à une porte NOR basée sur un ET, ce qui donne le circuit ci-dessous. Ou encore, nous avions vu plus haut que la porte NAND est une porte dérivée d'une porte OU. On peut créer une porte ET en ajoutant une porte NON à une porte NAND basée sur un OU, ce qui donne le circuit ci-dessous.

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

Les portes XOR/NXOR sont "superflues"

[modifier | modifier le wikicode]

Dans cette section, nous allons montrer que les portes XOR/NXOR peuvent se fabriquer à partir d'autres portes logiques. Il y a deux manières pour concevoir une porte XOR/NXOR à partir de portes ET/OU/NON. La première méthode combine plusieurs portes 1-combinaison avec une porte OU. L'autre méthode fait l'inverse : on combine plusieurs portes 3-combinaisons avec une porte ET.

La première méthode : combiner des portes dérivée d'un ET avec un OU

[modifier | modifier le wikicode]

Commençons par le cas d'une porte XOR. La sortie d'une porte XOR est à 1 dans deux situations : soit la première entrée est à 1 et l'autre à 0, soit c'est l'inverse. Les deux cas correspondent respectivement aux portes 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

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.

La porte NXOR peut se concevoir à parti du même raisonnement. La porte NXOR sort un 1 dans deux cas : 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, alors que la porte NOR (une OU suivie d'une NOT) a sa sortie à 1 dans le second cas.

Entrée 1 Entrée 2 NOR ET NXOR
0 0 1 0 1
0 1 0 0 0
1 0 0 0 0
1 1 0 1 1

Reste à combiner les deux portes avec une porte OU. Le circuit obtenu est le suivant :

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.

Il s'agit là d'une technique qui marche au-delà des portes XOR, et qui marche pour toutes les portes logiques. L'idée est de lister toutes les lignes de la table de vérité où la porte sort un 1. Pour chaque ligne, on prend la porte 1-combinaison adéquate : celle qui sort un 1 pour cette ligne. On effectue ensuite un OU entre toutes les portes dérivées d'un ET. 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 de portes 1-combinaison seules et qui demande d'utiliser une porte NON pour. On peut donc, en théorie, fabriquer toutes les portes logiques à partir de seulement les portes ET, OU et NON.

La seconde méthode : combiner des portes dérivées d'un OU avec un ET

[modifier | modifier le wikicode]

Une porte logique peut être conçue avec une méthode opposée à la précédente. Au lieu de procéder par addition, on procède par soustraction. Cette méthode demande de prendre des portes 3-combinaison et de les combiner avec une porte ET. Elle permet de fabriquer toutes les portes logiques, sauf la porte TRUE.

Par exemple, prenons la porte XOR. On 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. Sauf qu'on veut que la porte sorte un 0 quand les deux entrées sont à 1, alors qu'un ET fait l'inverse, ce qui indique qu'on doit plutôt utiliser une porte NAND. Voici ce que cela donne :

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

On voit qu'en faisant un ET entre les sortie des portes OU et NAND, on obtient le résultat voulu.

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.

Les portes NAND et NOR permettent de fabriquer toutes les autres portes

[modifier | modifier le wikicode]

Dans la section précédente, nous avons vu que toutes les portes logiques peuvent être créés soit uniquement à partir de portes ET/NAND , soit uniquement à partir de portes OU/NOR. 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 ! Pour comprendre pourquoi, nous allons essayer de créer toutes les portes à une entrée à partir de portes ET/OU/NAND/NOR.

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 ces portes, sur laquelle nous reviendrons rapidement dans le chapitre sur les circuits combinatoires, et qui sera très utile vers la fin du chapitre. Par contre, impossible de créer une porte NON facilement.

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

Maintenant, que se passe-t-il si on utilise une porte NAND/NOR ? La réponse est simple : on obtient une porte NON ! Pour comprendre pourquoi, il faut imaginer que la porte NAND/NOR est composée d'une porte ET/OU suivie par une porte NON. Le bit d'entrée subit un ET/OU avec lui-même, avant d'être inversé. Le passage dans le ET/OU se comporte comme une porte OUI, alors que la porte NON l'inverse.

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

On vient de voir qu'il est possible 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.

Les portes logiques à plus de deux entrées

[modifier | modifier le wikicode]

Par abus de langage, le terme "porte logique" désigne toutes les portes logiques à une ou deux entrées, mais pas au-delà. À l'exception de certains circuits assez simples, qui 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. 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. 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 | modifier le wikicode]

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 OU à 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

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, certains schémas seraient trop chargés en portes ET/OU pour être lisibles.

La porte à majorité

[modifier | modifier le wikicode]

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 impair, pour éviter une situation où exactement la moitié des entrées sont à 1 et l'autre à 0. Il existe cependant des portes logiques à 4, 6, 8 entrées, mais elles sont plus rares.

Une porte à majorité est souvent 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 utile. De plus, il est possible de créer une grande partie des circuits électroniques possibles en utilisant seulement des portes à majorité !

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

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.


Les circuits combinatoires

[modifier | modifier le wikicode]

Dans ce chapitre, nous allons aborder les circuits combinatoires. Ils prennent des données sur leurs entrées et fournissent un résultat en sortie, comme tous les circuits électroniques que nous verrons dans ce cours. Le truc, c'est que le résultat en sortie ne dépend que des entrées et de rien d'autre. Pour donner quelques exemples, on peut citer les circuits qui effectuent des additions, des multiplications, ou d'autres opérations arithmétiques du genre. Ils sont opposés aux circuits dit séquentiels qui ont une capacité de mémorisation, pour lesquels le résultat dépend des entrées et de ce qu'ils ont mémorisés avant. Nous verrons les circuits séquentiels dans un chapitre ultérieur, car ils sont fabriqués en combinant des circuits combinatoires avec des mémoires.

Quelle que soit sa complexité, un circuit combinatoire est 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 est un circuits séquentiel.

Dans ce qui va suivre, nous allons voir comment créer des circuits combinatoires à plusieurs entrées et 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 de résultat.

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

Créer des circuits à plusieurs sorties peut se faire en assemblant plusieurs circuits à une sortie. La méthode pour ce faire est très simple : chaque sortie est calculée indépendamment des autres, avec un circuit à une sortie. En assemblant ces circuits à plusieurs entrées et une sortie, on peut ainsi calculer toutes les sorties. Évidemment, il est possible de faire des simplifications ensuite. Par exemple, en mutualisant des portions de circuit identiques entre deux sous-circuits. Mais laissons cela pour plus tard.

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 | modifier le wikicode]

Pour commencer, nous avons besoin de décrire le circuit électronique qu'on souhaite concevoir. Et pour cela, il existe plusieurs grandes méthodes : la table de vérité, les équations logiques, 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 les portes logiques sont reliées. 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 comme le ferait un schéma avec des portes logiques.

La table de vérité

[modifier | modifier le wikicode]

La table de vérité décrit ce que fait le circuit, mais ne dit pas quelles sont les portes logiques utilisées pour fabriquer le circuit, ni comment celles-ci sont reliées. Elle se borne à donner la valeur de la sortie pour chaque entrée. Pour créer cette table de vérité, il faut commencer par lister toutes les valeurs possibles des entrées dans un tableau, puis de lister la valeur de chaque sortie pour toute valeur possible en entrée. 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.

Le premier circuit que l'on va créer est un inverseur commandable, qui fonctionne soit comme une porte NON, soit comme une porte OUI, selon ce qu'on met sur une entrée de commande. Le bit de commande indique 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

Pour donner un autre exemple, on va prendre un circuit calculant le bit de parité d'un nombre. Nous avons déjkà vu ce qu'est ce bit de parité dans le chapitre sur les codes correcteurs d'erreur, mais faisons un rappel rapide.. Dans le chapitre sur le binaire, nous avons vu que le nombre de bits à 1 dans un nombre est appelé sa population count. Par exemple, la population count du nombre 0110 est de 2, celle du nombre 0111 est de 3. Le bit de parité dit cette population count est un nombre pair ou impair : zéro si elle est paire et 1 si elle est impaire. Le bit de parité et la population count sont très utilisées dans les codes de détection/correction d'erreur, je ne reviens pas dessus. 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

Pour le dernier exemple, nous allons créer une porte à majorité de 3 bits. Pour rappel, une porte à majorité prend en entrée un opérande et a pour résultat le bit majoritaire dans ce nombre . 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 | modifier le wikicode]

La table de vérité peut être transformée en équations logiques, qui mettent en œuvre un circuit avec des portes logiques. Il ne s'agit pas des équations auxquelles vous êtes habitués, les équations logiques ne font que des ET/OU/NON sur des bits. Dans le détail, les variables sont des bits (les entrées du circuit considéré), 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 .

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.

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. L'équation obtenue n'est pas forcément idéale, mais on peut éventuellement la simplifier par la suite. Elle peut par exemple être simplifié en utilisant des portes XOR. 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, puis de traduire le tout en circuit.

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

[modifier | modifier le wikicode]

Une équation logique se traduit en circuit en substituant 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. Les schémas ci-dessous montrent des équations logiques et les circuits qui correspondent, tout en montrant les différentes substitutions intermédiaires.

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

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 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, avant 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. Une table de vérité peut être traduite en plusieurs équations logiques différentes. Après tout, on peut concevoir un circuit de différentes manières, des circuits câblés différents peuvent parfaitement faire la même chose. Des équations logiques qui décrivent la même table de vérité sont dites équivalentes. 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 est possible de convertir une équation logique en une autre équation équivalente, mais plus simple. Une section entière sera dédiée à ce genre de simplifications.

La méthode des minterms

[modifier | modifier le wikicode]

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. La première étape de ces deux méthodes est donc d'établir la table de vérité, voyons comment.

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

[modifier | modifier le wikicode]

La méthode des minterms est de loin la plus simple à comprendre. Pour l'expliquer, nous allons commencer par concevoir 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 | modifier le wikicode]

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) 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.

Combiner les comparateurs avec une constante

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

La méthode des maxterms

[modifier | modifier le wikicode]

La méthode des minterms, vue précédemment, n'est pas la seule pour 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 un circuit composé d'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 | modifier le wikicode]

La méthode des maxterms fonctionne sur un principe assez tordu. Elle effectue trois étapes, chacune correspondant à l'exact inverse de l'étape équivalente avec les minterms.

  • 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.

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 :

On peut se demander quelle méthode choisir entre minterms et maxterms. L'exemple précédent nous donne un indice. 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 | modifier le wikicode]

La méthode des maxterms fonctionne sur le principe inverse 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, si l'entrée est un minterm. Alors qu'un circuit maxterm vérifie si l'entrée ne met pas la sortie à 0, si l'entrée n'est pas un maxterm. Pour cela, le circuit 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 un comparateur avec une constante par maxterm. La sortie du comparateur est cependant l'inverse d'avec les minterms : 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.

Pour concevoir le circuit de comparaison, la méthode la plus simple reste de prendre un comparateur avec une constante, puis d'inverser sa sortie avec une porte NON. Une méthode équivalente remplace la porte ET à plusieurs entrées dans ce comparateur par une porte NAND. Il est alors possible d'utiliser les lois de De Morgan pour simplifier le circuit : la porte NAND devient une porte OU avec des portes NON sur ses entrées. Les porte NOn sur les entrées sont combinés avec la première couche de porte NON, elles s'annulent, ce qui donne le circuit final, avec une couche de portes NON suivie par une couche porte OU à plusieurs entrées.

Simplifier un circuit avec l'algèbre de Boole

[modifier | modifier le wikicode]

La méthode précédente donne une équation logique, en forme disjonctive ou conjonctive, qui est assez complexe. Heureusement, il est possible de la simplifier. Pour donner un exemple, prenez cette équation :

 ;

Elle peut se simplifier en :

Dans cet exemple, on passe donc de 17 portes logiques à seulement 3 ! Simplifier une équation logique permet de se faciliter la vie lors de la traduction de l'équation en circuit, mais cela donne un circuit plus rapide et/ou utilisant moins de portes logiques. Autant vous dire qu'apprendre à simplifier ces équations est quelque chose de crucial.

Pour simplifier une équation logique, on doit utiliser certaines propriétés mathématiques simples tirées de ce qu'on appelle l’algèbre de Boole, terme barbare qui regroupe pourtant des manipulations assez basiques. En utilisant ces règles algébriques, on peut factoriser ou développer certaines expressions, comme on le ferait avec une équation normale. 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 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 | modifier le wikicode]

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.

Le premier cas est celui où on fait un ET/OU/XOR entre un bit et lui-même. Il regroupe les trois formules suivantes :

Le second cas est celui où on fait un ET/OU/XOR entre un bit et son inverse. Il regroupe les trois formules suivantes :

,
.

Le troisième cas est celui où on fait un ET/OU/XOR entre un bit et 1. Il regroupe les trois formules suivantes :

,
.

Le quatrième cas est celui où on fait un ET/OU/XOR entre un bit et 0. Il 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é





Nous avons déjà utilisé implicitement les formules du premier cas, à savoir et dans le chapitre sur les portes logiques. En effet, elles disent comment fabriquer une porte OUI à partir d'une porte ET ou d'une porte OU. Au passage, la formule nous dit pourquoi cela ne marcherait pas du tout avec une porte XOR.

Ces formules 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. Mais rassurez-vous, un rappel sera fait dans ce chapitre, vous n'avez pas encore besoin de mémoriser ces formules par cœur, même si c'est toujours bon à prendre.

Les lois de de Morgan et la double négation

[modifier | modifier le wikicode]

Les lois de de Morgan et la double négation sont de loin les formules plus importantes à retenir. Les voici :

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


Les deux loi de De Morghan reformulent quelque chose que nous avons déjà vu dans le chapitre sur les portes logiques :

  • la première loi de de Morgan dit qu'une porte NAND peut se fabriquer avec une porte OU précédée de deux portes NON ;
  • la seconde loi dit qu'une porte NOR peut se fabriquer avec une porte ET précédée de deux portes NON.
Illustration des lois de De Morgan
1. Theorem.svg
2. Theorem.svg

Les lois de de Morgan peuvent se généraliser pour plus de deux entrées, elles marchent pour des portes NAND/NOR à 3/4/5 entrées, voire plus. 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 forme normale disjonctive, et réciproquement. Pour le dire autrement, 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 | modifier le wikicode]

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, comme nous l'avions vu dans le chapitre précédent.

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

XOR :
NXOR :

Les deux formules donnent les deux circuits qu'on avait obtenu dans le chapitre sur les portes logiques.

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

Exemples complets

[modifier | modifier le wikicode]

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 .

Annexe facultative : les tableaux de Karnaugh

[modifier | modifier le wikicode]

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. Nous allons cependant aborder la méthode du tableau de Karnaugh. Précisons cependant que cette section est facultative, la plupart des simplifications que permet un tableau de Karnaugh peuvent se faire en utilisant l'algébre de Boole, il faut juste être compétent et parfois bien se creuser le cerveau.

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 | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 les opérations bit à bit, un ensemble d'opérations qui appliquent une opération binaire sur un ou deux nombres. La plus simple d'entre elle est l'opération NON, aussi appelée opération de complémentation, qui inverse tous les bits d'un nombre. Il s'agit de l'opération la plus simple et nous en avions déjà parlé dans les chapitres précédents. Mais il existe des opérations bit à bit un chouia plus complexes, comme celles qui font un ET/OU/XOR entre deux nombres. Pour être plus précis, elles font un ET/OU/XOR entre les deux bits de même poids. L'exemple du OU bit à bit est illustré ci-dessous, les exemples du ET et du XOR sont similaires.

Opération OU bit à bit.

De telles opérations sont appelées bit à bit car elles combinent les bits de même poids de deux opérandes. Par contre, il n'y a pas de calculs entre bits de poids différents, les colonnes sont traitées indépendamment. Elles sont très utilisées en programmation, et tout ordinateur digne de ce nom contient un circuit capable d'effectuer ces opérations. Dans ce chapitre, nous allons voir divers circuits capables d'effectuer des opérations bit à bit, et voir comment les combiner.

Les opérations bit à bit classiques peuvent prendre une ou deux opérandes. La plupart en prenant deux comme les opérations ET/OU/XOR, l'opération NON en prend une seule. Les opérations bit à bit sur deux opérandes sont au nombre de 16, ce qui correspond au nombre de portes logiques à deux entrées possibles. Mais ce chiffre de 16 inclut les opérations bit à bit sur une opérande unique, qui sont au nombre de 4. Les opérations bit à bit sur une seule opérande sont plus simples à voir, nous verrons les opérations bit à bit à deux opérandes plus tard.

Les opérations bit à bit à une opérande

[modifier | modifier le wikicode]

Les opérations bit à bit n'ayant qu'une seule opérande sont au nombre de quatre :

  • Mettre à zéro l'opérande (porte FALSE).
  • Mettre à 11111...11111 l'opérande (porte TRUE).
  • Inverser les bits de l'opérande (porte NON).
  • Recopier l'opérande (porte OUI).

Dans ce qui va suivre, nous allons créer un circuit qui prend en entrée une opérande, un nombre, et applique une des quatre opérations précédente. On peut choisir l'opération voulue grâce à plusieurs bits de commande, idéalement deux. Le circuit est composé à partir de circuits plus simples, au maximum trois : 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, ou si le circuit doit se comporter comme une simple porte OUI, qui recopie sont entrée sur sa sortie et ne fait donc aucune 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.

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 d'opérande 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

Pour résumer ce qui va suivre :

  • 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 une porte ET un peu modifiée.

Le circuit de mise à la valeur maximale

[modifier | modifier le wikicode]

Dans cette section, nous allons voir un circuit : soit recopie l'entrée sur sa sortie, soit la met à 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. Ce circuit est utilisé pour gérer les débordements d'entier avec l'arithmétique saturée (revoir le chapitre sur le codage des entiers pour plus d'explications). Le circuit de mise à 111111...111 gére le cas où le calcul déborde, ce qui demande 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.

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.

Circuit de mise à 1111111...11

Le circuit de mise à zéro

[modifier | modifier le wikicode]

Le circuit de Reset fait la même chose que le circuit précédent, sauf que sa sortie n'est pas mise à 1111...1111, mais à 0. Le circuit de Reset prend en entrée un bit de Reset qui indique s'il faut mettre à zéro l'entrée ou non. 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 adaptée : elle recopie le bit d'entrée si le bit de commande vaut 1, et elle le met à 0 si le bit de commande vaut 0. Cependant, rappelons que l'on souhaite que le le circuit fasse un Reset si le bit de commande est à 1, pas 0, et la porte ET fait l'inverse. Pour corriger cela, on doit ajouter une porte NON. Le tout donne le circuit ci-dessous.

Circuit de mise à zéro d'un bit

Un circuit qui met à zéro un nombre est composé de plusieurs circuits ci-dessus, à la différence que la porte NON est potentiellement partagée. Par contre, chaque bit est bien relié à une porte ET.

Circuit de mise à zéro

L'inverseur commandable

[modifier | modifier le wikicode]

L'inverseur commandable est un circuit qui, comme son nom l'indique, inverse les bits d'entrée si un bit de commande nommé Invert vaut 1. 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.

Le circuit qui combine les trois précédents

[modifier | modifier le wikicode]

Voyons maintenant un circuit qui combine les trois circuits précédents. L'implémentation naïve met les trois circuits les uns à la suite des autres, ce qui donne pour chaque bit d'opérande trois portes logiques ET/OU/XOR en série. Le problème est qu'il faut préciser trois bits de commandes, alors qu'on peut en théorie se débrouiller avec seulement 2 bits. Il faut alors ajouter un circuit combinatoire pour calculer les trois bits de commande à partir des deux bits initiaux.

Porte logique universelle de 1 bit, faite avec trois portes

Mais il y a moyen de se passer d'une porte logique ! L'idée est que mettre à 0 et mettre à 1 sont deux opérations inverses l'une de l'autre. Mettre à 1 revient à mettre à 0, puis à inverser le résultat. Et inversement, mettre à 0 revient à mettre à 1 avant d'inverser le tout. Il suffit donc de mettre le circuit d'inversion commandable à la fin du circuit, juste après un circuit de mise à 0 ou de mise à 1, au choix. 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

Les opérations bit à bit à deux opérandes

[modifier | modifier le wikicode]

Les opérations bit à bit à deux opérandes effectuent un ET, un OU, ou un XOR entre deux opérandes. Ici, le ET/OU/XOR se fait entre deux bits de même poids dans une opérande. Les circuits qui effectuent ces opérations sont assez simples, ils sont composés de portes logiques placées les unes à côté des autres. Il n'y a pas de possibilité de combiner des portes comme c'était le cas dans la section précédente.

Les opérations de masquage

[modifier | modifier le wikicode]

Il est intéressant de donner quelques exemples d'utilisation des opérations bit à bit ET/OU/XOR. L'utilité des opérations bit est bit est en effet loin d'être évidente. L'exemple que nous allons prendre est celui des opérations de masquage, très connue des programmeurs bas niveau. 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. Le résultat dépend de l'opération entre masque et opérande, les trois opérations utilisées étant un ET, un OU ou un XOR.

Faire un ET entre l'opérande et le masque va mettre certains bits de l’opérande à 0. 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, certains bits de l'opérande sont mis à 1. Les bits mis à 1 sont ceux pour lesquels le bit du masque correspondant est un 1.

Masques 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

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.

Les opérations pour tester un bit

[modifier | modifier le wikicode]

Une opération assez courante teste si un bit précis vaut 0 ou 1 dans une opérande. Elle est implémentée, là encore, avec un masque. L'opération se fait en deux temps : on sélectionne le bit voulu avec un masque, on teste la valeur du résultat. Pour sélectionner le bit voulu, il suffit de mettre tous les autres bits à 0, grâce au masque adéquat. Le résultat de l'opération met tous les autres bits à 0. Il reste alors à comparer le résultat obtenu avec 0. Si le résultat vaut 0, c'est que le bit sélectionné valait 0. Sinon, le bit testé valait 1.

Masques pour tester un bit.

Tester la valeur d'un bit peut se faire avec un circuit assez simple, lui-même composé de trois sous-circuits. Le premier circuit génère le masque, le second fait un ET entre le masque et l'opérande, le troisième compare le résultat avec 0.

Circuit qui sélectionne un bit et teste sa valeur

Le circuit de comparaison avec zéro est une simple porte OU à plusieurs entrées. Si l'entrée vaut 0, le OU fournira un 0 en sortie. Mais si le bit testé va 1, le résultat après application du masque contiendra un 1. Ce qui fait qu'une entrée du OU sera à 1, ce qui implique une sortie à 1. La difficulté est de créer le circuit de génération du masque, ce qu'on ne peut pas faire à ce point du cours. Par contre, nous sauront le faire au chapitre suivant.

Les portes logiques universelles à deux entrées

[modifier | modifier le wikicode]

Dans cette section, nous allons voir comment créer un circuit capable d'effectuer plusieurs opérations logiques, le choix de l'opération étant le fait d'une entrée de commande. Par exemple, imaginons un circuit 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.

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.

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

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.

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.

Une autre manière d'expliquer leur fonctionnement est qu'il traduisent un nombre encodé en binaire vers la représentation one-hot. Pour rappel, sur cette dernière, le nombre N est encodé en mettant le énième bit à 1, les autres sont à 0. Le bit de poids faible compte pour le zéro.

Les décodeurs sont très utilisés, au point que faire la liste de leurs utilisations serait bien trop long. Par contre, on peut d'or et déjà prévenir que les décodeurs sont utilisés dans toutes les mémoires RAM et ROM, présentes dans tout ordinateur. La RAM de votre ordinateur contient un ou plusieurs décodeurs, idem pour la mémoire caché intégrée dans le processeur, etc. C'est donc un circuit absolument primordial à étudier, qui reviendra souvent dans ce cours.

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

[modifier | modifier le wikicode]

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 portes logiques. 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Le multiplexeur

[modifier | modifier le wikicode]

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.

Les multiplexeurs sont très utilisés et on en retrouve partout : dans les mémoires RAM, dans les processeurs, dans les circuits de calcul, dans les circuits pour communiquer avec les périphériques, et j'en passe. Il s'agit d'un composant très utilisé, qu'il est primordial de bien comprendre avant de passer à la suite du cours.

Le multiplexeur à deux entrées

[modifier | modifier le wikicode]

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.

Les multiplexeurs à plus de deux entrées

[modifier | modifier le wikicode]

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é.

Il existe toutefois une manière bien plus simple pour créer un multiplexeur, qui s'inspire d'un circuit que nous avons déjà vu. Imaginons que les entrées du multiplexeur soient une opérande. Le multiplexeur sélectionne un bit dans l'opérande, et copie sa valeur sur sa sortie. Il ressemble donc au circuit qui teste un bit, qu'on a vu au chapitre précédent et qu'on va revoir dans ce qui suit. Pour rappel, ce circuit sélectionne un bit en appliquant un masque qui met les autres bits à 0, qui en regardant la valeur du résultat. Le circuit est le suivant :

Circuit qui sélectionne un bit et teste sa valeur

Le circuit qui génère le masque transforme le numéro du bit en un masque adéquat. Si le numéro du bit est de N, le masque a son énième bit à 1, les autres à 0. Pour le dire autrement, il convertit le numéro du bit en sa représentation one-hot. Et ce n'est ni plus ni moins que ce que fait un décodeur : la génération du masque est donc le fait d'un décodeur. 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 utilise le circuit de mise à zéro basé sur une couche de portes ET. La comparaison avec zéro se fait avec une simple porte OU à plusieurs 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. Le résultat est le suivant :

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

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.
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 où 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.

Une autre manière d'expliquer son fonctionnement est la suivant : un encodeur traduit un nombre codé en représentation one-hot vers du binaire normal.

L'utilité d'un encodeur n'est pas très évidente à ce moment du cours, mais nous pouvons déjà dire qu'ils seront utiles dans certaines formes de mémoires RAM appelées des mémoires associatives, qui sont utilisées dans des routeurs, switchs et autre matériel réseau. La majorité des mémoires caches de nos ordinateurs sont de ce type, bien que leur implémentation exacte ne fasse pas usage d'un encodeur. Une autre utilisation est la transformation d'un nombre codé en représentation one-hot vers du binaire normal, chose marginalement utile.

L'encodeur 4 vers 2

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 fort" 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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


Les circuits séquentiels

[modifier | modifier le wikicode]

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 bit, qui sont appelés des bascules, ou latches. Nous alloons voir ces bascules dans ce chapitre, nous verrons comment les combiner pour former une mémoire ou des compteurs dans le chapitre suivant.

L'interface d'une bascule

[modifier | modifier le wikicode]

Avant de voir comment sont fabriquées les bascules, nous allons voir quelle est leur interface, à savoir quelles sont leurs entrées et leurs sorties. La raison à cela est que des bascules avec la même interface peuvent être construites avec des circuits totalement différents. 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.

Avant toute chose, faisons quelques précisions. Sur les schéma qui vont suivre, les entrées sont à gauche, les sorties sont à droite. Beaucoup de bascules disposent de deux sorties, nommées Q et , même si ce n'est pas systématique. La sortie Q fournit le bit mémorisé dans la bascule. C'est sur cette sortie qu'on peut lire le bit mémorisé, le récupérer. La sortie fournit quant à elle l'inverse de ce bit mémorisé. Par inverse, on veut dire qu'elle fournit un 0 si le bit mémorisé est à 1, un 1 s'il vaut 0. Il est possible de créer des bascules sans sortie , mais nous verrons des exemples avec cette sortie.

Les bascules D

[modifier | modifier le wikicode]
Interface d'une bascule D.

Les bascules les plus simples sont les bascules D. Elles ont deux entrées 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. Il faut que l'entrée Enable passe à 1 pour que l'entrée soit recopiée dans la bascule et mémorisée. Tant que l'entrée Enable reste à 0, le bit mémorisé par la bascule reste le même, le circuit est insensible à l'entrée D.

Les bascules RS

[modifier | modifier le wikicode]
Interface d'une bascule RS.

En second lieu, on trouve les bascules RS, qui possèdent deux entrées. 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, le comportement dépend du sous-type de bascule RS.
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. Il faut dire que cette combinaison demande de mettre le circuit à la fois à 0 (entrée R) et à 1 (entrée S). 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. Mais sur d'autres bascules dites à entrée R ou S dominante, une entrée sera prioritaire sur l'autre. 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 | modifier le wikicode]
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 Dépend de la bascule
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 | modifier le wikicode]
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 | modifier le wikicode]
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 | modifier le wikicode]
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 | modifier le wikicode]

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 !

Dans ce qui suit, nous allons omettre volontairement la sortie , sauf pour les bascules RS.

La bascule D fabriquée avec un multiplexeur

[modifier | modifier le wikicode]

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.

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. Ce circuit de mise à zéro est placé dans la boucle, entre la sortie du multiplexeur et son entrée.

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

[modifier | modifier le wikicode]

Voyons maintenant comment implémenter une bascule RS. Son implémentation la plus simple est la bascule RS de type ET-OU, composée de trois portes logiques : 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.

Bascule RS de type ET-OU.

Son fonctionnement est simple à expliquer. La porte ET a deux entrées, dont une est bouclée et l'autre est une entrée de commande. Idem pour la porte OU. 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 recopie 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 : est-ce que la sortie Q est la sortie de la porte OU, ou la sortie de la porte ET ? La seule différence sera ce qu'il se passe quand on active les deux entrées à la fois. Si on prend la sortie de la porte ET, l'entrée Reset sera prioritaire sur l'entrée Set quand elles sont toutes les deux à 1. Et inversement, si on prend la sortie de la porte OU, 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

Les bascules RS à NOR et à NAND

[modifier | modifier le wikicode]

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. Pour cela, au lieu d'ajouter une 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 et simplifier le tout, comme indiqué dans le schéma suivant. 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.

Dans l'exemple précédent, nous avions pris la sortie Q en sortie de la porte ET, mais il est possible de faire pareil avec une bascule RS inversée de type ET/OU. 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 | modifier le wikicode]

Il y a quelques chapitres, nous avons vu qu'il est possible de créer une porte logique en combinant d'autres portes logiques entre elles. 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. 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. Dans ce qui suit, nous allons surtout voir comment fabriquer des bascules communes à partir de bascules RS.

Le passage d'une bascule RS à une bascule RS inversée (et inversement)

[modifier | modifier le wikicode]

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 s'agit d'une méthode simple, qui a la particularité de garder le caractère dominant/non-dominant des entrées.

Bascule RS conçue avec une bascule RS inversée.

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 | modifier le wikicode]

Passons maintenant aux bascules RS à entrée Enable. Vous l'avez peut-être senti venir : il est possible de modifier les bascules sans entrée Enable, pour leur en ajouter une. 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.

Circuit d'une bascule RS NOR à entrée Enable.
Circuit d'une bascule RS NAND à entrée Enable.

Les bascules D conçues à partir de bascules RS à entrée Enable

[modifier | modifier le wikicode]

Passons maintenant aux bascules D construites à partir d'une bascule RS à entrée Enable. 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é. 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.

Bascule D à 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, quelle que 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 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 | modifier le wikicode]

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.

Exemple de circuit séquentiel.

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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. Dans les circuits synchrones, les bascules sont mises à jour en même temps. À l'opposé, 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.

Les circuits asynchrones ne sont presque pas utilisés dans la quasi-totalité des ordinateurs modernes, qui sont des circuits synchrones. Nous ne verrons pas beaucoup de circuits asynchrones dans la suite du cours. Il faut dire que la grosse majorité des processeurs, mémoires et périphériques, sont des composants synchrones. La seule exception que nous verrons dans ce cours sont les anciennes mémoires DRAM asynchrones, qui sont aujourd'hui obsolètes, mais ont été utilisées dans les anciens PCs. Aussi, nous allons nous concentrer sur les circuits synchrones dans ce qui suit.

Le signal d'horloge des circuits synchrones

[modifier | modifier le wikicode]

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, puis de 1 à 0 et ainsi de suite. 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.

La fréquence influence la performance d'un circuit

[modifier | modifier le wikicode]

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. Mais attention : un processeur de 4 gigahertz peut être bien plus rapide qu'un processeur de 20 gigahertz, pour des raisons techniques qu'on verra plus tard dans ce cours. Si dans les grandes lignes, une fréquence plus élevée signifie une performance plus élevée, ce n'est qu'une règle heuristique assez imparfaite.

Un autre point important est que plus la fréquence d'un composant est élevée, plus il chauffe et consomme d'énergie. Nous ne pouvons pas expliquer pourquoi pour le moment, mais sachez que nous détaillerons cela dans le chapitre sur les tendances technologiques. Disons pour simplifier que plus la fréquence d'un composant est élevée, plus il change d'état fréquemment par seconde, et que chaque changement d'état consomme de l'énergie. Sur les circuits CMOS modernes, la consommation d'énergie est proportionnelle à la fréquence. Ne vous étonnez donc pas que les circuits qui fonctionnent à haute fréquence, comme le processeur, chauffent plus que les circuits de basse fréquence. S'il y a un radiateur et un ventilateur sur un processeur, c'est en partie à cause de ça.

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 chacun. Les mises à jour des registres 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 bascules synchrones

[modifier | modifier le wikicode]

Utiliser une horloge demande d'adapter les bascules. Les bascules du chapitre précédent sont mises à jour quand une entrée Enable est mise à 1. Mais avec un signal d'horloge, les bascules doivent être mises à jour lors d'un front, montant ou descendant. Pour cela, elles remplacent l'entrée d'autorisation par une entrée qui réagit au signal d'horloge 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.

Les bascules commandées par une horloge sont appelées des bascules synchrones. Le terme anglais pour désigner les bascules synchrones est le terme flip-flops, le terme latches est utilisé uniquement pour les bascules asynchrones du chapitre précédent. Il s'agit d'une distinction qui est souvent respectée dans les documents ou livres écrits en anglais.

Les types de bascules synchrones

[modifier | modifier le wikicode]

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 où l'entrée Enable est remplacée par une entrée d'horloge. 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).

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 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 | modifier le wikicode]

Pour fabriquer une bascule synchrone, une méthode assez simple part d'une bascule non-synchrone et la modifie 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. Précisons qu'on peut faire exactement la même chose avec deux bascules asynchrones RS l'une à la suite de l'autre, ou même n'importe quelle autre bascule synchrone, mais le cas avec une bascule D est plus simple.

Negative-edge triggered master slave D flip-flop
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).

Transformer des bascules synchrones en d'autres

[modifier | modifier le wikicode]

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.

La distribution de l'horloge dans un circuit complexe

[modifier | modifier le wikicode]

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.

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.

Arbre d’horloge simple.
Arbre d'horloge avec des buffers (les triangles sur le schéma).

Une bonne partie de la consommation d'énergie a lieu dans l'arbre d'horloge. 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 | modifier le wikicode]

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. 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.

Clock skew lié aux temps de transmission dans les fils.

Le décalage d'horloge 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. Naïvement, l'arbre d'horloge part de là où se trouve la broche pour l'horloge, d'un côté du processeur. 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. Il y a quand même un délai entre le centre et les bords du processeur, mais le délai maximal est minimisé.

Il arrive que le clock skew soit utilisé volontairement pour compenser d'autres délais de transmission. Pour comprendre pourquoi, imaginons qu'un composant envoie ses données au second. Il y a 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 | modifier le wikicode]

Il existe des composants électroniques qui sont divisés en plusieurs morceaux cadencés à des fréquences différentes. 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 bien 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 | modifier le wikicode]

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 | modifier le wikicode]
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.

Enfin, il faut distinguer les registres synchrones des registres asynchrones. Les registres synchrones sont reliés au signal d’horloge. Pour cela, ils disposent d'une entrée d'horloge sur laquelle on envoie le signal d'horloge. Ils ne sont mis à jour que si on présente un front montant sur l'entrée d'horloge. Les registres asynchrones ne sont pas reliés au signal d'horloge et sont mis à jour quand on envoie ce qu'il faut sur leur entré Enable, rien de plus.

L'intérieur d'un registre simple

[modifier | modifier le wikicode]

Un registre est composé de plusieurs bascules D qui sont toutes mises à jour en même temps. Cela vaut aussi bien pour les registres asynchrones que les registres synchrones. Pour cela, toutes les entrées E des bascules sont reliées à l'entrée de commande Enable. De plus, les registre synchrones envoient le signal d'horloge sur toutes les bascules. Avec un registre synchrone, toutes les bascules sont des bascules synchrones, qui ont toutes une entrée d'horloge, relié au signal d'horloge.

Registre.

Les registres à décalage

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]
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 | modifier le wikicode]

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.

Voici ce que cela donne avec une RAM reliée à un bus de 1 bit, à savoir que chaque case mémoire ne contient que 1 bit, il y a un bit par adresse. Il s'agit d'un exemple bien trop simple pour avoir la moindre application pratique, mais c'est un exemple clairement pédagogique. L'entrée d'écriture est reliée à toutes les bascules, mais seule celle sélectionnée est écrite. Lors d'une lecture, l'adresse est envoyée au multiplexeur et la donnée lue sur sa sortie. Lors d'une écriture, c'est le démultiplexeur/décodeur qui est utilisé. Le décodeur active la bascule voulue, via son entrée d'horloge ou Enable. Le bit R/W précise qu'il faut effectuer une écriture. L'entrée d'écriture est alors recopiée dans la bascule sélectionnée.

Intérieur d'une RAM de 4 bits, reliée à un bus de 1 bit, fabriquée avec des registres et des multiplexeurs.

Les mémoire mortes et mémoires vives

[modifier | modifier le wikicode]

Les mémoires SRAM 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 très utilisées, surtout dans les processeurs. 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. Nous verrons les différents types de RAM dans les chapitres sur les mémoires, aussi nous allons mettre cela de côté pour le moment.

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. En général, les mémoires ROM conservent leur contenu quand on coupe l’alimentation électrique. Si on éteint l'ordinateur, le contenu de la ROM n'est pas perdu, il reste le même. C'est l'exact inverse de ce qu'on a avec les registres, mémoires SRAM, bascules et autres : tout est effacé quand on coupe le courant. Les mémoires RAM sont dites volatiles, alors que les mémoires ROM sont dites non-volatiles.

Les mémoires ROM

[modifier | modifier le wikicode]

Il existe deux types de mémoires ROM : les ROM non-programmables et les ROM programmables. La différence est que les premières sont fournies telles quelle et qu'on ne peut pas changer leur contenu, alors que ce n'est pas le cas pour les secondes.

Les ROM programmables sont des ROM dans lesquelles on ne peut évidemment pas écrire, mais qui 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é. 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 non-programmables sont aussi appelées des mask ROM. Elles sont utilisées dans quelques applications particulières, pour lesquelles on n'a pas besoin de changer leur contenu. 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.

L'implémentation des mémoires ROM

[modifier | modifier le wikicode]

Les mémoires ROM sont conçues, sur le même principe que les mémoires SRAM : on combine des registres avec des multiplexeurs. Il y a cependant des différences importantes, liées au fait que les écritures sont interdites. Et il y a une grosse différence suivant que la mémoire soit reprogrammable ou non.

Si la mémoire est reprogrammable, la différence principale est que les registres sont conçus de manière à ne pas être effacés quand on coupe le courant. Ils ne sont pas fabriqués avec des bascules, mais avec d'autres circuits plus complexes, à base de transistors à grille flottante. Les bascules sont remplacés par un équivalent qui se comporte de la même manière, sauf qu'on ne peut pas changer leur contenu facilement (interdiction des écritures), et que leur contenu ne s'efface pas quand on coupe le courant. Il peut y avoir d'autres différences, mais nous verrons cela dans le chapitre dédié aux mémoires ROM.

Quant aux mask ROM, leur implémentation est beaucoup plus simple. Ils sont conçus sur le même principe que les SRAM. Sauf que vu que l'écriture et la reprogrammation sont interdites, on peut retirer les démultiplexeurs utilisés pour les écritures (et la reprogrammation). Quand aux registres, ils sont remplacés en connectant directement la tension d'alimentation ou la masse sur les entrées des multiplexeurs de lecture. Là où on veut mettre un 0, on connecte la masse. Là où on veut mettre un 1, on connecte la tension d'alimentation. Le circuit obtenu se simplifie alors et peut se remplacer par un circuit composé d'un décodeur connecté à un paquet de portes OU.

Mémoire ROM simple.

L'implémentation d'une mask ROM est en réalité plus complexe sur certains points, notamment l'implémentation des portes OU, qui sont en réalité des OU câblés comme vu dans le chapitre sur les circuits imprimés. Mais nous reverrons cela dans quelques chapitres. L'important est que vous reteniez ce qu'est une mémoire ROM, qui n'est qu'un cas particulier de circuit combinatoire. Nous aurons à utiliser des mémoires ROM dans les chapitres suivants, à quelques endroits bien précis.


Les compteurs/décompteurs sont des circuits électroniques qui mémorisent un nombre et l'incrémentent à la demande. En clair, ce sont des registres améliorés afin de supporter l'incrémentation et la décrémentation. Pour donner un exemple d'utilisation, imaginez un circuit qui compte le nombre de voitures dans un parking dans la journée. Pour cela, vous allez prendre deux circuits qui détectent respectivement l'entrée ou la sortie d'une voiture, 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. Les exemples de ce type sont suffisamment nombreux pour qu'on dédie un chapitre aux compteurs.

Les compteurs/décompteurs : généralités

[modifier | modifier le wikicode]

Un compteur mémorise donc un nombre qui est incrémenté ou décrémenté au besoin. Le nombre mémorisé sera appelé le décompte dans ce qui suit. Il faut faire la différence entre les compteurs d'un côté et les décompteurs de l'autre. Les compteurs incrémentent le décompte, les décompteurs le décrémentent, les compteurs-décompteurs peuvent faire les deux, suivant ce qu'on leur demande. Le décompte est incrémenté/décrémenté d'une quantité appelée le pas du compteur. La plupart des compteurs utilisent un pas constant, qui est fixé à la création du compteur, ce qui simplifie la conception du circuit. Le cas le plus fréquent est un pas fixe de 1, à savoir que le contenu de leur registre est incrémenté/décrémenté de 1 à la demande.

Quelques compteurs ont un pas variable, ce qui sert à 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.

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 que nous allons voir encodent leur décompte en binaire normal sur bits, mais il faut savoir que d'autres compteurs utilisent le BCD, d'autre le code Gray, etc. Au passage, le nombre de bits du compteur est appelé la taille du compteur, par analogie avec les registres. 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 décompte atteint cette limite. Certains restent bloqués sur le décompte 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 | modifier le wikicode]

Les compteurs et décompteurs sont des circuits synchrones et ont donc une entrée d'horloge. Les compteurs les plus simples incrémentent leur contenu à chaque cycle d'horloge, et nous verrons un usage de ce genre de compteur dans le chapitre suivant. Mais la majorité des compteurs n'incrémente le décompte que sur demande. Pour cela, ils disposent d'une entrée Enable, similaire à celle des registres. Le décompte est incrémenté/décrémenté seulement si l'entrée Enable est à 1, mais pas si elle est à 0. L'entrée Enable est séparée de l'entrée d'horloge, le compteur/décompteur est incrémenté seulement si il y a un front sur le signal d'horloge et une entrée Enable à 1.

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.

Les compteurs ont aussi une entrée Reset qui permet de les remettre à zéro. Il y a parfois une entrée qui permet d'initialiser le compteur à une valeur par défaut, non-nulle. Par exemple, on peut initialiser le décompte à la valeur 5, ou une autre. Pour cela, le compteur dispose de deux entrées : une entrée sur laquelle envoyer le décompte initial, une entrée pour autoriser la réinitialisation. Les entrées en question sont appelées Preload Data et Preload Enable. La seconde entrée est parfois distincte de l'entrée de réinitialisation, pour permettre de réinitialiser le compteur soit à zéro, soit à la valeur voulue.

Interface d'un compteur-décompteur.

Le circuit d'un compteur : généralités

[modifier | modifier le wikicode]

Un compteur/décompteur est registre amélioré pour le rendre capable de compter/décompter. Tous les compteurs/décompteurs combinent un registre pour mémoriser le nombre, avec 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

Les compteurs modulo ont une valeur maximale qui est plus faible que la valeur maximale du registre. Ils sont construits à partir d'un compteur normal, couplé à un circuit comparateur qui remet à zéro le registre quand il atteint la valeur maximale. Par exemple, on peut imaginer un compteur modulo 6, ce qui veut dire qu'il compte de 0 à 5. Il est construit à partir d'un compteur 4 bits qui compte de 0 à 15 (donc un compteur modulo 16), mais qui est remis à 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 met à 1 l'entrée Reset si c'est le cas. Le comparateur est juste un comparateur avec une constante, que vous savez déjà fabriquer à cet endroit du cours.

Compteur modulo N.

La valeur maximale d'un compteur modulo peut être configurable. Pour cela, le compteur est associé à un registre de configuration qui mémorise la valeur maximale souhaitée. A chaque cycle d'horloge, la valeur dans le compteur est comparée au registre de configuration. Si elles sont identiques, le compteur est remis à zéro. Le compteur est associé au registre de configuration et à un comparateur qui vérifie que les deux sont égaux. Pour le moment, nous ne savons pas faire de circuits comparateurs, ce qui fait qu'on ne peut pas expliquer ce circuit plus en détail.

Compteur 4 bits à valeur maximale programmable.

Les compteurs synchrones et asynchrones

[modifier | modifier le wikicode]

Dans cette section, nous allons créer des compteurs qui incrémentent leur décompte à chaque cycle d'horloge, ou du moins quand leur Enable le permet. A ce propos, dans les schémas qui vont suivre, les entrées Enable ne sont pas représentées. Il est sous-entendu qu'il y a une entrée Enable pour tous les compteurs qui vont suivre. Il existe deux méthodes pour créer de tels compteurs  : la première donne ce qu'on appelle des compteurs asynchrones, et l'autre des compteurs synchrones.

Les compteurs asynchrones

[modifier | modifier le wikicode]

Pour comprendre comment fonctionne un compteur asynchrone, il faut regarder la séquence des premiers entiers :

  • 000 ;
  • 001 ;
  • 010 ;
  • 011 ;
  • 100 ;
  • 101 ;
  • 110 ;
  • 111.

Il faut remarquer que le bit de poids faible s'inverse à chaque cycle d'horloge. Pour les colonnes suivantes, le bit s'inverse quand le bit de la colonne précédente passe de 1 à 0, lors d'un front descendant sur la colonne précédente. Maintenant que l'on sait cela, on peut créer un compteur avec des bascules T (elles inversent leur contenu à chaque cycle d'horloge). La première colonne inverse son contenu à chaque cycle, elle correspond donc à une bascule T reliée directement à l'horloge. Les autres colonnes utilisent des bascules T activées sur front descendant.

Attention, la bascule la plus à gauche stocke le bit de poids faible, pas celui de poids fort. Cela sera pareil dans tous les schémas qui suivront.
Compteur asynchrone de 4 bits.

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. Cette implémentation permet d'ailleurs de réinitialiser le compteur à une valeur non-nulle. Pour cela, l'entrée de chaque bascule D est précédée d'un multiplexeur, qui choisit entre le bit calculé par le compteur et celui présenté sur l'entrée de ré-initialisation. Quand l'entrée Reset est activée, les multiplexeurs connectent les bascules aux bits sur l'entrée de ré-initialisation. Dans le cas contraire, le compteur fonctionne normalement, les multiplexeurs connectant l'entrée de chaque bascule à sa sortie.

Compteur asynchrone, avec initialisation.

Les compteurs synchrones

[modifier | modifier le wikicode]

Passons maintenant au compteurs synchrones. Et pour comprendre comment créer un tel compteur, reprenons la séquence d'un compteur :

  • 000
  • 001
  • 010
  • 011
  • 100
  • 101
  • 110
  • 111

Il faut remarquer que lors d'une incrémentation, un bit s'inverse quand tous les bits des colonnes précédentes valent 1. Pour implanter cela en circuit, on a besoin d'un circuit qui détermine si les bits des colonnes précédentes sont à 1. Il n'est autre qu'un simple ET entre les bits en question. Et au lieu d'utiliser une porte ET à plusieurs entrée pour chaque colonne, 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.

L’implémentation de circuit avec des bascules D nécessite, pour chaque colonne, un inverseur commandable pour inverser le bit de la bascule si besoin. L'inverseur commandable est, pour rappel, une simple XOR. Il prend en entrée la sortie de la bascule D et la sortie de la porte ET, et envoie son résultat à l'entrée de la bascule D. On peut appliquer la même logique pour un décompteur, ce qui donne un circuit presque identique, si ce n'est que les sorties des bascules doivent être inversées avant d'être envoyée à l'inverseur commandable.

Compteur synchrone avec des bascules D.

La gestion du débordement des compteurs synchrones/asynchrones

[modifier | modifier le wikicode]

Les compteurs précédents ne peuvent pas être réinitialisés, ce qui pose des problèmes, notamment pour implémenter des compteurs modulo. Pour cela, 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 connecter ensemble les entrées Reset des bascules à l'entrée Reset du compteur.

Compteur réinitialisable.

Implémenter un compteur modulo demande d'ajouter un comparateur qui détecte quand la valeur maximale est atteinte, afin de commander l'entrée de réinitialisation. Un tel circuit est juste un comparateur avec une constante, que vous savez déjà fabriquer à cet endroit du cours.

Compteur modulo 10.

Il peut être utile de prévenir quand un compteur a débordé, ce qui est utile pour fabriquer des circuits diviseurs de fréquence et des timers (qu'on verra dans le prochain chapitre). Pour cela, on ajoute une sortie de débordement 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. Pour les compteurs non-modulo, un débordement se traduit par un front descendant sur la sortie de la dernière bascule, celle qui contient le bit de poids fort.

Les compteurs en cascade

[modifier | modifier le wikicode]

Il est possible de concevoir des compteurs à partir de compteurs plus petits, mis en cascade. Par exemple, en créant un compteur 16 bits à partir de compteurs 4 bits, enchainés l'un à la suite de l'autre. Les compteurs de 4 bits peuvent même être des compteurs asynchrones, ce qui donne un compteur hybride entre synchrone et asynchrone. Les compteurs sont mis en cascade de la manière suivante : leur sortie de débordement est connectée sur l'entrée Enable du compteur suivant, celle qui déclenche l'incrémentation du compteur. La sortie de débordement est notée RCO dans les schéma qui suivent, nous verrons pourquoi dans le prochaine paragraphe.

Cascaded binary counters

Les compteurs mis en cascade ont les mêmes entrées et sorties que les compteurs normaux, avec cependant une entrée en plus, indiquant que le compteur a atteint sa valeur maximale, appelée sortie Ripple carry out (RCO). La sortie en question est calculée avec une porte ET entre tous les bits du compteur pour un compteur synchrone. Si tous les bits du compteurs sont à 1, cela signifie que le compteur suivant doit être incrémenté au prochain cycle d'horloge.

Cascadable binary up-counter

Les compteurs basés sur des registres à décalage

[modifier | modifier le wikicode]

Il existe des compteurs 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. 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 ne sont pas des registres à décalage, on les omets dans ce qui suit. Les trois types restants de registres à décalage permettent de créer des compteurs. Les compteurs en question ne portent pas de noms proprement dit, à l'exception de certains compteurs basés sur un registre SISO 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. Ils sont conçus pour dérouler une suite de nombres bien précise, prédéterminée lors de la création du compteur. En conséquence, on peut les réinitialiser, mais pas insérer une valeur dans le 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, ce qui peut servir pour fabriquer des suites de nombres pseudo-aléatoires, par exemple.

Un exemple d'utilisation est celui de 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 | modifier le wikicode]

Les compteurs basés sur des registres à décalage SISO sont aussi appelés des compteurs en anneau. Ils sont appelés ainsi car le bit sortant du registre est renvoyé sur son entrée. Précisément, 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é. La première possibilité, où le bit entrant est égal au bit sortant, donne un compteur one-hot. La seconde possibilité, où le bit sortant est inversé, donne un compteur de Johnson. Les deux sont très différents, et ne fonctionnent pas du tout pareil.

Les compteurs en anneau : les compteurs one-hot

[modifier | modifier le wikicode]

Les compteurs one-hot sont appelés ainsi, car ils permettent de compter dans une représentation des nombres appelée la représentation one-hot. Pour rappel, dans une telle représentation, un seul bit est à 1 pendant que les autres sont à 0. 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. Il est important de remarquer que dans cette représentation, le zéro est n'est PAS codé en mettant tous les bits à 0, la valeur 0000...0000 est une valeur interdite. A la place, le zéro est codé en mettant le bit de poids faible à 1. Pour N bits, on peut encoder seulement N valeurs, dont le zéro.

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

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 (0) ;
  • 0010 (1) ;
  • 0100 (2) ;
  • 1000 (3) .

Un compteur one-hot basique est composé d'un registre à décalage SISO dont on boucle la sortie sur son entrée. En faisant cela, on garantit que le registre revient à zéro lors d'un débordement, zéro étant codé avec un 1 dans le bit de poids faible. Au passage, si vous ne mettez que des 0 dans un compteur en anneau, il restera bloqué pour toujours : décaler une suite de 0 donnera la même suite de 0. Initialiser un compteur one-hot demande donc quelques subtilités qu'on détaillera plus bas.

Compteur en anneau de 4 bits

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. Par contre, ce compteur n'est pas très économe en bascules. Imaginons que l'on veut un compteur qui compte jusqu'à une valeur N arbitraire : un compteur 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 comparateurs/incrémenteurs l'est beaucoup plus.

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, parfaitement adaptés au stockage de petites valeur, et surtout : ils n'ont pas besoin de circuit comparateur pour connaitre la valeur stockée dedans. Nous n'allons pas rentrer dans le détail de leurs utilisations car nous en reparlerons dans la suite du cours.

Les compteurs de Johnson : les compteurs unaires

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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.

É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. Le terme combinaison linéaire demande quelques explications.

Pour simplifier, cela veut dire qu'on multiplie chaque bit par 0 ou 1, avant d'additionner le tout. 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 permet de remplacer les additions par une simple opération 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écalages à rétroaction affine sont identique aux précédents à une différence près : le bit calculé est inversé avant d'être inséré dans le registre. 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.

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, ils utilisent 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, ou NXOR pour leur variante affine.

Leur avantage est qu'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. 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.

Registre à décalage à rétroaction de Galois.

Il existe enfin des compteursqui 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 | modifier le wikicode]

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, il ne peut contenir qu'un nombre fini de valeurs, ce qui fait qu'il finira 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, 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. 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. Dans le meilleur des cas, le registre à décalage à rétroaction passera par toutes les valeurs que le registre peut prendre, sauf une : 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.


L'initialisation d'un compteur à rétroaction

[modifier | modifier le wikicode]

Les compteurs à rétroaction ne peuvent pas être initialisés à une valeur arbitraire, en raison de la présence d'une valeur interdite qui bloque le compteur, sans compter que les débordements d'entiers y sont impossibles. Pour ce qui est de l'initialisation, tous les compteurs basés sur un registre à décalage ne sont pas égaux : soit le compteur peut être initialisé avec zéro sans que cela pose problème, soit ce n'est pas le cas.

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

Le second cas est celuid es compteurs en anneau et des LFSR non-affine. Lors de la réinitialisation, il faut que toutes les bascules soient réinitialisées à 0, sauf une qui est mise à 1. La bascule en question doit disposer d'une 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. 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 en code Gray

[modifier | modifier le wikicode]

Il existe des compteurs qui comptent en code Gray. Pour rappel, le code Gray permet de coder des nombres d'une manière un peu différente du binaire normal. Son avantage principal est que lorsqu'on incrémente ou décrémente un nombre, seul un bit change ! Ils ont beaucoup d'avantages, qui sont tous liés à cette propriété.

L'absence d'états transitoires douteux

[modifier | modifier le wikicode]

Le premier l'absence d'état transitoires douteux. En binaire normal, lorsqu'on passe d'un nombre au suivant, plusieurs bits changent. La moyenne est d'environ deux bits, avec beaucoup de transitions de 1 bit, mais aussi quelques-unes qui en changent beaucoup plus. Le problème est que tous les bits modifiés 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 exemple typique, évoqué dans les chapitres précédents, est l'échange d'informations entre deux domaines d'horloge. Pour rappel, il arrive que deux portions d'un circuit imprimé aillent à des fréquences différences : on dit que le circuit à plusieurs domaines d'horloge. Mais il faut échanger des informations entre ces deux portions, et divers problèmes surviennent alors. Un domaine d'horloge sera plus rapide que l'autre, et il pourra voir les états transitoires invisible pour le circuit. Et par voir, on veut dire qu'il les prendra pour des états valides, et cela fera dysfonctionner le circuit. Pour éviter cela, diverses techniques de croisement de domaines d'horloge existent. Et les compteurs Gray en font partie : si un domaine d'horloge utilise la valeur d'un compteur de l'autre, mieux vaut que ce compteur soit un compteur Gray. Et cette situation est assez fréquente !

La consommation énergétique du compteur Gray

[modifier | modifier le wikicode]

Un autre point est que la consommation d'énergie de ces compteurs est bien plus réduite qu'avec un compteur normal. Rappelons que pour fonctionner, les circuits électroniques consomment un peu d'électricité. Un compteur ne fait pas exception. Et la majeure partie de cette consommation sert à changer l'état des portes logiques. Faire passer un bit de 0 à 1 ou de 1 à 0 consomme beaucoup plus d'énergie que de laisser un bit inchangé. Ce qui fait que quand un compteur est incrémenté ou décrémenté, cela consomme un peu d'énergie électrique. Et les conséquences de cela sont nombreuses.

Premièrement, plus on change de bits, plus la consommation est forte. Or, comme on l'a dit plus haut, la moyenne pour un compteur binaire normal est de 2 bits changés par incrémentation/décrémentation, contre un seul pour un compteur Gray. Un compteur Gray consomme donc deux fois moins d'énergie. En soi, cela n'est pas grand-chose, un compteur consomme généralement peu. Mais l'avantage est que cela va avoir des effets en cascade sur les circuits qui suivent ce compteur. Si l'entrée de ces circuits ne change que d'un seul bit, alors leur état changera moins que si c'était deux bits. Les circuits qui suivent vont donc moins consommer.

Un autre avantage en matière de consommation énergétique est lié justement au point précédent, sur les transitions d'état douteux. Les circuits connectés au compteur vont voir ces transitions d'état douteux : ils vont réagir à ces entrées de transition et modifier leur état interne en réaction. Bien sur, l'état final correct fera de même, ce qui effacera ces états transitoires intermédiaires. Mais chaque état intermédiaire transitoire correspondra à un changement d'état, donc à une consommation d'énergie. En supprimant ces états transitoires, on réduit fortement la consommation d'énergie du circuit. Cela vaut pour le compteur Gray lui-même, mais aussi sur tous les circuits qui ont ce compteur comme entrée !


Les compteurs servent à créer divers circuits fortement liés la gestion de la fréquence, ainsi qu'à la mesure du temps. Les circuits en question comptent les cycles d'horloge, ce qui demande juste des compteurs automatiquement incrémentés à chaque cycle d'horloge. Compter les cycles d'horloge permet de mesurer des durées, ou de diviser une fréquence. Dans ce qui va suivre, nous allons voire deux types de circuits : les diviseurs de fréquence, et les timers.

Les diviseurs de fréquence

[modifier | modifier le wikicode]

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 N fois 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. Il existe des diviseurs de fréquence qui divisent la fréquence par 2, d'autres par 4, d'autres par 13, etc.

Les diviseurs de fréquence basés sur des compteurs

[modifier | modifier le wikicode]

Leur implémentation est simple : il suffit d'un compteur auquel on rajoute une sortie. 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. 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 : 4, 6, 8, 10, 12, etc. Or, les compteurs en anneau, sont particulièrement adaptés pour compter jusqu'à des valeurs assez faibles. Il est donc naturel d'utiliser un compteur en anneau dans un diviseur de fréquence. 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 portes logiques, sont très rapides, prennent peu de place, permettent de se passer de circuit comparateur.

Les diviseurs de fréquence basés sur des compteurs à bascule T

[modifier | modifier le wikicode]

Il est aussi possible de concevoir des diviseurs de fréquence avec des bascules T. La seule contrainte est qu'il faut diviser la fréquence d'entrée par une puissance de deux. Le cas le plus simple divise par 2 la fréquence d'entrée et ne demande qu'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. 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.

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, aussi appelés Programmable interval timer, sont des circuits capables de compter des durées, exprimées en cycles d'horloge. Leur fonctionnement est assez simple : ils émettent un signal quand un certain nombre de cycles est écoulé, ce nombre de cycles étant configurable. On peut ainsi générer un signal qui surviendra après 50 cycles d'horloge, ou après 100 cycles d'horloge, etc. 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.

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. Le nombre de cycles en question est envoyé sur une entrée d’initialisation du compteur.

Les timers matériels peuvent compter de deux manières différentes, appelées mode une fois et mode périodique.

  • 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 | modifier le wikicode]

Le watchdog timer est un timer spécifique dont le but est de redémarrer automatiquement l'ordinateur si jamais celui-ci ne répond plus ou plante. 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 | modifier le wikicode]

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 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, qui cadencent ce timer à la fréquence du processeur mais utilisent des mécanismes de synchronisation assez complexes pour corriger l'effet de la fréquence variable.

L'horloge temps réel

[modifier | modifier le wikicode]

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, l'horloge temps réel sa fréquence n'aide pas : 1024 Hz est proche de 1000, mais pas assez pour faire des mesures à la milliseconde près, chose qui est nécessaire pour mesurer le ping ou d'autres choses utiles. A la place, l'ordinateur l'utilise pour que l'ordinateur soit toujours à l'heure. L'ordinateur sait quelle heure il est avec une précision de l'ordre de la seconde (vous pouvez regarder le bureau de Windows dans le coin inférieur droite de votre écran pour vous en convaincre).

Le Programmable Interval Timer : l'Intel 8253

[modifier | modifier le wikicode]

L'Intel 8253 était un timer programmable autrefois soudé sur les cartes mères des premiers PC. Il fût suivi par l'Intel 8254, qui en était une légère amélioration. Il était 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. S'il n'est plus présent dans un boitier sur 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é. 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 0, 1 et 2. Pour chaque compteur, 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. Les trois compteurs étaient utilisés pour dériver plusieurs fréquences allant de 18,2 Hz à environ 500 KHz. Par exemple, il é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.

Intel 8253 and 8254

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é. Il y a aussi 5 entrées de configuration :

  • Deux bits A0 et A1 pour sélectionner le compteur voulu avec son numéro.
  • Un bit RD à mettre à 0 pour que l'ordinateur récupère le compteur sélectionné sur le port de 8 bits.
  • Un bit WR à mettre à 0 pour que l'ordinateur modifie le compteur sélectionné, 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.

L'Intel 8253 intégre un registre de 8 bits, le Control Word register qui mémorise la configuration de l'Intel 8253. Pour programmer les trois compteurs, il faut écrire un mot de 8 bits dans ce Control Word register. Pour écrire dans le Control Word register, il faut mettre le bit CS à 0 (on active l'Intel 8253), mettre le bit RDà 1 , le bit WR à 0 le bit WR (on indique qu'on fait une écriture), sélectionner le Control Word register en mettant les deux bits A0 et A1 à 1, puis envoyer la configuration du Control Word register sur le port de 8 bits.

Le High Precision Event Timer (HPET)

[modifier | modifier le wikicode]

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 gère nativement plusieurs valeurs limites à laquelle générer un signal, qui sont configurables. Pour cela, il est couplé à plusieurs comparateurs, chacun associé à un registre pour mémoriser la valeur limite. Il doit y avoir au moins trois comparateurs/registres, mais le nombre peut monter jusqu’à 256.

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 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 é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 premier comparateur fournisse la fréquence de 1024 Hz de la RTC, 3 autres comparateurs 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. Chaque comparateur doit pouvoir fonctionner en mode une fois, et au moins un comparateur doit pouvoir fonctionner en mode périodique. Aussi, il contient aussi 3 registres de configuration. Notons qu'il est aussi possible de lire ou écrire dans le compteur de 64 bits, mais ce n'est pas recommandé.

La génération de nombres pseudo-aléatoires

[modifier | modifier le wikicode]

Les compteurs peuvent aussi être utilisés pour générer des nombres "aléatoires". Je dis aléatoires entre guillemets car ils ne sont pas vraiment aléatoires, mais s'en rapprochent suffisamment pour être considérés comme tels. Pour mettre en avant cela, on parle aussi de nombres "pseudo-aléatoires". De nombreuses situations demandent de générer des nombres pseudo-aléatoire 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.

Les méthodes que nous allons voir produisent un nombre pseudo-aléatoire un bit à la fois, à quelques exceptions près. Les circuits que nous allons voir fournissent un bit sur leur sortie et ce bit varie de manière assez aléatoire. Les bits en sortie du circuit sont accumulés dans un registre à décalage normal, pour former un nombre aléatoire. Nous appellerons ce registre : l'accumulateur.

L'usage de registres à décalage à rétroaction

[modifier | modifier le wikicode]
Nonlinear-combo-generator

La première solution utilise des registres à décalages à rétroaction, les fameux LSFR du chapitre précédent. Un LSFR seul ne fournit pas un aléatoire digne de ce nom, mais il est possible de 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.

Exemple avec trois LSFR différents, de taille différentes : le bit envoyé à l'accumulateur est un XOR du bit sortant des trois LSFR.

Pour rendre le tout encore plus aléatoire, il est possible de cadencer les LSFR à des fréquences différentes. Cette technique est utilisée dans les générateurs stop-and-go, alternative step, et à shrinking.

  • Le générateur alternative step utilise trois LSFR. Le premier commande un multiplexeur qui choisit la sortie parmi les deux restants.
  • Le générateur stop-and-go utilise deux LSFR. Le premier est relié à l'entrée d'horloge du second et 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.
  • Le shrinking generator utilise deux LSFR 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, le bit de sortie du second registre est oublié.

L'aléatoire généré par des timers ou des compteurs d'horloge

[modifier | modifier le wikicode]

Au-delà des LSFR, il est possible d'utiliser des compteurs pour générer du pseudo-aléatoire. Par exemple, une technique très simple utilise un simple timer. Si on a besoin d'un nombre pseudo-aléatoire, il suffit de lire le timer et d'utiliser le nombre lu comme nombre pseudo-aléatoire. Si le délai entre deux demandes est irrégulier, le résultat semblera 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 la plus simple utilise 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 son œuvre, les deux horloges seront très légèrement désynchronisées 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 , il vaut 0 si ce nombre est pair. 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 | modifier le wikicode]

Il existe d'autres solutions matérielles qui utilisent le bruit thermique. Tous les circuits électroniques de l'univers 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 métalliques des 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. Ce principe a été utilisé sur des anciens processeurs Intel qui géraient l'instruction RDRAND, une instruction qui produisait un nombre aléatoire.


Les circuits de calcul et de comparaison

[modifier | modifier le wikicode]

Dans ce chapitre, nous allons voir des opérations appelées les décalages et les rotations. Nous allons voir ce que sont ces opérations, puis les nombreux circuits qui permettent d'implémenter ces opérations. Mais expliquons d'abord les différentes opérations de décalage et de rotation.

Les opérations de décalage

[modifier | modifier le wikicode]

Les décalages décalent un nombre de un ou plusieurs rangs vers la gauche, ou la droite. 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 ci-contre. L'exemple montre le décalage de deux rangs vers la droite, d'un opérande de 8 bits valant 01011101. On obtient 010111 : les deux bits de poids forts sont vides et les deux bits de poids faible (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. Si on décale de n rangs, alors cela laissera n vides et fera sortir n bits. Ces deux points, la gestion des vides et des bits sortants, sont assez liés.

Le différents types de décalages

[modifier | modifier le wikicode]

Au-delà de la distinction assez intuitive entre les décalages vers la gauche et vers la droite, 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 de l'opérande décalé (qui est supposé être signé), contrairement aux autres décalages. 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 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 et il se produit une forme particulière de débordement d'entier.

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 seulement 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 préserve pas le bit de signe.

Modulo et quotient d'une division par une puissance de deux en binaire

Les arrondis lors des décalages

[modifier | modifier le wikicode]

Les décalages à droite entraînent l'apparition d'arrondis. Lorsqu'on effectue un décalage à droite, les bits qui sortent du résultat sont 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. Vu que les bits sortants sont simplement éliminés, on pourrait croire que cela correspond à un arrondi vers zéro (vers la valeur inférieure). C'est bien le cas 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, tout dépend de la représentation binaire utilisée. L'arrondi se fait bien vers zéro en complément à 1, mais pas en complément à deux, où l'arrondi se fait à la valeur inférieure, vers moins l'infini.

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.

Les débordements d'entiers lors des décalages

[modifier | modifier le wikicode]

Les décalages peuvent aussi causer des débordements d'entier. Pour rappel un débordement d'entier est une situation 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. Par exemple supposons que l'ordinateur sur lequel vous travailler 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 : si au moins un bit non-nul sorte à gauche, c'est un débordement d'entier.

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ébordement et 0 sinon (ou l'inverse). Une autre solution est de corriger le débordement, mais elle est utilisée uniquement pour les opérations arithmétiques, pas 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. 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 | modifier le wikicode]
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 décaleur. Pour cela, 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 l'idée jusqu'au bout, 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. Les décaleurs élémentaires 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. Le énième bit du nombre de rang servira à actionner le décaleur par 2^n.

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 | modifier le wikicode]

Commençons par étudier le cas du décalage logique par 4 rangs à droite. 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.

Exemple d'un décaleur par 4.

Le tout peut être adapté pour créer des décaleurs par 1, par 2, par 8, etc. Il suffit de faire la même chose pour tous les autres bits, et le tour est joué. 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 | modifier le wikicode]

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ù c'était le 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 | modifier le wikicode]

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 | modifier le wikicode]
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 cette section, nous allons nous concentrer sur les barrel shifters unidirectionnels, 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.

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 | modifier le wikicode]

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.

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.

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

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 | modifier le wikicode]

Les mask barrel shifters sont des décaleurs-rotateurs basés autour d'un rotateur, qui est modifié afin de supporter les décalages logiques/arithmétiques. 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.

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 qui est calculé par un circuit dédié. Le circuit de calcul du masque est un encodeur modifié, qu'on peut concevoir avec les techniques des chapitres précédents.

Le circuit qui combine le masque avec le résultat de la rotation 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

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. 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. 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.

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.

Pour le moment, ce détail 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 une 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 à droitepar une rotation à gauche équivalente (ou inversement, mais nous allons supposer que le rotateur fait des rotations vers la gauche). 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 . Pour résumer, on a :

Ls 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. 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.

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.


Dans ce chapitre, nous allons voir les circuits capables de faire une addition ou une soustraction, ainsi que quelques circuits spécialisés. Précisons cependant que les fabricants de processeurs 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 circuits 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 | modifier le wikicode]

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 d'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. 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 et l'additionneur complet

[modifier | modifier le wikicode]

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.

Demi-addtionneur.
Demi-addtionneur.
Circuit d'un demi-addtionneur.
Circuit d'un demi-addtionneur.
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. Il fournit en sortie deux bits : un bit de somme et une retenue sortante. 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 Bit de somme
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'utiliser un tableau de Karnaugh pour traduire la table de vérité, mais elle donne un résultat légèrement sous-optimal. D'autres méthodes donnent des résultats plus compréhensibles. Nous allons les détailler dans ce qui suit.

L'additionneur complet conçu avec deux demi-additionneurs

[modifier | modifier le wikicode]

La solution 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.

Le circuit de calcul de la retenue peut être remplacé par une porte à majorité, mais cette possibilité n'est presque jamais utilisée, on lui préfère le circuit à trois portes logiques.

Additionneur crée avec une porte à majorité

Les autres implémentations 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 retenue doit absolument être le plus rapide possible,vu que la propagation des retenues est le point limitant pour les performances d'un additionneur.

L'additionneur complet basé sur la propagation et la génération de retenue

[modifier | modifier le wikicode]

Le circuit précédent est basé sur deux additions 2-bits successives : une première pour additionner deux bits d'opérande, une seconde pour additionner la retenue. Mais il existe une autre façon de faire l'addition, qui est terriblement importante pour la suite du cours. L'idée est de regarder ce que vaut la retenue sortante, en fonction de la retenue entrante. Pour cela, reprenons la table de vérité de l'additionneur complet.

Dans la majorité des cas, la retenue sortante est égale à la retenue entrante. On dit que la retenue entrante est propagée sur la sortie de retenue. Cependant, il y a aussi deux cas où la retenue n'est pas propagée : celui où la retenue sortante est forcée à 1, et celui où elle est forcée à 0. Dans le premier cas, l'addition donne une retenue à 1, quelle que soit la retenue envoyée en entrée (sous-entendu, même si celle-ci vaut 0). On dit que la retenue sortante est générée. Dans le cas inverse, la retenue sortante est forcée à 0, peu importe la retenue entrante. On dit que la retenue entrante est absorbée.

Il y a cependant une petite ambiguïté à dire que la retenue a été propagée, absorbée ou générée. En effet, prenons le cas où la retenue sortante et entrantes valent toutes deux 0 : est-ce que la retenue a été propagée ou bien absorbée, ou les deux ? Idem quand les deux retenues sont à 1. Il y a un choix arbitraire à faire dans ce genre de cas, pour la plupart des lignes de la table de vérité. Cependant, il y a un choix bien précis qui est supérieur aux autres, et c'est celui qui est présenté dans le tableau suivant. Les lignes rouge correspondent à une retenue propagée, celles en bleu à une retenue absorbée, celle en vert à une retenue générée.

Retenue entrante Opérande 1 Opérande 2 Retenue sortante Bit de somme
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

Avec ce choix, on peut déterminer si la retenue est propagée, absorbée ou générée, sans tenir compte de la retenue elle-même. On peut déterminer dans quel cas on est seulement en regardant les bits d'opérandes nommés A et B.

  • La retenue est propagée si les deux bits d'opérande sont différents.
  • La retenue est générée si les deux bits d'opérande sont à 1.
  • La retenue est absorbée si les deux bits d'opérande sont à 0.

L'additionneur que nous allons voir détermine si la retenue est propagée, absorbée ou générée, et calcule la retenue sortante en fonction de ça. Il génère deux bits, nommés P et G : P pour Propagate, G pour Generate. Le bit P indique que la retenue entrante doit être propagée ou non : il est mis à 1 pour propager la retenue entrante, à 0 si elle ne doit pas être propagée. Le bit G indique si une retenue a été générée ou non : 1 si une retenue générée, 0 sinon. Une retenue est considérée comme absorbée si elle n'est pas ni propagée ni générée, pas besoin d'un troisième bit pour gérer ce cas.

Pour rappel, la retenue est propagée si les deux bits sont différents, n'est pas propagée s'ils sont identiques. Déterminer si deux bits sont identiques ou différents est le comportement d'une banale porte XOR. Le bit P est donc généré par une simple porte XOR. Quant au bit G, il est à 1 si les deux bits d'opérandes sont à 1, ce qui correspond à une porte ET.

Il existe des pseudo-additionneurs qui ne calculent pas la retenue sortante et fournissent à la place les signaux P et G, en plus du résultat. Un tel additionneur est appelé un additionneur P/G (P/G pour propagation/génération). Ils sont très utiles pour créer des additionneurs dits "à anticipation de retenue", comme on le verra dans la suite du chapitre.

Additionneur P/G : entrées et sorties. Additionneur P/G : circuit de génération des signaux P et G.

Pour créer un additionneur complet avec cette méthode, il faut ajouter un circuit qui calcule la retenue sortante à partir des bits P et G. La retenue finale 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. La traduction en équation logique; puis en circuits, donne un circuit strictement identique à celui basé sur deux demi-additionneurs... Vous remarquerez que les signaux P et G sont calculés par le premier demi-additionneur.

Additionneur complet avec propagation et génération de retenue.

Une méthode alternative donne cependant un circuit différent. Le circuit en question choisit entre les deux situations : soit il propage la retenue, soit il calcule la retenue adéquate. Propager une retenue demande de connecter l'entrée de retenue sur la sortie de retenue. Mais cela ne doit être fait que si les conditions sont réunies, que si la retenue est belle et bien propagée. Si ce n'est pas le cas, il faut connecter la sortie de retenue à un circuit qui calcule la retenue adéquate. Pour cela, on utilise un multiplexeur, commandé par le bit P.

Additionneur crée avec un multiplexeur

Quand la retenue entrante n'est pas propagée, la retenue sortante vaut 1 si une retenue est générée, 0 sinon. Le circuit qui calcule la retenue doit donc fournir un 0 si les bits d'opérande valent tous les deux 0, un 1 s'ils valent tous les deux 1. Mais si la retenue est propagée, la retenue calculée peut prendre n'importe quelle valeur, vu que le multiplexeur ne choisira pas sa sortie. Suivant quelles valeurs on prend dans ce cas, le circuit obtenu sera différent. Si on suppose que le circuit fournit un 0 si la retenue est propagée, alors la retenue calculée indique une retenue est générée ou non : on peut alors réutiliser le bit G ! Le tout donne alors ce circuit :

Additionneur complet basé sur un MUX

Le circuit semble utiliser plus de portes logiques que nécessaires. Cependant, tout dépend de l'implémentation du multiplexeur. En réalité, nous verrons dans quelques chapitres qu'il est possible d'implémenter un multiplexeur avec seulement 6 transistors. L'implémentation utilise des portes à transmission, mais nous en reparlerons dans le chapitre sur les transistors, quand nous verrons les additionneurs à Manchester Carry Chain. Au passage, une variante de ce circuit a été utilisée dans le processeur processeur 8086 d'Intel, comme on le verra dans le chapitre suivant.

L'additionneur complet basé sur une modification de la retenue sortante

[modifier | modifier le wikicode]

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. L'unité de calcul de l'Intel 4004 et de l'Intel 8008 faisaient autrement : le bit du résultat était calculé à 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 ces deux cas, le bit du résultat vaut 0, quelle que 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 un circuit qui vérifie si tous les bits opérande valent 0, un autre s'ils valent tous 1. 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. Notons qu'on peut encore optimiser le circuit en fusionnant les deux portes NOR entre elles, mais c'est là un détail.

Full adder basé sur une modification de la retenue

À ce stade, vous êtes certainement étonné qu'un tel circuit ait existé. Il utilise beaucoup de portes logiques, a une profondeur logique supérieure : il n'a rien d'avantageux. Sauf qu'il était utilisé sur d'anciens processeurs, qui utilisaient la technologie dite TTL, différente de la technologie CMOS des transistors modernes. Et avec la technologie TTL, il est possible de fusionner plusieurs portes logiques ET et NOR en une seule porte logique ET/OU/NON ! Un additionneur complet construit ainsi ne prenait que deux portes logiques : une pour le calcul de la retenue sortante, une autre pour le reste du circuit.

L'addition non signée

[modifier | modifier le wikicode]

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. L'addition des nombres codés en complètement à deux sera vu dans une section ultérieure.

L'additionneur série

[modifier | modifier le wikicode]

Il est possible d'additionner deux nombres bit par bit,colonne par colonne, avec un additionneur complet. Cela demande de coupler un additionneur complet avec plusieurs registres à décalages. Les opérandes sont placées chacune dans un registre à décalage, afin de passer d'un bit au suivant, d'une colonne à la suivante, à chaque cycle. Même chose pour le résultat, qui a sont propre registre à décalage. 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.

L'additionneur série a été utilisé sur d'anciens prototypes dans les années 50-60, et quelques ordinateurs commerciaux très rares.

L'additionneur à propagation de retenue

[modifier | modifier le wikicode]

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. 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).

Additionneur à propagation de retenue.

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.

L'avantage est qu'il utilise très peu de portes logiques et est assez économe en transistors, ce qui fait qu'il était utilisé sur certains processeurs 8 et 16 bits assez anciens. Bien que très simple, 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. La raison 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.

L'addition étant une opération fréquente, il vaut mieux utiliser d'autres méthodes d'addition, plus rapides. Pour cela, les autres additionneurs utilisent diverses optimisations : calculer les retenues en parallèle, éliminer certaines opérations inutiles quand c'est possible, accélérer le calcul de la retenue avec des techniques d'anticipation de retenue, etc. Mais ces optimisations demandent d'utiliser plus de circuits, quitte à gagner quelque peu en rapidité. Si on met de côté les additionneurs de type Manchester carry chain, qu'on ne peut pas encore expliquer à ce stade du cours, il existe plusieurs solutions, qui donnent respectivement les additionneurs à saut de retenue, à sélection de retenue, et à anticipation de retenue. Nous allons les voir dans les sections suivantes.

Les accélérations de la propagation de retenue

[modifier | modifier le wikicode]
Additionneur 4 bits, un bloc.

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. 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 un morceau des deux opérandes à additionner, mais aussi une retenue d'entrée. Il fournit en sortie un résultat codé sur 4/5 bits, mais aussi une retenue sortante.

Dans un bloc, la retenue sortante est plus ou moins calculée à part du résultat. L'enjeu est de calculer la retenue sortante d'un bloc rapidement, plus rapidement qu'un additionneur à propagation de retenue. Le calcul du résultat n'a pas besoin d'être accéléré, on garde des additionneurs à propagation de retenue. En enchaînant plusieurs blocs les uns à la suite des autres, la retenue sortante d'un bloc est connectée sur l'entrée de retenue du bloc suivant, la retenue est propagée d'un bloc au suivant.

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 | modifier le wikicode]
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é d'un additionneur proprement dit, 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 et d'utiliser les techniques du chapitre sur les circuits combinatoires. 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 calcule l'addition en propageant les retenues. 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.

L'additionneur à saut de retenue

[modifier | modifier le wikicode]

L'additionneur à saut de retenue (carry-skip adder) est un additionneur dont le temps de calcul est variable. 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. Mais dans 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 est lui aussi composé de blocs qui additionnent 4/5 bits. Il peut, sous certaines conditions, sauter complètement la propagation de la retenue dans le bloc. L'idée est de calculer si un bloc génère une retenue sortante, ou si la retenue entrante est simplement propagée. Dans le second cas, le bloc ne fait que recopier la retenue entrante sur la sortie de retenue. La propagation de retenue entre blocs 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 d'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 | modifier le wikicode]

L'additionneur à sélection de retenue utilise aussi des blocs, comme les additionneurs précédents. 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 | modifier le wikicode]

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 | modifier le wikicode]

Les additionneurs à calcul parallèle de préfixes sont des additionneurs à anticipation de retenue améliorés pour gagner en performances. Les additionneurs à anticipation de retenue générent des signaux propagate et generate pour un bit, sous-entedu 1 bit par opérande. L'optimisation apportée est de générer des signaux propagate et generate pour un bit, mais aussi pour des groupes de 2, 3, 4, ..., N bits. Par exemple, il est possible de générer un signal P 0 vers 7, qui précise si la retenue de la seconde colonne est propagée jusqu'à la 7ème colonne ou non. Un autre exemple est un signal de génération qui indique si les colonnes 4 à 7 génèrent une retenue ou non.

En clair, les signaux P et G ont maintenant un intervalle, qui précise de quelle colonne vers quelle colonne se fait la propagation, ou entre quelles colonnes se fait la génération. De plus, les signaux pour un intervalle peuvent se calculer en combinant les signaux pour des intervalles plus restreints. Par exemple, pour calculer P pour les colonnes 0 à 10 peuvent se calculer à partir des deux signaux P des colonnes 0-4 et 5-10.

Néanmoins, il y a plusieurs manières pour subdiviser les intervalles en intervalles plus petits et combiner le tout. Et elles donnent chacune des additionneurs différent, comme l'additionneur de Ladner-Fisher, l'additionneur de Brent-Kung, l'additionneur de Kogge-Stone, ou tout design hybride. Ils ont des caractéristiques différentes. 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. Les autres ont des performances un peu plus variables, mais utilisent plus de portes logiques.

Additionneur de Kogge-Stone.
Additionneur de Ladner-Fisher.
Additionneur de Kogge-Stone pour 4 bits.

L'additionneur Kogge-Stone est illustré ci-contre. Il est composé de plusieurs couches de portes logiques. La toute première calcule les signaux P et G pour chaque colonne, comme le ferait un additionneur à anticipation de retenue. Il s'agit de la couche en rouge dans le schéma ci-dessous. Les circuits en jaune combinent ces signaux de manière à calculer les signaux P et G pour plusieurs colonnes. En vert, les circuits calculent la retenue finale.

Voici le circuit pour 8 bits :

Additionneur de Kogge-Stone pour 8 bits.

L'addition signée et la soustraction

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 différence de l'addition, 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 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. Il ressemble beaucoup à un demi-additionneur, les différences se résumant à une porte NON ajoutée pour le calcul de la retenue.

Demi-soustracteur.

Comme pour l'additionneur, seux demi-soustracteurs peuvent être combinés pour donner un soustracteur complet. Le calcul de la retenue se fait en combinant les deux retenues des demi-soustracteurs avec une porte OU. Les soustracteurs complets sont utilisés pour créer des soustracteurs à propagation de retenue ou tout autre circuit soustracteur, sur le même modèle que les additionneurs.

Soustracteur complet.

Il est possible de créer un circuit capable de faire à la fois des additions et des soustractions. Il suffit de modifier les additionneurs complets pour qu'ils supportent la soustraction. Concrètement, la seule différence est la présence des deux portes NON dans le schéma précédent : ils sont absents sur un additionneur complet. Une modification simple remplace ces deux portes NON par deux inverseurs commandable. Cependant, il y a une meilleure manière de faire, qu'on va détailler dans ce qui suit.

Additionneur-soustracteur complet

L'additionneur-soustracteur pour opérandes codées en complément à deux

[modifier | modifier le wikicode]

Étudions 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 | modifier le wikicode]

Passons maintenant aux nombres codés en signe-valeur absolue, les deux opérandes étant notées 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). Une astuce est que 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, reste à corriger le résultat. Il suffit de lui ajouter un inverseur commandable pour obtenir le circuit d'addition finale.

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 (A + B et −A − B) ne pose aucun problème, autant les soustractions posent problème (A − B et −A + B). Suivant que ou que , le signe du résultat ne sera pas le même. Déterminer le signe du résultat se fait en regardant les bits de débordement d'entier, comme on le verra plus bas.

L'additionneur-soustracteur pour opérandes codées en représentation par excès

[modifier | modifier le wikicode]

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'additionneur BCD

[modifier | modifier le wikicode]

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 | modifier le wikicode]

Nous allons commencer par voir un additionneur qui additionne deux chiffres en BCD, une sorte d'équivalent BCD de l'additionneur complet. 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). Les deux opérandes sont des chiffres BCD codés sur 4 bits et sont additionnés en binaire par un additionneur des plus normaux, similaire à ceux vus plus haut. Le résultat est alors un entier codé en binaire, sur 5 bits, qu'on corrige/convertit pour obtenir un chiffre BCD et une retenue sortante.

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 un circuit diviseur par 10 utilise beaucoup de portes logiques, ce qui ne vaut pas le coup. 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, qui calcule 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, en combinant le comparateur avec 10, 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 choix 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 le bit calculé par le comparateur.

Additionneur BCD, seconde version.

Une version alternative du circuit précédent est la suivante. Il contient deux additionneurs : un pour additionner les deux chiffres BCD, un autre pour additionner 6 si besoin. Le résultat du comparateur est directement utilisé pour générer l'opérande du second additionneur : 0 ou 6. Le circuit est simple à concevoir, mais gaspille beaucoup de circuit. Idéalement, il vaudrait mieux utiliser un circuit combinatoire d'addition avec une constante.

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. Au final, l'additionneur BCD est beaucoup plus compliqué qu'un additionneur normal, car il rajoute un comparateur ">9", un petit additionneur pour ajouter 6 et éventuellement d'autres circuits. 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.

La soustraction en BCD se fait comme en binaire : le nombre à soustraire est remplacé par son complément, le circuit additionne le complément et l'autre opérande, le débordement d'entier fait que le résultat marche. Sauf qu'ici, le complément est un complément à 9. Il se calcule chiffre par chiffre : chaque chiffre est remplacé par (9 - le chiffre en question).

L'additionneur BCD par ajustement décimal

[modifier | modifier le wikicode]

L'additionneur BCD précédent effectuait son travail chiffre BCD par chiffre BCD, mais il existe des additionneurs BCD qui font autrement. Sur les premiers processeurs x86, il n'y avait pas d'opération d'addition BCD proprement dit, seulement une addition binaire normale de 8, 16 ou 32 bits. Par contre, elle était secondée par une opération dite d'ajustement décimal qui transformait un nombre binaire en nombre codé en BCD. L'opération d'ajustement décimal prenait un opérande de 8 bits codé en binaire et fournissait un résultat de la même taille, c'est à dire deux chiffres 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'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 6, à 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. L'opération d'ajustement décimal calcule automatiquement la constante. Elle découpe l'opérande en nibbles, vérifie si chaque nibble est supérieur ou égal à 10, puis 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

L'additionneur biquinaire

[modifier | modifier le wikicode]

Les entiers BCD ne sont qu'un des encodages hybrides entre décimal et binaire. L'encodage biquinaire est l'un d'entre eux et nous allons faire un rappel rapide à ce sujet. Pour simplifier, un chiffre encodé en biquinaire est composé de deux parties : un bit, couplé à une partie quinaire encodée en représentation one-hot. La partie quinaire encode un nombre allant de 0 à 4, ce qui prend 5 bits (0, 1, 2, 3 et 4). Le bit indique s'il faut ou non ajouter 5 à la valeur encodée par la partie quinaire. Ainsi, on peut coder tous les nombres de 0 à 9.

Additionner deux nombres de biquinaire demande donc d'additionner deux parties quinaires encodées en one-hot et d'additionner deux bits. Mais attention : il faut tenir compte de la retenue de l'addition des parties quinaires. Et idéalement, il faut aussi tenir compte d'une retenue entrante, provenant de l'addition de la colonne de chiffres précédente. Toute la difficulté vient de la création de l'additionneur one-hot. Heureusement, vu qu'il n'y a que 4-5 bits à additionner, il est souvent fabriqué à partir de sa table de vérité.

Additionneur bi-quinaire

Un avantage du biquinaire est que le calcul du complément à 9 est très simple. Il faut pour cela : inverser la partie binaire avec une porte NON, puis inverser l'ordre des bits de la partie quinaire. Concrètement, le bit de poids faible devient le bit de poids fort, et ainsi de suite. Par exemple, une partie quinaire 01000 devient 00010, 10000 devient 00001, 00100 ne change pas, etc. Le tout peut se calculer avec une porte NON et 5 multiplexeurs.

L'additionneur BCD avec calculs intermédiaires en biquinaire

[modifier | modifier le wikicode]

L'ordinateur IBM 1401, un ancien mainframe des années 60, utilisait un additionneur BCD un peu particulier. Les nombres étaient encodés en BCD dans la mémoire de l'ordinateur, mais les circuits de calcul utilisaient la représentation biquinaire. Lors d'un calcul, le processeur de l'ordinateur traduisait les chiffres BCD en représentation biquinaire, faisait une addition en biquinaire, avant de traduire le résultat en BCD normal.

Pour être précis, l'IBM 1401 utilisait une variante du biquinaire. L'encodage biquinaire de l'IBM 1401 est le suivant : la partie binaire disait si le chiffre était pair ou non, la partie quinaire encodait les valeurs 0, 2, 4, 6 et 8. Le chiffre se calculait en additionnant la partie binaire (0 ou 1) au nombre pair encodé par la partie quinaire. Si l'IBM 1401 utilisait cette variante du biquinaire, c'est car elle donnait des circuits de conversion BCD-biquinaire plus économes en portes logiques et plus rapides.

La partie binaire est le bit de poids faible du chiffre BCD, la partie biquinaire est calculée par un simple décodeur qui prend en entrée le chiffre BCD, amputé de son bit de poids faible. La traduction inverse demande d'utiliser un encodeur, à la place du décodeur. Par contre, le circuit d'addition biquinaire était plus compliqué du fait de la gestion des retenues. L'addition des parties binaires et quinaires se faisait en parallèle, dans deux additionneurs séparés. Cependant, l'addition des parties binaire fournit une retenue, qu'il faut prendre en compte. Pour cela, l'IBM 1401 disposait d'un troisième additionneur qui fournissait le résultat final, encodé en biquinaire.

Additionneur biquinaire de l'IBM 1401

Une implémentation moderne demanderait d'utiliser des portes ET combinées à des portes OU, le circuit pouvant être construit simplement à partir de sa table de vérité. Sur l'IBM 1401, le circuit était cependant différent, en raison de l'utilisation de OU câblés, des croisements de fils qui fonctionnent comme des portes OU, que nous n'avons pas encore vu pour le moment, mais qui seront détaillés dans quelques chapitres. Les OU câblés étaient utilisés pour simplifier le design du circuit, mais demandaient des portes logiques spécifiques, ce qui collait avec le fait que ce mainframe utilisait des transistors en Germanium. L'implémentation exacte est décrite dans cet article de blog, mais je ne recommande sa lecture qu'à ceux qui savent ce qu'est un OU câblé :

L'incrémenteur

[modifier | modifier le wikicode]

L'incrémenteur est un circuit capable d'incrémenter un nombre. De tels circuits é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. Le circuit incrémenteur se construit sur la même base qu'un additionneur, qu'on simplifie. 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 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 | modifier le wikicode]

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.

Quelques incrémenteurs permettent cependant de configurer l'entrée de retenue de l'incrémenteur. Il est ainsi possible de la mettre à 0 ou à 1, ce qui effectue soit : une opération identité (l'opérande est recopié sur la sortie), soit une incrémentation. Un tel circuit est nommé un incrémenteur commandable. Nous aurons à utiliser une fois ou deux de tels incrémenteurs commandables dans la suite du cours.

L'incrémenteur à propagation de retenue était utilisé sur le processeur Intel 8085, avec cependant une optimisation très intéressante. Un demi-additionneur usuel est construit comme ci-dessous

Demi-additionneur en CMOS, les portes coloriées en jaunes sont construites avec un seul transistor CMOS/TTL.

Regardons ce que cela donne quand on enchaine deux demi-additionneurs l'un à la suite de l'autre.

Brique de base de l'incrémenteur du 8085

Les ingénieurs ont réussit à se débarrasser de la porte NON, pour une colonne sur deux. Les trois portes en jaune dans le schéma précédent sont fusionnées, de manière à donner une porte NOR couplée à une porte NON. 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 à chaque colonne, il traverse une seule porte : une porte NAND pour les colonnes paires, une porte NOR pour les colonnes impaires. Mine de rien, cette optimisation économisait des portes logiques et rendait le circuit deux fois plus rapide.

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.

Les incrémenteurs plus complexes sont rares

[modifier | modifier le wikicode]

Pour résumer, ce circuit ne paye pas de mine, mais il était largement suffisant sur les premiers microprocesseurs, qui géraient des opérandes de 8 bits. 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 :

Les débordements d'entier lors d'une addition/soustraction

[modifier | modifier le wikicode]

Les instructions arithmétiques manipulent des entiers codés sur un nombre fixe de bits, 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 à . Pour les nombres négatifs, l'intervalle est différent et dépend de la représentation utilisée. 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 | modifier le wikicode]

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, le circuit 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.

D'autres circuits utilisent ce qu'on appelle l'arithmétique saturée : si un calcul génère 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 plus complexes, 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 et le corrige 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 couche 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 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. 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.

La détection des débordements entiers

[modifier | modifier le wikicode]

Quand un débordement d'entier a eu lieu, il vaut mieux que l'additionneur prévienne ! Pour cela, l'additionneur a une sortie de débordement, parfois nommée Overflow, dont la valeur indique si l'addition a généré 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é ou signé.

Pour les additionneur non-signés, l'additionneur calcule un bit de plus que ce qui est supporté par l'ordinateur. Par exemple, un additionneur 32 bits fournit un résultat sur 33 bits, un débordement d'entier a lieu quand le 33ème bit est à 1. Précisément, la sortie de débordement n'est autre que la retenue finale, celle fournie par le dernier additionneur complet. Le seul type de débordement possible est un débordement par le haut, où le résultat dépasse la valeur maximale. Avec l'arithmétique saturée, le circuit de saturation consiste en une seule couche de multiplexeurs, voire en un circuit de mise à la valeur maximale tel que vu dans le chapitre sur les opérations bits à bits.

Gestion des débordements d'entiers lors d'une addition non-signée.

Pour les additionneurs non-signés, la gestion des débordements d'entiers dépend fortement de la représentation signée. Nous allons étudier le cas du complément à deux. Si vous vous rappelez le chapitre 1, les calculs sur des nombres en complètement à deux utilisent les règles de l'arithmétique modulaire, c'est une condition nécessaire. À priori, on peut 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 écrase le bit de poids fort, il y a un débordement d'entiers. Il existe une règle simple qui permet de détecter ces débordements d'entiers. L'addition de deux nombres positifs ne peut pas être un nombre négatif. Si on additionne deux nombres dont le bit de signe est à 0 et que le bit de signe du résultat est à 1, on est en face d'un débordement d'entiers. Même chose pour deux nombres négatifs : 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é.

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.


Dans les chapitres précédents, nous avons vu les circuits pour l'addition, la soustraction et les comparaisons. Nous avons aussi vu qu'il est très facile d'implémenter la soustraction en rajoutant quelques portes logiques à un additionneur. Et de même, une fois qu'on sait faire la soustraction, implémenter les comparaisons demande juste d'ajouter quelques portes logiques. Mais il est possible d'aller plus loin ! Dans ce chapitre, nous allons voir un 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 additions, des soustractions, des comparaisons et des opérations bit à bit. La plupart des ALUs ne gèrent pas les multiplications/divisions et vous comprendrez pourquoi dans ce qui suit.

Tous les processeurs contiennent au moins une ALU. En fait, créer un processeur demande une unité de calcul, des registres, un circuit de communication avec la mémoire et d'interconnecter le tout. Il faut aussi ajouter des circuits pour commander le tout, qui sont regroupés dans l'unité de contrôle. L'unité de contrôle lit les instructions en mémoire, puis commande l'unité de calcul, les registres et la mémoire pour que l'instruction soit exécutée correctement. L'unité de contrôle est assez complexe et aura droit à plusieurs chapitres dédiés, nous avons déjà vu les registres, il est temps de voir l'unité de calcul.

Microarchitecture d'un processeur

L'interface d'une unité de calcul et sa conception

[modifier | modifier le wikicode]

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, qui varie d'une ALU à l'autre. La suite de bit peut être vu est aussi appelée l'opcode, ce qui est un diminution de code opération.

L'ALU a aussi une entrée de retenue entrante, sur le même modèle que les additionneurs. Pour rappel, les additionneurs sont conçus avec des additionneurs complets, qui prennent trois bits en entrée : deux bits d'opérande et un bit de retenue. Pour la colonne des bits de poids faible, il y a aussi un additionneur complet qui prend en opérande les deux bits de poids faible, mais aussi une retenue entrante. Les unité de calcul entières contiennent un additionneur entier, ce qui fait qu'elles aussi disposent de cette entrée de retenue. Elles fournissent aussi la retenue en sortie, avec d'autres informations, ce qui nous amène à parler des sorties de l'ALU.

En plus de la sortie pour le résultat, l'ALU a des sorties de 1 bit appelées des flags, ou indicateurs. Les plus fréquents sont les fameux bits intermédiaires vu dans le chapitre sur les comparaisons : un bit qui est à 1 si un débordement d'entier a eu lieu (la retenue de sortie), un bit qui est à 1 si un débordement d'entier en complètement à deux a eu lieu, un bit qui indique si le résultat est zéro, le bit de signe du résultat en complément à deux. Si c'est le cas, les bits intermédiaires alimentent souvent un circuit qui calcule le résultat d'une comparaison, qui est considéré comme séparé de l'ALU.

Mais une ALU peut fournir d'autres flags en plus de ces 4 bits intermédiaires, voire ne pas fournir les 4 bits précédents, tout dépend de l'ALU. Par exemple, certains processeurs avaient un flag qui donnait le bit de parité du résultat. Autre exemple, les processeurs avec un support du BCD avaient des flags dédiés à la gestion du BCD. Le processeur Z80 fournissait les deux flags des exemples précédents, à savoir un flag pour le bit de parité du résultat, un autre pour la gestion du BCD, et un autre pour indiquer que le résultat valait zéro.

Interface d'une ALU

Le bit-slicing

[modifier | modifier le wikicode]

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 des opérandes de 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 pour 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 le 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. Il n'y a pas, à ma connaissance, d'ALU en bit-slicing capable d'effectuer une multiplication ou une division.

L'implémentation des opérations bit à bit avec une ALU bit-slice est triviale, la seule complication mineure 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. Il faut que l'ALU ait 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 | modifier le wikicode]

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.

Mais les ALU que nous allons voir fonctionnent autrement. Elles sont construites sur le même modèle que l'additionneur-soustracteur, qui est un circuit configurable. On lui envoie un bit de commande qui décide entre addition ou soustraction, ce bit de commande configure un inverseur commandable et la retenue entrante. Les ALU qui vont suivre disposent de plusieurs circuits semblables à l'inverseur commandable. Ils possèdent une entrée de commande, dont la valeur est déduite par un circuit combinatoire à partir du code opération (généralement un décodeur).

ALU composée de sous-ALU configurables.

Les ALU entières basées sur un additionneur-soustracteur

[modifier | modifier le wikicode]

Pour rappel, un additionneur soustracteur est fait en combinant un additionneur avec un inverseur commandable. L'entrée de retenue et l'entrée de commande de l'inverseur sont partagée, c'est le même bit qui est envoyé sur les deux. Mais dans ce qui suit, on va supposer qu'elles sont découplées, qu'on peut envoyer des bits différents sur les deux. Le circuit est donc celui-ci :

Additionneur soustracteur

De plus, nous allons ajouter un circuit commandable de mise à zéro pour la seconde entrée d'opérande.

ALU basée sur un additionneur soustracteur modifié

L'ALU obtenue ainsi supporte 8 opérations distinctes, résumées dans le tableau ci-dessous. Les principales sont l'addition, la soustraction, l'opération NOT, l'incrémentation, le calcul du complément à deux, et l'identité (une entrée est recopiée sur la sortie).

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 - B
1 0 0 B
1 0 1 B + 1
1 1 0
1 1 1 + 1 (complément à deux)

Pour les autres opérations bit à bit, l'idéal est d'ajouter des circuits pour les opérations ET/OU/XOR en parallèle de l'additionneur-soustracteur et d'utiliser un multiplexeur pour choisir quel circuit donne le résultat. Une amélioration relie l'inverseur commandable non seulement à l'additionneur, mais aussi aux portes ET/OU/XOR. Il est aussi possible de faire pareil avec le circuit pour mettre à zéro l'opérande non inversée. Le tout permet d'ajouter quelques opérations logiques gratuitement, juste en changeant le câblage du circuit

ALU simplifiée.

Les ALU basées sur la manipulation des retenues

[modifier | modifier le wikicode]

L'ALU précédente implémente pas les opérations bit à bit en ajoutant des circuits autour de l'additionneur. Cependant, il existe une alternative qui modifie l'additionneur pour qu'il devienne capable de faire des opérations ET/OU/XOR. Pour comprendre comment faire, il faut rappeler qu'un additionneur est composé de deux parties : une couche d'additionneurs complets, et le reste qui s'occupe du calcul ou de la propagation des retenues. Et il se trouve qu'en manipulant les retenues, on peut émuler d'autres opérations à partir de l'addition.

Par exemple, nous avons déjà vu que l'opération XOR est une addition dans laquelle les retenues seraient ignorées. En conséquence, on peut émuler un XOR à partir d'une addition, en rajoutant un circuit pour mettre les retenues à 0, simplement composé de portes ET. 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. Mais on peut aller encore plus loin...

Circuit qui fait ADD et XOR.

Les unités de calcul logiques fabriquées avec des additionneurs complets

[modifier | modifier le wikicode]

Mine de rien, un additionneur complet seul est capable d'exécuter de nombreuses opérations bit à bit, ce qui permet d'implémenter une unité de calcul logique avec des additionneurs complets. Pour rappel, une unité de calcul logique ne gère que les opérations bit à bit, pas l'addition ni la soustraction. Les opérations supportées sont les opérations NOT, OU, ET, XOR, parfois d'autres comme NXOR. Et un additionneur complet gère ces opérations nativement. Pour rappel, un additionneur complet additionne trois bits, en faisant deux XOR :

Il est alors intéressant de voir ce qui se passe si on force la retenue entrante à 0 ou 1. Si on force la retenue entrante à 0, le tout se simplifie grandement. On rappelle à toute fin utile que . Les équations précédentes deviennent :

A l'opposé, si on force les retenues à 1, les équations deviennent totalement différentes. Sachant que , on obtient :

Pour résumer :

  • Si la retenue d'entrée est à 0, la retenue de sortie est un ET entre les deux bits d'opérandes, le bit de somme en est le XOR.
  • Si on met la retenue entrante à 1, alors la retenue sortante sera un OU entre les deux bits d'opérandes, le bit de somme en est le NXOR.

L'unité de calcul à forçage des retenues

[modifier | modifier le wikicode]

Pour manipuler des retenues, il faut ajouter un circuit de masquage dans l'additionneur-soustracteur, pour mettre les retenues à 0/1. 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. Le circuit de masquage est composé de portes universelles 1 bit, un circuit qu'on a abordé dans le chapitre sur les opérations bit à bit, avec une porte universelle par retenue.

Additionneur modifiée en ALU entière capable de faire des XOR et NXOR

Pour finaliser le circuit, il faut connecter la sortie soit aux bits de résultat, soit aux entrées de retenue, ce qui demande un simple multiplexeur.

Implémentation d'une ALU entière simple

Les ALU basées sur des ALU 1 bit

[modifier | modifier le wikicode]

Les ALU précédentes sont basées sur des additionneurs auxquels on a rajouté des circuits, mais où les additionneurs complets eux-mêmes n'ont pas été modifiés. Les ALU que l'on va voir dans ce qui suit font l'inverse : elle transforment les additionneurs complets en de véritables ALU de 1 bit, capables de faire des opérations logiques ET/OU/XOR/NXOR. Les ALU 1 bit sont souvent connectées comme dans un additionneur à propagation de retenue, mais ce n'est pas systématique et on peut faire la même chose avec des additionneurs plus complexes.

ALU parallèle fabriquée à partir d'ALU 1 bit.

L'exemple de l'ALU du processeur 8086 d'Intel

[modifier | modifier le wikicode]

Voyons maintenant l'ALU du processeur 8086 d'Intel, un des tout premier de la marque. Elle est basée sur un additionneur complet qui calcule la retenue sortante avec un multiplexeur 2 vers 1, illustré ci-dessous.

Additionneur complet basé sur un MUX

Sur le 8086, la porte XOR et la porte ET sont remplacées par une porte logique universelle commandable 2 bit, à savoir un circuit qui peut remplacer toutes les portes logiques 2 bit existantes. Pour configurer les deux portes, l'ALU contient un petit circuit combinatoire qui traduit l'opcode en signaux envoyés aux portes universelles.

ALU du 8086 (bloc de 1 bit)

Pour l'addition et la soustraction, les deux portes sont configurées pour reformer sur un additionneur complet. Pour les opérations bit à bit, la porte qui remplace le XOR est alors configurée pour donner la porte voulue : soit un ET, soit un OU, soit un XOR, soit.... En parallèle, l'autre porte logique a un 0 sur sa sortie, afin de mettre les retenues à 0.

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écaler est envoyé sur les entrées A de chaque additionneur complet. Les deux portes logiques universelles sont alors configurées comme suit : 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

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 | modifier le wikicode]

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. Leur budget en transistors était en faveur des additionneurs à propagation de retenue. Elle est construite en assemblant plusieurs ALU de 1 bits, chacune basée sur un additionneur implémenté avec le circuit suivant, abordé précédemment dans ce chapitre :

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 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 | modifier le wikicode]

L'unité de calcul 74181 est très souvent présentée dans les cours d'architecture des ordinateurs, pour son aspect pédagogique indéniable. Elle a été commercialisée dans les années 60, à une époque où processeurs étaient vendus en kit, en pièces détachées. 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.

Le 74181 était une ALU de 4 bits, ce qui veut dire qu'elle prenait en entrée deux opérandes entiers 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 | modifier le wikicode]

Le 74181 fonctionne concrètement comme un additionneur-soustracteur amélioré sur deux points. Premièrement, l'inverseur commandable est remplacé par une porte universelle 2 bits. Pour l'additionneur, il conserve son entrée de retenue, mais il est désactivable. Concrètement, il y a un MUX en sortie de l'ALU qui choisit la sortie parmi : la sortie des portes universelles 2 bits, la sortie de l'additionneur. 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 M qui indique s'il faut faire l'addition ou non. Dans le groupe de 4 bits, les bits sont notés s0, s1, s2 et s3.

Schéma fonctionnel du 74181.

En conséquence, le 74181 peut combiner l'addition et les 16 opérations bit à bit (donc toutes les opérations de ce type possibles entre deux bits). L'ALU 74181 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. Un exemple d'opération de ce genre est la soustraction, obtenue en combinant l'opération bit à bit NOT, une retenue d'entrée à 1, et une addition. En tout, le 74181 était capable de réaliser 32 opérations différentes : les 16 opérations bit à bit seules, et 16 autres opérations obtenues en combinant une opération bit à bit avec une addition.

L'implémentation de l'ALU 74181

[modifier | modifier le wikicode]

Le 74181 comprend 75 portes logiques, mais ce nombre est à relativiser car l’implémentation utilisait des optimisations qui fusionnaient plusieurs portes entre elles. Elle utilisait notamment des portes AND-OR-NOT, identique à une porte ET suivie d'une porte NOR. 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. Le circuit est cependant très optimisé, dans le sens où l'additionneur complet est fusionné avec la porte logique universelle.

L'idée part d'un additionneur PG, qui génère deux signaux de propagation et de génération de retenue sont calculés. Le 8086 remplace les deux portes qui calculent ces signaux par des portes universelles 2 bits. Le 74181 n'utilise qu'une seule porte logique universelle, très modifiée. En clair, le 714181 est composé d'ALU 1 bit reliées à un circuit d’anticipation de retenue. La table de vérité de vérité des ALU 1 bit est la suivante. 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 œil 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 :

Les ALU sérielles

[modifier | modifier le wikicode]

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.

L'ALU sérielle est facile à concevoir à partir de sa table de vérité, aussi je ne va pas détailler sa conception, je laisse le tout en exercice au lecteur. Mais un moyen de la concevoir facilement est simplement d'utiliser un additionneur complet avec de quoi mettre la retenue à 0/1, idem pour une des deux entrées d'opérande.

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.


Dans ce chapitre, nous allons voir les opérations de manipulation de bits, des opérations qui manipulent les bits d'un opérande. La différence avec les opérations bit à bit est que les bits de colonnes différentes peuvent être combinés entre eux pour fournir leur résultat, elles peuvent les faire changer de place, en supprimer, en ajouter, etc. Mais cela deviendra plus compréhensible en voyant quelques exemples d'opérations de ce genre.

L'opération de population count

[modifier | modifier le wikicode]

L'opération de population count compte le nombre de bits à 1 d'un opérande entier. Elle est très utilisée quand on manipule des tableaux de bits, pour le codage/décodage vidéo/audio, pour 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 est aussi très courante dans les algorithmes de correction d'erreur, utilisés dans les transmissions réseaux ou dans certaines mémoires. Les autres livres d'architecture des ordinateurs parlent de l'opération elle-même, mais pas des circuits pour la calculer, erreur que nous ne ferons pas.

Le circuit de population count : les compteurs parallèles

[modifier | modifier le wikicode]

La population count est un cas particulier d'addition multiopérande, où chaque opérande fait 1 bit. En effet, pour calculer la population count d'un opérande de N bits, il faut additionner ces N bits entre eux. Et les techniques vues dans le chapitre sur l'addition multiopérande marchent aussi pour la population count. La technique la plus simple est celle dite du diviser pour régner. L'idée est que si on découpe l'opérande en deux, la population count est la somme des population count de chaque partie. Par exemple, pour calculer la population count d'un entier 32 bits (4 octets), on peut additionner les population count des 4 octets de l'opérande. Pour cela, il faut fabriquer des circuits capables d'additionner entre 2 et 7 bits, appelés des parallel counters, terme que nous traduirons par compteurs parallèles.

Circuit de calcul de population count.

Il se trouve que nous avons déjà vu des compteurs parallèles dans les chapitres précédents. Les additionneurs complets sont des compteurs parallèles 3 bits, les demi-additionneurs sont des compteurs parallèle 2 bits. Pour rappel, le premier additionne trois bits, alors que le second additionne deux bits. Dans ce qui suit, nous allons utiliser les abréviations suivantes : FA (Full-Adder) pour additionneur complet, HA (Half-Adder) pour demi-additionneur.

Le calcul de la population count peut donc utiliser des FA comme compteurs parallèles. Un tel additionneur regroupe les bits de l'opérande par triplets et additionne chaque triplet avec un FA. S'il reste une paire de bits isolées après avoir formé les triplets, elle est additionnée avec un HA. Les FA/HA calcule des population count intermédiaires codées sur 2 bits, qui sont ensuite additionnées par l'additionneur multi-opérande. Le circuit ne paye pas de mine, mais c'est un circuit de ce style qui a été utilisé sur le processeur ARM1, un des tout premier CPU ARM, prévu pour les premiers iPhones.

Illustration de la première couche du circuit de POPCNT.

Le circuit précédent additionne un grand nombre de résultat intermédiaires codées sur 2 bits. L'idéal serait pourtant l'inverse : moins d'opérandes à additionner, mais celles-ci sont plus longues. Pour cela, il faut remplacer les FA par des compteurs parallèles qui fournissent des résultats codés sur 4, 5, 6, 7 bits, parfois plus. Intuitivement, on se dit qu'il faudrait découper les opérandes en groupes de 4, 8, 16 bits, ou toute autre puissance de deux. Sauf que ce n'est pas l'idéal. Par exemple, avec un résultat de 3 bits, on peut coder les valeurs de 0 à 7, ce qui fait 7 bits. Sur 4 bits, cela permet de gérer 15 bits, pas plus. Sur 5 bits, cela permet de gérer 63 bits d'entrée. À chaque fois, il nous manque un bit pour avoir un groupe bien rond de 4, 8, 16 bits. La raison est qu'il faut encoder le zéro pour le résultat.

Les compteurs parallèles peuvent être construits comme n'importe quel circuit combinatoire, à partir de la table de vérité. Mais ils peuvent aussi être construits en combinant des compteurs parallèles plus petits avec un additionneur. Il s'agit de l'implémentation la plus simple, la moins optimisée, la plus gourmande en portes logiques. Heureusement, des simplifications sont possibles, comme on le verra dans la suite du chapitre.

Population count avec des compteurs parallèles

Les compteurs parallèles de moins de 8 bits

[modifier | modifier le wikicode]

Dans cette section, nous allons voir comment créer des compteurs parallèles en utilisant des FA/HA, couplés à un additionneur. Nous simpliferons ces circuits par la suite. Le premier exemple intéressant est le compteur parallèle 4 bits, qui prend 4 bits d'opérande et fournit un résultat de 3 bits. Premièrement, il additionne les bits d'opérande deux par deux, avec deux HA/ Les deux résultats de 2 bits sont ensuite additionnés avec un additionneur 2 bits. Le circuit final est le suivant :

Compteur parallèle de 4 bits, fabriqué avec des HA et FA

Pour les compteurs parallèles de plus de 4 bits, il devient intéressant d'additionner les bits par groupes de 3, par triplet. Les bits d'un triplet sont additionnés avec un FA, ce qui donne un résultat sur deux bits. Le compteur parallèle 5 bits est construit sur le même modèle. Il calcule la population count d'une paire de bit et d'un triplet de bit, et additionne le tout avec un additionneur 2 bits.

Compteur parallele 5 bits, naïf

Pour un compteur de 6 bits, la première couche ne contient que des FA, tout en conservant l'additionneur 2 bits.

Compteur parallèle 6 bits

Dans l'exemple précédent, il est intéressant d'ajouter un 7ème bit d'opérande, pour profiter de l'entrée de retenue de l'additionneur. En effet, tous les additionneurs dignes de ce nom disposent d'une entrée de retenue, utilisée pour l'incrémentation ou d'autres opérations. Elle permet d'ajouter un bit en plus, sur la colonne des bits de poids faible. Et bien on peut rajouter un 7ème bit sur cette entrée. Et dans le cas général, au lieu d'additionner des triplets de bits, on peut rajouter un bit en plus.

Compteur parallèle 7 bits

Les simplifications liées aux demi-additionneurs

[modifier | modifier le wikicode]

Les compteurs vus plus haut peuvent être simplifiés, afin de faire des économies de portes logiques. Les compteurs parallèle de 6 et 7 bits ne peuvent pas se simplifier facilement, mais ceux de 4 et 5 bits le peuvent. Les simplifications sont liés au fait que ces deux circuits intégrent des paires de HA en série, à savoir que le second prend l'entrée du premier. Et à une porte logique près, ces deux HA en série forment un FA.

Pour le compteur parallèle 5 bits, la simplification donne le circuit suivant. Le circuit originel avait deux HA et deux FA, cette version simplifié élimine un HA, ce qui fait une petite économie de circuits. Avec cette simplification, on perd l'organisation en deux couches : une couche de FA/HA qui calcule la population count de paires/triplets de bits, suivi par un additionneur 2 bits. Mais le seul désavantage est que le circuit est moins simple à expliquer, ce qui est juste un petit défaut.

Circuit de calcul de PCOUNT de 5 bits

Pour le compteur parallèle 4 bits, une implémentation alternative donne le circuit suivant. Le circuit incorpore un FA et deux HA, contre 4 HA avec le circuit de base. Sachant qu'un FA contient 2 HA et une porte logique, le circuit n'économise pas de portes logiques. Notez que ce circuit peut être interprété comme un FA couplé à un additionneur. Précisément, il additionne trois bits, puis décide d'incrémenter le résultat ou non suivant la valeur du quatrième bit. Le circuit est donc composé d'un FA suivi par un incrémenteur commandable (son entrée de retenue est configurable).

Compteur parallèle de 4 bits, version alternative

Les adders compressors : fusionner des additionneurs complets

[modifier | modifier le wikicode]

Nous venons de voir une simplification assez basique, à savoir fusionner plusieurs HA en un seul FA. Mais est-ce qu'il existe un équivalent pour les FA ? A savoir un circuit qui fusionne plusieurs FA en un seul circuit amélioré ? intuitivement, on se dit que non, car un tel circuit serait tout sauf intuitif pour ce qui est des retenues. Sauf que la réponse est oui, il existe des circuits qui fusionnent plusieurs FA entre eux et fournit exactement les mêmes sorties que les FA fusionnés. De tels circuits sont appelés des adder compressors.

Les adder compressors sont utilisés dans le cadre de l'addition multiopérande en général, pas seulement pour le calcul de la population count. Ils sont surtout utilisés dans les additionneurs multiopérandes en arbre. Les arbres de Wallace ou de Dadda peuvent être modifiés de manière à fusionner des FA d'une même couche, en les remplaçant par des adder compressors. Mais la construction de l'arbre d'additionneur est alors beaucoup plus complexe, ce qui explique pourquoi je n'en ai pas parlé dans le chapitre sur l'addition multiopérande.

Les adder compressors fournissent le bit de poids faible du résultat et les différentes retenues des additions intermédiaires fournies par les FA fusionnés. Par exemple, prenons le compteur parallèle de 5 bits, composé de deux FA placés l'un à la suite de l'autre, avec un HA qui additionne les deux retenues produites par les deux FA. Les deux FA peuvent être remplacés par un mal-nommé adder compressor 4:2, qui prend 5 bit d'opérande et fournit les trois mêmes sorties que les deux FA : le bit de poids faible du résultat final, les deux retenues des additions intermédiaires. Les retenues doivent être combinées ensemble par des circuits à part, pour obtenir un compteur parallèle. Dans le cadre de l'addition multiopérande, les retenues sont simplement propagées à la colonne suivante, elles sont envoyées en entrée d'un autre adder compressor.

Simplification d'un compteur parallèle 5 bits avec un adder compressor.

L'avantage est que le calcul du bit de somme est simplifié. Le mieux est de partir de l'exemple avec un compteur parallèle 5 bits, avec ses deux FA en série. Voici ce que l'on obtient en enchainant deux FA, au niveau des XOR :

Adder compressor 4-2

Le schéma montre que l'on a quatre portes XOR placées en série, ce qui est tout sauf idéal. Mieux vaut essayer de les mettre en parallèle, pour gagner un petit peu en rapidité. Il est par exemple possible d'améliorer le circuit précédent pour passer de 4 portes en série à seulement 3, ce qui est légèrement plus rapide.

Adder compressor 4-2 optimisé

En clair, la fusion des FA permet de réorganiser la chaine de portes XOR qui calcule le bit de somme. Les portes XOR doivent idéalement former un arbre équilibré, de manière à réduire le nombre de portes XOR à traverser. Un adder compressor utilise l'arbre des portes XOR idéal, mais calcule les retenues par groupes de 3 bits.

Addition des bits de somme avec des HA : arbre équilibré

Les opérations FFS, FFZ, CTO et CLO

[modifier | modifier le wikicode]

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 que nous allons voir sont au nombre de 8 et elles s'expliquent facilement avec le schéma ci-dessous.

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.

Les quatre opération suivantes donnent la position des 0/1 de poids faible/fort :

  • L'opération Find First Set, donne la position du 1 de poids faible.
  • L'opération Find highest set donne la position du 1 de poids fort.
  • L'opération Find First Zero donne la position du 0 de poids faible (le plus à droite).
  • L'opération Find Highest Zero donne la position du 0 de poids fort (le plus à gauche).

Elles ont des opérations corolaires qui elles, comptent le nombre de 0/1 avant ou après des 0/1 de poids fort/faible.

  • L'opération Count Trailing Zeros compte les zéros situés à droite du 1 de poids faible.
  • L'opération Count Leading Zeros compte les zéros à gauche du 1 de poids fort.
  • L'opération Count Trailing Ones compte les 1 situés à gauche du 0 de poids fort.
  • L'opération Count Leading Ones compte les 1 situés à droite du 0 de poids faible.

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 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. Il y a deux classes d'opérations : celles à gauche dans les équations précédentes, celles à droite. Les premiers donnent la position du 0/1 de poids faible/fort, celles qui comptent des 0/1 de poids faibles/fort. Et les deux classes s'implémentent par des circuits très différents.

L'implémentation avec un encodeur à priorité

[modifier | modifier le wikicode]

La première implémentation implémente les quatre calculs suivants :

  • 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.

Implémenter chaque opération peut se faire avec un encodeur à priorité. Pour les quatre opérations précédentes, il existe un encodeur à priorité qui s'en charge. Par exemple, on peut utiliser un encodeur à priorité qui donne la position du 1 de poids fort, c’est-à-dire qui réalise l'opération Find Highest Set. Il existe aussi un autre encodeur à priorité qui lui donne la position du 1 de poids faible, ce qui correspond à l'opération Find First Set. Il existe aussi un encodeur qui donne la position du zéro de poids faible (Find First Zero) et un autre qui donne celle du zéro de poids fort (Find highest Zero).

Mais utiliser quatre encodeurs différents n'est pas l'idéal. Il est en effet possible de faire avec un seul encodeur. L'idée est qu'un encodeur à priorité est composé d'un encodeur normal, couplé à un circuit de priorité qui sélectionne le 0/1 de poids fort/faible. L'idée est de rendre ce circuit configurable, de manière à choisir l'opération voulue parmi les 4 précédentes.

Encodeur à priorité

Une autre méthode utilise un inverseur commandable. En, effet, les opérations FHS et FHZ peuvent se déduire l'une de l'autre, en inversant le nombre passé en entrée : les 0 de poids fort deviennent alors des 1 de poids fort, et vice-versa. 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. Inverser les bits de l'entrée se fait avec un inverseur commandable.

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

L'implémentation avec la population count

[modifier | modifier le wikicode]

Maintenant, voyons comment implémenter les quatre opérations suivantes. Il s'agit des opérations qui comptent les 0 ou 1 de poids faible, et ceux de poids fort.

  • L'opération Count Trailing Zeros donne le nombre de zéros situés à droite de ce 1 de poids faible.
  • L'opération Count Leading Zeros donne nombre de zéros situés à gauche du 1 de poids fort.
  • L'opération Count Trailing Ones donnent le nombre de 1 à gauche du 0 de poids fort.
  • L'opération Count Leading Ones donne le nombre de 1 à droite du 0 de poids faible.

Les quatre opérations listées plus haut comptent un certain nombre de 0 ou 1. Compter des 1 ressemble beaucoup à ce que fait le circuit de population count. La différence est qu'ici, seuls certains bits sont à prendre en compte : ceux situés à droite/gauche d'un 0/1 de poids faible/fort. Or, nous avons déjà un outil pour ignorer certains bits : l'usage de masques avec des opérations bit à bit. L'idée est alors de générer un masque qui indique la position des 0/1 de poids faible/fort. Chaque bit du masque est associé au bit à la même place dans l'opérande, celui de même poids. Un bit du masque à 1 indique que le bit est à prendre en compte, alors qu'un bit à 0 indique un bit à ignorer.

Par exemple, prenons le cas où on veut compter le nombre de Trailing Zeros, à savoir les 0 de poids faible, ceux situés à droite du premier 1 rencontré en lisant l'opérande de droite à gauche. La première étape génère un nombre qui a un 1 à la place de chaque Trailing Zero, et un 0 ailleurs.

Opérande 0010 1101 1001 1000 0000
Masque 0000 0000 0000 0111 1111

Une fois le masque voulu obtenu, on compte le nombre de 1 dans le masque généré. En clair, on calcule sa population count. Le résultat donne le nombre voulu.

Le circuit qui génère le masque a une implémentation similaire à celle utilisée par un encodeur à priorité. Avec un encodeur à priorité qui calcul l'opération Find First Set, le circuit met à 0 les bits qui suivent le 1 de poids fort. Avec l'opération Count Leading Zero, le circuit fait la même chose, sauf que les bits sont mis à 1. Le circuit est construit de la même manière, comme illustré ci-dessous. Il s'agit de l'implémentation la plus simple, composée de briques qui mettent à 0/1 un bit de l'opérande, qui sont enchainés les uns à la suite des autres.

L'implémentation de l'opération CLZ avec la population count

Le circuit précédent met à 1 un bit à la fois. Une amélioration serait d'en traiter plusieurs à la fois. Par exemple, on peut imaginer un circuit qui traite des groupes de 4/5 bits. Pour chaque groupe, un circuit détecte les 1 de poids fort et met les bits suivants à 1. Le circuit peut se concevoir simplement avec un tableau de Karnaugh. Évidemment, pour enchainer plusieurs circuits, il faut gérer le cas où un 1 de poids fort a été détecté dans un groupe précédent et mettre toutes les sorties à 1 cas échéant, avec un circuit de mise à 111111 qu'on a déjà vu.

Implémentation optimisée de l'opération CLZ basée sur la POPCOUNT

Une version améliorée de cette technique a apparemment été utilisée par Intel dans certains de ces processeurs, le brevet "Combined set bit count and detector logic" détaille l'implémentation d'une technique similaire.

Les circuits générateurs/vérificateurs d'ECC

[modifier | modifier le wikicode]

Au tout début de ce cours, nous avions vu les codes ECC, qui détectent ou corrigent des corruptions de données. Si un bit est altéré, ils permettent de détecter que le bit en question a été inversé, et peuvent éventuellement le corriger pour retrouver la donnée initiale. Les deux codes ECC les plus connus sont le bit de parité et les codes de Hamming. Et ils sont très liés à la population count. Dans ce qui suit, nous allons voir des circuits qui calculent soit un bit de parité, soit le code de Hamming d'un nombre.

Le générateur de bit de parité

[modifier | modifier le wikicode]

Pour rappel, le bit de parité permet de détecter qu'un bit a été inversé, à savoir qu'il est passé de 1 à 0 ou inversement. Pour cela, on ajoute un bit de parité aux données à sécuriser, afin que le nombre de bits à 1 soit pair, bit de parité inclus. En clair, le bit de parité vaut 0 si la donnée a un nombre de bits à 1 pair, il vaut 1 si ce nombre est impair. Dans cette section, nous allons voir un circuit qui calcule le bit de parité d'un opérande.

Circuit de parité

Intuitivement, on se dit qu'il faut compter les 1 dans l'opérande, avant de calculer sa parité et d'en déduire le bit de parité. Le bit de parité est donc le bit de poids faible de la population count. Le bit de parité se calcule donc en additionnant les bits de l'opérande, mais sans tenir compte des bits de poids fort, sans tenir compte des retenues, en ne conservant que le bit de somme. Lors de l'addition de deux bits, ce bit de somme est calculé en faisant un XOR entre les deux bits à additionner. Pour N bits, il suffit d'enchainer N-1 portes XOR. Avec cette logique, on peut créer un générateur de parité parallèle, un circuit qui calcule le bit de parité d'un opérande, en faisant un XOR entre tous ses bits. En réfléchissant, on devine qu'on peut structurer les portes XOR comme illustré ci-contre.

Le circuit précédent calcule le bit de parité d'un opérande. Pour ce qui est de vérifier si une donnée est corrompue, rien de plus simple : il suffit de générer le bit de parité de la donnée seule, et de le comparer avec le bit de parité stocké dans la donnée avec la porte logique adaptée. Le circuit qui génère un bit de parité et celui qui vérifie si le bit de parité est valide sont donc très similaires.

Le générateur/checker d'ECC

[modifier | modifier le wikicode]
Hamming(7,4)

Les codes de Hamming calculent plusieurs bits de parité, qui sont chacun calculés en prenant en compte certains bits de l'opérande. Par exemple, le code de Hamming de type 7,4 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. Le schéma ci-contre indique quels sont les bits de données utilisés pour calculer un bit de parité : les bits de parité sont notés p, les bits de données d.

L'implémentation matérielle est donc très simple : un circuit qui génère un code de Hamming est composé de plusieurs circuits de génération de parité, idem pour un circuit qui vérifie le code de Hamming d'un opérande. Étudions par exemple le circuit suivant, conçu pour le code 7-4-3. Il vérifie si les 4 bits de données sont valides, à savoir si les trois bits d'ECC collent bien aux données associées. Si les deux valeurs correspondent, il n'y a pas d'erreur. Mais si les bits ne correspondent pas, alors on sait quel bit est erroné en regardant quel bit d'ECC est invalide. Le circuit corrige alors le bit erroné.

En premier lieu, le circuit calcule le code de Hamming des 4 bits de données, avec une première couche de portes XOR. Le résultat est alors comparé avec les trois bits d'ECC présents dans l'opérande, par un comparateur d'égalité, lui-même construit avec des portes XOR. Le tout est suivi par un circuit de correction d'erreur. Une couche de portes ET/NON sélectionne le bit à corriger. Elle génère un masque de 4 bits qui indique quel bit inverser : celui dont le bit du masque est à 1. La dernière couche de portes XOR prend ce masque et l'applique aux 4 bits de données, ce qui inverse le bit adéquat.

Circuit de vérification d'un code de Hamming 7,4.


Comparateur 4 Bits.

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. 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 | modifier le wikicode]

Pour commencer, nous allons voir un circuit qui prend deux bits comme opérande, et vérifie si le premier est inférieur, supérieur ou égal au second. Les deux bits d'opérandes sont appelés respectivement a et b, et le circuit vérifie si , si ou si .

La supériorité et l'infériorité

[modifier | modifier le wikicode]

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 < :

Un tel circuit ne paye pas de mine, mais il permet d'encoder toutes les comparaisons possibles entre deux bits. Par exemple, nous n'avons pas besoin de circuit supplémentaire pour savoir si . Par définition, est l'inverse de . Donc, on peut savoir si en regardant la sortie  : elle est à 0 si c'est le cas. Et il en est de même avec est l'inverse de .

L'égalité et l'inégalité

[modifier | modifier le wikicode]

Il est aussi possible de savoir si les deux bit d'entrée sont égaux. Par définition, si les deux bit opérande sont égaux, alors les deux conditions et sont fausses. Il s'agit des deux lignes de la table de vérité où les deux sorties sont à 0. Il suffit de combiner les deux résultats avec une porte NOR pour obtenir le bit d'égalité. Pour vérifier si deux bits sont différents, c'est le même principe. Si A et B sont différents, alors on a soit A>B, soit A<B. On a juste à combiner les deux sorties "A > B" et "A < B" avec une porte OU pour tester si une des deux conditions est respectée.

Comparateur d'égalité 1 bit.

Les deux circuits précédents devraient vous dire quelque chose, si vous vous souvenez vraiment bien du cours sur les portes logiques. En effet, il s'agit respectivement de portes NXOR et de portes XOR ! Pour vous en convaincre, le mieux est d'établir la table de vérité des deux circuits, qui est donnée ci-dessous. On voit rapidement qu'il s'agit des tables de vérité des portes XOR et NXOR.

Entrée 1 Entrée 2 Egalité = Inégalité !=
0 0 0 1
0 1 1 0
1 0 1 0
1 1 0 1

La comparaison entre comparateur et soustracteurs

[modifier | modifier le wikicode]

Dans ce qui suit, nous partons du principe que l'égalité est testée avec une porte NXOR, et non avec les circuits vus précédemment, mais c'est seulement pour simplifier les schémas. Le circuit utilise trois portes logiques : une pour tester l'égalité, une pour la supériorité, une pour l'infériorité.

Comparateur de magnitude 1 bit.
Comparateur de magnitude 1 bit, alternatif.
Demi-soustracteur.

Il est intéressant de comparer ce circuit avec les circuits soustracteurs et additionneurs. Pour rappel, l'image ci-contre est le circuit qui fait une soustraction sur deux bits, un demi-soustracteur. Vous remarquerez qu'il ressemble beaucoup à un comparateur-1 bit. Précisément, un demi-soustracteur est équivalent à un comparateur 1 bit qui teste les deux conditions suivantes : , . Et avec ces deux conditions, on peut retrouver toutes les autres !

a < b
0 0 a = b
0 1 a > b
1 0 Impossible
1 1 a < b

Il y a donc un lien assez fort entre soustraction et comparaison. Et c'est normal, car une retenue est générée quand  ! D'ailleurs, dans les processeurs modernes, les comparateurs sont fabriqués en modifiant un soustracteur. Nous verrons comment dans la section adéquate.

Les comparateurs spécialisés

[modifier | modifier le wikicode]

Dans cette section, nous allons voir des comparateurs assez simples, plus simples que les autres, qui 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 porte logique combine les résultats de ces comparaisons individuelles et en déduit le bit de sortie. Les comparateurs que nous allons voir sont : un comparateur d'égalité qui vérifie si deux nombres sont égaux, et un comparateur qui vérifie si une opérande vaut zéro.

Le comparateur avec zéro

[modifier | modifier le wikicode]

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. La quasi-totalité des représentation binaire utilisent la valeur 0000...0000 pour encoder le zéro. Les entiers non-signés et en complément à deux sont dans ce cas, c'est la même chose en complément à un ou en signe-magnitude si on omet le bit de signe. La seule exception est la représentation one-hot, et encore !

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 comparateur d'égalité

[modifier | modifier le wikicode]

Passons maintenant à un circuit qui compare deux nombres et vérifie s'ils sont identiques/différents. Les deux sont l'inverse l'un de l'autre, aussi nous allons nous concentrer sur le comparateur d'égalité. Intuitivement, on se dit que deux nombres sont égaux s'ils ont la même représentation en binaire, ils sont différents sinon. Mais cela ne marche pas pour toutes les représentations d'entiers signés. La raison est que certaines ont deux zéros : un zéro positif et un zéro négatif. Mais laissons ces représentations de côté pour le moment.

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. La moindre différence entre deux bits entraîne l'inégalité. Pour tester la différence de deux bits, une porte XOR suffit. 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.

Voici une autre interprétation de ce circuit : le circuit compare un circuit XOR et un comparateur avec zéro. Il faut savoir que lorsqu'on XOR un nombre avec lui-même, le résultat est zéro. Et c'est le seul moyen d'obtenir un zéro comme résultat d'un XOR. Donc, les deux nombres sont XOR et un comparateur vérifie si le résultat vaut zéro. Le circuit final est donc un paquet de portes XOR, et un comparateur avec zéro.

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, en remplacant la porte ET par une porte NAND (ou la porte NOR par une porte OU).

Il existe des comparateurs d'égalité sériel, qui font la comparaison bit par bit, sur le même modèle que l'additionneur 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 très simple : la bascule n'a besoin que de mémoriser un seul bit, la comparaison d'égalité est réalisée par une porte NXOR, son résultat est combiné avec le contenu de la bascule avec une porte ET.

Comparateur d'égalité sériel.

Les comparateurs basés sur un soustracteur

[modifier | modifier le wikicode]

Les comparateurs présents dans les ordinateurs modernes sont des soustracteurs modifiés. Une comparaison est implémentée avec une soustraction, dont on ne conserve pas le résultat. Une fois le résultat de la soustraction a - b connu, on regarde le signe du résultat, pour vérifier s'il est positif, nul ou négatif. Ce qui permet de tester respectivement la supériorité, l'égalité et l'infériorité. Les autres conditions s'en déduisent.

La génération des conditions

[modifier | modifier le wikicode]

Plus haut, j'ai dit qu'un comparateur soustrait les deux opérandes et regardait si le résultat est positif, nul ou négatif. Il nous faut donc déterminer si le résultat est positif, nul ou négatif. Toute la difficulté est de déterminer si le résultat est positif, négatif ou nul. Pour le déterminer, on analyse quatre propriétés intéressantes du résultat : s'il vaut zéro 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 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.

Dans ce qui va suivre, nous allons noter les quatre bits intermédiaires comme suit :

  • 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).

Le comparateur est donc composé de trois sous-circuits : le soustracteur, un circuit qui calcule les 4 bits intermédiaires, puis un autre qui calcule les conditions. La génération des 4 bits intermédiaire demande peu de calculs. Déterminer si le résultat est nul demande juste d'utiliser un comparateur avec zéro. Le bit de signe est tout simplement le bit de poids fort. La génération des bits de débordement a été vue dans le chapitre sur les circuits d'addition et de soustraction, 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.

Comparateur fabriqué avec un soustracteur.

Le bit Z indique que le résultat est nul, ce qui permet de détecter que deux opérandes sont égales. Déterminer si le résultat est nul ou non se fait de la même manière en complément à deux et avec des entiers non-signés, car il n'y a pas de zéro signé comme en signe-magnitude. Le bit de signe et les bits de débordement permettent de calculer la condition d'infériorité, à savoir si la première opérande est inférieure à la seconde ou non. Les autres conditions se déduisent en combinant ces deux bits. Et pour cela, on procède différemment entre le complément à deux et les opérandes non-signées.

Avec des opérandes non-signées, il n'y a pas de bit de signe. Pour déterminer si le résultat est négatif, il faut regarder si la soustraction génère une retenue sortante, un débordement entier non-signé. Si un débordement entier a lieu lors de la soustraction, le résultat est négatif : 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. En clair, le cas où "C = 1" implique que "A < B". Inversement, s'il vaut 0, alors on a "A >= B". Pour les conditions restantes, il suffit de combiner les deux résultats précédents avec le bit Z.

Opérandes non-signées
Z C A < B A = B
0 0 0 0
0 1 1 0
1 0 0 1
1 1 Impossible

Voyons maintenant le cas des opérandes en complément à deux. Avec ces opérandes, le bit de signe dit si le résultat est négatif ou non, mais ne distingue pas un résultat positif d'un résultat nul. Pour cela, on doit ajouter un comparateur avec zéro, qui vérifie si le résultat est nul ou non. La logique a l'air de marcher, mais il y a un petit problème : il faut tenir compte les débordements d'entier en complément à deux. Mais restons sur des opérandes non-signées.

Pour la comparaison en complément à deux, le principe est le même : on regarde si le résultat est positif, nul ou négatif. Il faut regarder le signe du résultat, mais aussi regarder les débordements d'entier (sous-entendu, un débordement en complément à deux). Et c'est intuitif quand on sait qu'un débordement d'entier en complément à deux correspond au cas où le bit de signe a été inversé par un calcul. La règle est la suivante : le résultat est négatif si le bit de signe vaut 1 (donc résultat négatif) avant un débordement d'entier. Donc soit il n'y a pas eu de débordement et le bit de signe vaut 1, soit il y a eu un débordement et il est passé à 0. Le tout se détecte simplement en faisant un XOR entre le bit de débordement et le bit de signe.

Circuit de comparaison entière en complément à 2.

Le tout est résumé dans le tableau suivant :

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 complet

[modifier | modifier le wikicode]

Il est possible de raffiner ce circuit, en ajoutant un multiplexeur en sortie, afin de choisir une 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. Pour configurer le multiplexeur, le circuit possède une entrée de commande sur laquelle on envoie un nombre, qui permet de choisir la condition à tester. Par exemple, pour vérifier si deux nombres sont égaux, on envoie sur l'entrée de commande le nombre qui correspond au test d'égalité.

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 avantages du circuit comparateur-soustracteur

[modifier | modifier le wikicode]

Utiliser un soustracteur pour les comparaisons a de nombreux avantages. Les processeurs modernes supportent l'addition, la soustraction, les comparaisons et potentiellement d'autres opérations. Utiliser le soustracteur pour faire des comparaisons permet de se passer d'un circuit comparateur en plus du soustracteur. mutualiser les deux circuits entraine une économie de portes logiques assez conséquente. Un autre avantage est est plus rapide, car le soustracteur peut utiliser des techniques d'anticipation de retenue pour accélérer les calculs, au lieu de traiter les opérandes colonne par colonne. 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.

Si on veut un circuit comparateur isolé, qui n'est pas fusionné avec un soustracteur, il est possible de prendre un soustracteur et de retirer les portes inutiles. En effet, on n'a pas besoin de calculer le résultat proprement dit. Vérifier si les deux opérandes sont égales peut se faire sans utiliser le résultat de la soustraction, cela demande juste d'utiliser un comparateur d'égalité entre les deux opérandes. Le calcul des trois autres bits intermédiaires se fait avec un soustracteur castré, qui calcule et propage les retenues, mais ne calcule les bits du résultat proprement dit (sauf le bit de signe). Concrètement, cela demande juste de retirer quelques portes XOR, une par soustracteur complet. Les portes XOR utilisées pour tester l'égalité peuvent être mutualisées avec le soustracteur castré.

Les comparateurs pour opérandes entiers

[modifier | modifier le wikicode]

Dans ce qui suit, nous allons créer des comparateurs qui n'utilisent pas de soustraction. Ils sont composés de comparateurs 1-bit. Nous avons vu que ces comparateurs de 1 bit sont semblables à des demi-soustracteurs ou des demi-additionneurs complets, et cela se ressent sur la manière dont on combine leurs résultats. Il est possible de créer des comparateurs sériels, à propagation de retenue ou parallèle, sur le même modèle que les additionneurs/soustracteurs.

Les comparateurs pour entiers non-signés

[modifier | modifier le wikicode]

Il existe un équivalent pour les comparaisons de l'additionneur à propagation de retenue. Il traite les bits les uns après les autres, avec un comparateur 1-bit par colonne. Pour vérifier quelle opérande est supérieure, on part du bit de poids fort. Tant que les bits des opérandes sont égaux, on passe à la colonne précédente. Le premier bit rencontré qui est différent entre les deux opérandes donne le résultat. Un inconvénient de cette méthode est que le résultat est lent à calculer, car on doit traiter les opérandes colonne par colonne, comme pour les additionneurs.

Pour donner un exemple, prenons le cas d'un comparateur qui vérifie si le premier opérande A est supérieur au second opérande B. La comparaison se fait bit par bit, en partant du bit de poids fort. Il y a alors deux cas : soit le bit An est supérieur au bit Bn, soit ils sont égaux. Dans le premier cas, la réponse est immédiate. Dans le second, le résultat est celui obtenu à la colonne précédente. Le circuit est donc composé en enchainant plusieurs briques de base, une par colonne, qui ressemblent à ceci :

Comparateur de supériorité 1 bit

Vérifier l'infériorité au lieu de la supériorité se fait avec un circuit très similaire. Il suffit de remplacer la porte logique qui vérifie "A > B" par une qui vérifie "A < B".

Le processeur HP Nanoprocessor, un des tout premiers microprocesseurs, vérifiait infériorité et supériorité. Il utilisait des comparateurs 1 bit qui fournissaient deux résultats : "A > B" et "A < B", chacun contenant de quoi combiner son résultat avec le résultat de la colonne précédente. Une différence est qu'il propageait ses résultats dans l'autre sens : il envoyait les deux résultats "A > B" et "A < B" à la colonne précédente. Nous ne détaillerons pas ici le circuit de combinaison, surtout qu'il suffit d'écrire sa table de vérité pour le concevoir. Un tel circuit n'a pas l'air très optimisé, mais il était économe en portes logiques.

Comparateur série

Il est possible de créer un comparateur configurable, qui vérifie soit l'infériorité, soit la supériorité, suivant le bit envoyé sur une entrée de commande. Pour cela, il suffit de remplacer la porte logique mentionnée avant. L'idée est d'avoir une porte qui vérifie "A > B", une autre qui vérifie qui vérifie "A < B" et un multiplexeur pour choisir entre les deux. Les multiplexeurs des différentes colonnes sont tous commandés par le même signal de commande.

Comparateur configurable 1 bit

Les comparateurs précédents ont le même défaut que l'additionneur à propagation de retenue, à savoir que l'on doit traiter toutes les colonnes une par une, avant de donner un résultat. Le temps de calcul est donc proportionnel à la taille des opérandes. Mais il est possible de ruser, comme pour les additionneurs, en faisant des calculs en parallèle. L'idée est d'assembler plusieurs comparateurs simples pour traiter des nombres plus longs. Par exemple, on peut combiner 2 comparateurs 4 bits pour obtenir un comparateur 8 bits. Comme pour les additionneurs/soustracteurs, il y a deux possibilités : soit on enchaine les comparateurs en série, soit on les fait travailler en parallèle.

Interface d'un comparateur parallèle avec "retenues".
Comparateur entier parallèle.

Le comparateur sériel est construit sur le modèle de l'additionneur-soustracteur sériel. Mais un tel circuit est rarement utilisé, pour une raison simple : il est possible de modifier un additionneur-soustracteur sériel de manière à ce qu'il fasse des comparaisons. Le cout en circuits est ridicule, à peine une dizaine de portes logiques, pour un gain en fonctionnalités drastique. Personne n'a intérêt à utiliser un comparateur sériel quand il peut ajouter quelques portes pour supporter l'addition et la soustraction en plus. Aussi, tout ce qui suit étudiera des comparateurs dit parallèles, à savoir non-sériels.

Comparateur sériel.

Le comparateur signe-magnitude

[modifier | modifier le wikicode]

Les comparateurs précédents comparaient des opérandes non-signées, à savoir nulles ou positives, mais pas négatives. Et comparer deux opérandes signées n'est pas la même chose que comparer deux nombres non-signés, sauf éventuellement pour la comparaison d'égalité/différence. Et 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. Avec les représentations en complément à un ou à deux, le seul moyen pratique de faire une comparaison est d'utiliser une soustraction. Aussi, nous ne verrons que la comparaison en signe-magnitude.

Effectuer une comparaison entre deux nombres en signe-magnitude demande de comparer les valeurs absolues, mais il faut aussi tenir compte des bits de signe. Le comparateur en sgine-magnitude contient donc un circuit comparateur, qui compare les valeurs absolues. Le résultat de cette comparaison est alors combiné avec les bits de signe par un circuit combinatoire assez simple. Le circuit en question prend 4 bits : deux pour le résultat de la comparaison, et les deux bits de signe.

Comparateur en signe-magnitude

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.

Pour des opérandes de même signe, la comparaison des valeurs absolues ne donne pas immédiatement le résultat. En effet, il y a un cas problématique : les opérandes négatives. Si je compare - 2 et -5, la comparaison des valeurs absolues me dit que 5 > 2. Pourtant, le résultat est que - 2 > -5, soit, l'inverse. Et le même phénomène a lieu quand on compare deux opérandes négatives. Intuitivement une solution est d'inverser les résultats des comparaisons de supériorité/infériorité. Mais il faut prendre en compte le cas où les deux opérandes sont nulles, ce qui biaise le résultat.

Voici la table de vérité de ce circuit, simplifiée :

Bit de signe de A Bit de signe de B Résultats de la comparaison Sortie < Sortie >
0 (+) 0 (+) X Y X Y
1 (-) 0 (+) X Y 0 1
0 (+) 1 (-) X Y 1 0
1 (-) 1 (-) X Y
Notons que la dernière ligne ne tient pas compte du cas où les deux valeurs absolues sont nulles.


À cet instant du cours, nous ne disposons que d'additionneurs 2-opérandes, à savoir qu'ils additionnent deux nombres. Il est maintenant temps de voir les additionneurs qui additionnent plus de deux opérandes en même temps. Additionner plus de deux opérandes est appelé une addition multiopérande, terme que nous utiliserons dans la suite de ce cours. C'est l'opération à la base de la multiplication binaire, mais aussi d'autres opérations, comme certaines opérations vectorielles utilisées dans le rendu 3D (les moteurs de jeux vidéo). Nous aurions d'ailleurs pu en parler dans le prochain chapitre sur la multiplication, mais nous en avons fait un chapitre propédeutique séparé. Nous allons voir qu'il y a différents types d'additionneurs multiopérandes, qui sont implémentés avec des circuits très différents.

L'additionneur multiopérande itératif

[modifier | modifier le wikicode]

Ladditionneur multi-opérande itératif additionne les opérandes une par une, le résultat temporaire étant stocké dans un registre. Il est composé d'un additionneur 2-opérandes couplé à un registre dit accumulateur, terme qui trahit bien son rôle dans le circuit. Le tout entouré de circuits de contrôle (non-représentés dans les schémas suivants), qui se résume souvent à un simple compteur initialisé avec le nombre d'opérandes à additionner.

Additionneur multi-opérande itératif.

Un avantage de ce circuit est qu'il gère un nombre d'opérandes 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). 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. Un additionneur itératif a juste besoin d'une entrée, il envoie une nouvelle opérande à chaque cycle sur l'entrée adéquate.

Les additionneurs multiopérande sériels et parallèles

[modifier | modifier le wikicode]
Additionneur multiopérande de 4 bits, pour 3 opérandes.

Si les additionneurs itératifs peuvent gérer un nombre arbitraire d'opérandes, ce n'est pas le cas des autres additionneurs que nous verrons dans ce chapitre. Ils additionnent toujours le même nombre d'opérandes. En conséquence, leur interface est totalement différente. Ils ont autant d'entrées qu'ils ont d'opérandes. L'un d'entre eux est illustré ci-contre, pour l'addition de trois opérandes de 4 bits.

Une implémentation naïve enchaine plusieurs additionneurs , en les mettant l'un après l'autre. Un premier additionneur additionne le deux premières opérandes, l'additionneur suivante additionne la troisième opérande au résultat de l'additionneur précédente, un troisième additionneur additionne la quatrième opérande au résultat précédent, et ainsi de suite. L'implémentation la plus simple utilise des additionneurs à propagation de retenue, mais d'autres additionneurs peuvent en théorie être utilisés. Voici le circuit d'un additionneur série de 3 opérandes de 4 bits, basé sur des additionneurs à propagation de retenue :

Multiplieur en chaine fait avec des additionneurs à propagation de retenues.
Additionneur multiopérande série versus parallèle.

Bizarrement, mettre des additionneurs à propagation de retenue en série est la solution qui donne les meilleures performances, tout en économisant beaucoup de portes logiques. Les performances sont même meilleures qu'un utilisant des additionneurs à anticipation de retenue ! Et vous avez peut-être eu l'idée de mettre les additionneurs en parallèle, comme indiqué dans le schéma ci-contre, mais même cette solution est moins performante !

Sans rentrer dans des calculs de complexité algorithmique, la raison est liée au fait que la performance est limitée par la propagation des retenues. Avec des additionneurs à propagation de retenue, les retenues se propagent rapidement entre couches d'additionneurs, chaque additionneur peut commencer ses calculs à peine un cycle après le précédent. Par contre, avec les autres additionneurs possibles, la propagation des retenues entre les additionneurs ne se fait pas de manière optimale.

Avec des additionneurs à propagation de retenue en série, le temps est proportionnel à . Avec les additionneurs à anticipation de retenue en parallèle, le temps de calcul est proportionnel à . Ce qui est plus grand que si N et n sont assez grands.

L'addition carry save

[modifier | modifier le wikicode]

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. 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 | modifier le wikicode]

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.

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.

Le tout donne la table de vérité de 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 naïfs à plus de 3 opérandes

[modifier | modifier le wikicode]

Pour additionner plus de trois opérandes, l'idée est d'additionner les opérandes par groupes de trois, avec une addition carry save par groupe, avant de combiner les résultats carry save entre elles. Les additionneurs précédents peuvent être adaptés de manière à utiliser des additionneurs carry save, en lieu et place d'additionneurs 2-opérandes normaux. La seule contrainte est de faire attention au poids des bits à additionner (les retenues doivent être décalées d'un cran avant l'addition).

Par exemple, un additionneur itératif peut utiliser un additionneur carry save pour additionner deux opérandes par cycle, au lieu d'une seule. Le circuit obtenu est le suivant :

Additionneur multi-operande itératif en carry save

Il est aussi possible de faire la même chose avec un additionneur multiopérande sériel, où plusieurs additionneurs simples sont enchainés l'un après l'autre. En remplaçant les additionneurs 2-opérandes par des additionneurs carry save, le circuit devient tout de suite plus rapide.

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.

Les additionneurs carry save organisés en arbre d'additionneurs complets

[modifier | modifier le wikicode]

Avec des additionneurs 2-opérande, utiliser des additionneurs à propagation de retenue était la meilleure solution. Mais ce n'est plus le cas avec des additionneurs carry save. Une organisation en arbre, avec des additions faites en parallèle, devient plus rapide. Et il existe de nombreuses manières pour construire un arbre d'additionneurs. Les deux méthodes les plus connues donnent les additionneurs en arbres de Wallace, ou en arbres Dadda. La première a le meilleur temps de calcul, l'autre est la plus économe en portes logiques.

Les arbres les plus simples à construire sont les arbres de Wallace. Le principe est d'ajouter des couches d'additionneurs carry-save les unes à la suite des autres. Lors de l'ajout de chaque couche, on vise à 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 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.


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 génération des produits partiels est assez simple. Sur le principe, la table de multiplication binaire est un simple ET logique. Générer un produit partiel demande donc, à minima, de faire un ET entre un bit du multiplicateur et le multiplicande. Le circuit pour cela est trivial.

La seconde étape est ensuite de décaler le résultat du ET pour tenir compte du poids du bit choisit. En effet, regarder le schéma de droite qui montre comment faire une multiplication en binaire. Vous voyez que c'est comme en décimal : chaque ligne correspond à un produit partiel, et chaque produit partiel est décalé d'un cran par rapport au précédent. Il faut donc ajouter de quoi faire ce décalage. Intuitivement, on se dit qu'il faut ajouter des circuits décaleurs, un pour chaque bit du multiplicateur. Ce ne sera pas toujours le cas, mais il y en aura parfois besoin.

Nous allons d'abord commencer par les multiplieurs qui font de la multiplication non-signée. La multiplication de deux nombres signés est en effet un peu particulière et demande des techniques particulières, là où la multiplication non-signée est beaucoup plus simple.

Les multiplieurs non-itératifs

[modifier | modifier le wikicode]

Les multiplieurs non-itératifs calculent 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. Notons que la génération des produits partiels se passe de circuits décaleur, elle se contente d'utiliser un paquet de portes ET. Le câblage permet de câbler les sorties des portes ET aux bonnes entrées de l'additionneur, ce qui permet de se passer de circuits décaleurs.

Multiplieur en arbre.

Les multiplieurs diviser pour régner sont un autre type de multiplieur non-itératif. 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.

Les multiplieurs itératifs

[modifier | modifier le wikicode]

Les multiplieurs itératifs 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 deux registres. Un multiplieur itératif 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éterminer avec un compteur).

Circuit itératif de multiplication sans optimisation.

Rappelons que l'additionneur multiopérande itératif est composé d'un additionneur à deux opérandes, couplé à un registre accumulateur. Il mémorise le résultat temporaire de l'addition des produits partiels. A la fin de la multiplication, l'accumulateur contient le résultat.

Circuit itératif de multiplication sans optimisation, détaillée.

Les deux types de multiplieurs itératifs

[modifier | modifier le wikicode]

Les multiplieurs itératifs différent par le sens de parcours du multiplicateur : certains traitent les bits du multiplicateur de droite à gauche, les autres dans le sens inverse. Précisément, le traitement se fait soit des bits de poids faible vers les bits de poids fort, ou inversement des bits de poids fort vers les bits de poids faible. Pour cela, on stocke le multiplieur dans un registre à décalage, le bit qui en sort à chaque cycle est utilisé pour générer le produit partiel.

Il faut noter que le contenu du registre accumulateur est aussi décalé d'un cran vers la gauche ou la droite à chaque cycle. Rappelez-vous que les produits partiels ne sont pas alignés sur une même colonne : ils sont chacun décalés d'un cran par rapport au précédent. Vu qu'on additionne les produits partiels un par un, on doit donc faire un décalage d'un cran entre chaque addition. La solution idéale effectue ce décalage directement dans le registre accumulateur, qui devient alors un registre à décalage ! Avec cette technique, les produits partiels générés ont la même taille que le multiplicateur, le même nombre de bits. On économise pas mal de circuit : pas besoin de circuits décaleur, moins d'entrées sur l'additionneur.

Le sens de décalage du multiplicateur et du registre accumulateur sont identiques. Si on effectue la multiplication de droite à gauche, en commençant par les bits de poids faible, alors le registre accumulateur est aussi décalé vers la droite. Cela permet au produit partiel suivant d'être placé un cran à gauche du précédent. A l'inverse, si on effectue la multiplication en commençant par les bits de poids fort, alors on décale le registre accumulateur vers la gauche, histoire de placer un produit partiel à la droite du précédent. La seconde solution a un avantage qu'on va expliquer.

Prenons un multiplicateur de N bits. Avec une multiplication qui commence par les bits de poids fort, l'addition donne un résultat sur 2N bits, la totalité d'entre eux étant utiles. En allant dans l'autre sens, l'addition donne un résultat qui a N bits effectifs, à savoir que le reste sont systématiquement à zéro et sont en réalité pris en charge par le décalage de l'accumulateur. Commencer par les bits de poids faible permet d'utiliser des produits partiels sur n bits, donc d'utiliser un additionneur sur N bits. Les produits partiels aussi sont de N bits. Le registre accumulateur reste de 2N bits, mais seuls les N bits de poids fort sont utilisés dans l'addition.

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 accumulateur 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é

Voici ce que donne une multiplication commençant par les bits de poids faible.

Fonctionnement multiplieur.

Les optimisations liées aux opérandes

[modifier | modifier le wikicode]

Les circuits précédents peuvent incorporer des optimisations liées à la valeur des opérandes. Avec ces optimisations, la multiplication sera plus ou moins rapide suivant l'opérande : certaines opérandes donneront une multiplication en 32 cycles, d'autres en 12 cycles, d'autres en 20, etc.

La première optimisation consiste à terminer l'opération une fois que le multiplieur décalé atteint 0. Dans ce cas, on a multiplié tous les bits à 1 du multiplieur, tous les produits partiels restants valent 0, pas besoin de les calculer. L'optimisation ne marche cependant que si on commence les calculs à partir du bit de poids faible du multiplieur. Si on commence par les bits de poids fort, il faudra faire plusieurs décalages pour obtenir le bon résultat. Par exemple, si les N bits de poids faible du multiplieurs valent 0, alors il faudra décaler le résultat dans le registre accumulateur de N rangs vers la gauche.

Une seconde optimisation commence la multiplication pour le premier bit du multiplieur à 1, et zapper les 0 de poids fort précédents. Ce faisant, on calcule le nombre de 0 de poids fort avec un circuit adéquat (un circuit de count leading zeros), puis décale le multiplieur et le reste partiel de ce nombre. Les zéros de poids faible sont eux traités en simplement comparant le multiplieur avec zéro. La même optimisation s'applique si on commence la multiplication à partir du bit de poids faible, il faut cependant compter les 0 de poids faibles pour savoir de combien décaler vers la droite.

Les multiplieurs en base 4, 8, 16

[modifier | modifier le wikicode]

Avec les multiplieurs itératifs précédents, la multiplication se fait produit partiel par produit partiel. À l'inverse, avec les multiplieurs non-itératifs, on génère tous les produits partiels en même temps, pour les additionner. Il existe cependant des multiplieurs intermédiaires, qui génèrent et additionnent plusieurs produits partiels à la fois, tout en restant des multiplieurs itératifs. L'idée est de générer deux, trois, quatre produits partiels en même temps et de les additionner au résultat temporaire, avec un additionneur multiopérandes. On parle alors de multiplieurs en base 4, 8, ou 16. Le multiplieur en base 4 génère deux produits partiels à la fois, celui en base 8 en génère 3, celui en base 16 en génère 4, etc.

Il existe plusieurs manières de fabriquer un multiplieur en base 4, 8, 16, etc. La première, la plus simple, utilise un multiplieur hybride, qui mélange un multiplieur itératif et un autre non-itératif. La seconde, plus complexe, modifie le circuit d'un multiplieur itératif en rajoutant des circuits annexes.

Les multiplieurs hybrides

[modifier | modifier le wikicode]

Une solution, assez évidente, rajoute des circuits de génération des produits partiels et on remplace l'additionner normal par un additionneur multiopérande. Voici ce que cela donne quand on prend deux produits partiels à la fois :

Multiplieur en base 4

La seule difficulté est de gérer les décalages des registres. Dans l'exemple avec deux produits partiels, vu qu'on traite deux bits à la fois, on doit décaler le registre accumulateur de deux rangs. De plus, les deux bits du multiplieur utilisés n'ont pas le même poids. Un des produits partiel doit être décalé d'un rang par rapport à l'autre. En théorie, on devrait user d'un circuit décaleur, mais on peut s'en passer avec des bidouilles de câblage. La même chose a lieu quand on génère trois produits partiels à la fois : l'un n'est pas décalé, le suivant l'est d'un rang, l'autre de trois rangs. Et ainsi de suite avec quatre produits partiels simultanés. Rien d'insurmontable en soi, cela ne fait que marginalement complexifier le circuit.

Il est possible d'optimiser le circuit en faisant les additions en carry save uniquement, sans passer par un résultat temporaire en binaire. Pour cela, il faut déplacer l'additionneur normal après le registre accumulateur. Le registre accumulateur mémorise alors le résultat temporaire en carry save. Il est donc dupliqué, avec un registre pour les retenues, et l'autre pour la somme. Une fois que tous les produits partiels ont été additionnés, on traduit le résultat temporaire en carry save en binaire normal, avec l'additionneur normal.

Multiplieur itératif en base 4 optimisé.

Le design précédent peut être amélioré en tenant compte d'un détail portant sur le registre accumulateur. Il s'agit d'un registre synchrone, commandé par un signal d’horloge non-représenté dans les schémas précédents. Une implémentation de ce registre utilise des bascules dites master-slave, composées de deux bascules D non-synchrones à entrée Enable qui se suivent, comme nous l'avions vu dans le chapitre sur les circuits synchrones. Le registre synchrone est donc composé de deux registres non-synchrones qui se suivent. Avec ce type de registres, il est possible de modifier le multiplieur précédent de manière à doubler le nombre de produits partiels additionnés à chaque cycle d'horloge. L'idée est très simple : on insère un second additionneur carry save entre les deux registres ! On obtient alors un multiplieur multibeat.

Multiplieur itératif de type multibeat

Les multiplieurs itératifs en base 4, 8, 16

[modifier | modifier le wikicode]

Une implémentation alternative utilise un additionneur 2 opérandes normal, mais précalcule les produits partiels. Prenons l'exemple le plus simple : celui d'un multiplieur en base 4, qui travaille deux produits partiels à la fois. A chaque cycle, il génère deux produits partiels, qui sont additionnés avec le registre accumulateur. Une autre manière de voir les choses est que ces produits partiels sont additionnés entre eux, et le résultat est additionné avec le registre accumulateur. Pour un multiplicande A, la somme des produits partiels vaut : O, A, 2A, 3A. Et c'est cette somme qu'il faut additionner au contenu du registre accumulateur. Les quatre sommes possibles sont toujours les mêmes, et on peut les précalculer et les mémoriser dans des registres dédiés. On peut choisir la bonne somme en fonction des deux bits du multiplieur

Multiplieur itératif non-hybride en base 4

Il est cependant possible de ruser afin d'éliminer certains registres. Par exemple, pas besoin d'un registre pour le 0 : juste d'un circuit de mise à zéro, comme dans n'importe quel circuit de génération de produit partiel. Pareil pour le registre contenant le double du multiplicande : un simple décalage suffit pour le calculer à la volée (une simple bidouille de câblage permet de se passer de circuit décaleur). Seuls restent les registres pour le multiplicande et son triple. Il est généré par l'additionneur normal, en fin de circuit, au tout début de l'addition.

La solution marche aussi quand on veut générer trois produits partiels à la fois, ou quatre, ou cinq, mais deviennent rapidement inutiles. Par exemple, pour générer trois produits partiels à la fois, il faut calculer 0, A, 2A, 3A, 5A et 7A, et calculer le reste à partir de cela. Mais le jeu n'en vaut pas la chandelle. Certes, calculer trois produits partiels à la fois divise par trois le nombre d'additions, sauf que générer à l'avance les produits partiels rajoute quelques additions. Ce qu'on gagne d'un côté, on le perd de l'autre. Autant dire que cette méthode n'est que rarement utilisée, car elle utilise plus de circuits ou sont moins performantes.

La multiplication de nombres signés

[modifier | modifier le wikicode]

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.

Multiplier les valeurs absolues et convertir

[modifier | modifier le wikicode]

Une première solution pour multiplier des entiers signés est simple : on prend les valeurs absolues des opérandes, on multiplie, et on inverse le résultat si besoin. 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. On s’aperçoit qu'on doit inverser le résultat si et seulement si une seule opérande est négative, pas les deux.

Signe du multiplicande Signe du multiplieur Signe du résultat
+ + +
- + -
+ - -
- - +

Pour les entiers en signe-valeur absolue, le calcul est très simple, vu que la valeur absolue et le signe sont séparés. Il suffit de calculer le bit de signe à part, et multiplier les valeurs absolues. En traduisant le tableau d'avant 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 faire un vulgaire XOR entre les bits de signe.

Multiplication en signe-magnitude

Pour les entiers en complément à deux, cette solution n'est pas utilisée. Prendre les valeurs absolues demande d'utiliser deux incrémenteurs et deux inverseurs, sans compter qu'il faut en rajouter un de plus pour inverser le résultat. Le cout en circuits serait un peu gros, sans compter qu'on peut faire autrement.

Les multiplieurs itératifs signés en complément à deux

[modifier | modifier le wikicode]

Pour la représentation en complément à deux, les multiplieurs non-signés 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. Les multiplieurs vus plus haut peuvent gérer la situation so on utilise 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. Cette technique marche très bien que on utilise un multiplieur qui travaille de droite à gauche, avec des décalages à droite.

Pour traiter les multiplicateurs négatifs, le produit partiel correspondant au bit de poids fort doit être soustrait. L'explication du pourquoi est assez dure à comprendre, aussi je vous épargne les détails, mais c'est lié au fait que ce bit a une valeur négative. L'additionneur doit donc être remplacé par un additionneur-soustracteur.

Multiplieur itératif pour entiers signés.

Les multiplieurs de Booth

[modifier | modifier le wikicode]

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 | modifier le wikicode]

La division en binaire se fait de la même manière qu'en décimal : avec une série de soustractions. L'opération implique un dividende, qui est divisé par un diviseur pour obtenir un quotient et un reste.

Implémenter la division sous la forme de circuit est quelque peu compliqué. La difficulté est simplement que chaque étape de la division dépend de la précédente ! Cela réduit les possibilités d'optimisation. Il est très difficile d'utiliser des soustracteurs multiopérande non-itératifs pour créer un circuit diviseur. Pas de problème pour la multiplication, où utiliser un paquet d'additionneurs en parallèle marche bien. La division ne permet pas de faire de genre de choses facilement. C'est possible, mais le cout en circuits est prohibitif.

Les techniques que nous allons voir en premier lieu calculent le quotient bit par bit, elles font une soustraction à la fois. Il est possible de calculer le quotient non pas bit par bit, mais par groupe de deux, trois, quatre bits, voire plus encore. Mais les circuits deviennent alors très compliqués. Dans tous les cas, cela revient à utiliser des diviseurs itératifs, sur le même modèle que les multiplicateurs itératifs, sauf que l’addition est remplacée par une soustraction. Nous commencer par les trois techniques les plus simples pour cela : l'implémentation naïve, la division avec restauration, et sans restauration.

L'implémentation itérative naïve

[modifier | modifier le wikicode]
Division en binaire.

En binaire, l'opération de division est la même qu'en décimal, si on omet que la table de soustraction est beaucoup plus simple. La seule différence est qu'en binaire, à chaque étape, on doit soit soustraire zéro, soit soustraire le diviseur, rien d'autre. Mais pour le reste, tout se passe de la même manière qu'en décimal. À chaque étape, on prend le reste partiel, le résultat de la soustraction précédente, et on abaisse le bit adéquat, exactement comme en décimal.

Sur le principe, général, un diviseur ressemble à ce qui est indiqué dans le schéma ci-dessous. On trouve en tout quatre registres : un pour le dividende, un pour le diviseur, un pour le quotient, et un registre accumulateur dans lequel se trouve le "reste partiel" (ce qui reste une fois qu'on a soustrait le diviseur dans chaque étape).

À chaque étape de la division, on effectue une soustraction, ce qui demande un circuit soustracteur. On soustrait soit le diviseur, soit zéro. Le choix entre les deux est réalisé par un multiplexeur, ou encore mieux : par un circuit de mise à zéro sélectif.

Abaisser le bit suivant demande un peu plus de réflexion. Le bit abaissé appartient au dividende, et on abaisse les bits en progressant du bit de poids fort vers le bit de poids faible. Pour faire cela, le dividende est placé dans un registre à décalage, qui se décale d'un rang vers la gauche à chaque itération. Le bit sortant du registre n'est autre que le bit abaissé. Il suffit alors de le concaténer au reste partiel, avec une petite ruse de câblage, et d'envoyer le tout en opérande du soustracteur. Rien de bien compliqué, il faut juste envoyer le bit abaissé sur l'entrée de poids faible du soustracteur, et de câbler le reste pareil à côté, ce qui décale le tout d'un rang automatiquement, sans qu'on ait besoin de circuit décaleur pour le reste partiel.

Reste ensuite à déterminer le quotient, ce qui est fait par un circuit spécialisé relié au diviseur et au dividende. Au passage, déterminer le bit du quotient permet au passage de savoir si on doit soustraire le diviseur ou non (soustraire zéro). Ce circuit n'est pas relié qu'au registre pour le quotient, mais aussi au multiplexeur mentionné précédemment. Toute la difficulté tient dans la détermination du quotient. En soi, elle est très simple : il suffit de comparer le dividende et le diviseur. Si le dividende est supérieur au diviseur, alors on peut soustraire. S'il est inférieur, on ne soustrait pas, et on passe à l'étape suivante. Si les deux sont égaux, on soustrait.

Circuit diviseur, principe général

L’optimisation de ce circuit la plus intéressante est la mise à l'échelle les opérandes. L'idée est juste de commencer la division au bon moment, en ne faisant pas certaines étapes dont on sait qu'elles vont fournir un zéro sur le quotient. L'idée marche sur les dividendes dont les n bits de poids fort sont à zéro. L'idée est de zapper ces n bits, en décalant le dividende de n rangs au début de la division, vu qu'on sait que ces bits donneront des zéros pour le quotient et pas de reste partiel. Il faut aussi décaler le quotient de n rangs, en insérant des 0 à chaque rang décalé.

L'implémentation itérative sans redondance du soustracteur

[modifier | modifier le wikicode]

Un défaut du circuit précédent est qu'il y a une duplication de circuit cachée. En effet, le circuit de détermination du quotient est un comparateur. Mais un comparateur peut s'implémenter par un circuit soustracteur ! Pour vérifier si un opérande est supérieur, égale ou inférieur à une seconde opérande, il suffit de les soustraire entre elles et de regarder le signe du résultat. On a donc deux circuits soustracteurs cachés dans ce circuit : un pour déterminer le quotient, un autre pour faire la soustraction. Mais il y a moyen de ruser pour éliminer cette redondance.

De plus, retirer cette duplication ne rend pas le circuit plus lent. En n'utilisant qu'un seul soustracteur, on fera la comparaison et la soustraction dans le même soustracteur, l'une après l'autre. Mais avec le circuit ci-dessous, c'est la même chose : on effectue la comparaison pour sélectionner le bon opérande, avant de faire la soustraction. Dans les deux cas, c'est globalement la même chose.

Si on retire la redondance mentionnée dans la section précédente, le circuit reste globalement le même, à un détail près. Chaque étape demande de comparer reste partiel et diviseur pour déterminer le bit du quotient et l'opérande à soustraire, puis faire la soustraction. La comparaison se fait avec une soustraction, et le bit de signe du résultat est utilisé pour déterminer le signe de l'opérande. Chaque étape est donc découpée en deux sous-étapes consécutives : la comparaison et la soustraction. La manière la plus simple pour cela est de faire en sorte que le circuit fasse chaque étape en deux cycles d'horloges : un dédié à la comparaison, un autre dédié à la soustraction proprement dit.

Le circuit doit donc fonctionner en deux temps, et la meilleure manière pour cela est de lui faire faire une étape de la division en deux cycles d'horloges. Certains circuits vont fonctionner lors du premier cycle, d'autres lors du second. Lors du premier cycle, le bit du quotient est déterminé, le multiplexeur est configuré pour pointer vers le diviseur, et le registre du quotient est décalé. Les autres circuits ne fonctionnent pas. Le résultat de la soustraction n'est pas pris en compte, il n'est pas enregistré dans le registre du reste partiel. Lors du second cycle, c'est l'inverse : le multiplexeur est configuré par le bit calculé à l'étape précédente, le résultat de la soustraction est enregistré dans le registre accumulateur, et le registre du dividende est décalé.

Circuit diviseur naif amélioré en stoppant modification de l'accumulateur lors d'une comparaison

On pourrait croire que le circuit de division obtenu est plus lent, vu qu'il a besoin de deux cycles d'horloge pour faire son travail. Mais la réalité est que ce n'est pas forcément le cas. En réalité, on peut très bien doubler la fréquence de l'horloge uniquement dans le circuit de division, qui fonctionne deux fois plus vite que les circuits alentours, y compris ceux auquel il est relié. Par exemple, si le circuit de division est intégré dans un processeur, le processeur ira à une certaine fréquence, mais le circuit de division ira deux fois plus vite. Mine de rien, cette solution a été utilisée dans de nombreux designs commerciaux, et notamment sur le processeur HP PA7100.

La division avec restauration

[modifier | modifier le wikicode]
Division avec restauration.

Un point important pour que l’algorithme précédent fonctionne est que le résultat fournit par le soustracteur ne soit pas pris en compte lors de l'étape de comparaison. Plus haut, la solution retenue était de ne pas l'enregistrer dans le registre du reste partiel. Il s'agit là de la solution la plus simple, mais il existe une solution alternative plus complexe, qui autorise l'enregistrement du reste partiel faussé dans le registre accumulateur, mais effectue une correction pour restaurer le reste partiel tel qu'il était avant la comparaison. C'est le principe de la division avec restauration que nous allons voir dans ce qui suit.

Développons la division avec restauration par un exemple illustré ci-contre. Nous allons cherche à diviser 1000 1100 1111 (2255 en décimal) par 0111 (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 soustraire le diviseur à ce bit, pour 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. On restaure alors le reste partiel antérieur, en ajoutant le diviseur retranché à tort. Ensuite, on abaisse le bit juste à côté du bit qu'on vient de tester, et on recommence. À chaque étape, on restaure le reste partiel si le résultat de la soustraction est négatif, on ne fait rien s'il est positif ou nul.

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 n’aurait pas dû 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. L'algorithme utilise en tout, pour des nombres de N bits, 2N+1 additions/soustractions maximum.

Le seul changement est la restauration du reste partiel. Restaurer le dividende initial demande d'ajouter le diviseur qu'on vient de soustraire. L'algorithme ressemble au précédent, sauf que l'on a plus besoin du multiplexeur, le diviseur est toujours utilisé comme opérande du soustracteur. Sauf que le soustracteur est remplacé par un additionneur-soustracteur. Le circuit de détermination du bit du quotient commande non seulement l'additionneur/soustracteur. Il est beaucoup plus simple que le comparateur d'avant.

Circuit de division.

La division sans restauration

[modifier | modifier le wikicode]

La méthode précédente a toutefois un léger défaut : on a besoin de remettre le reste partiel comme il faut lorsqu'on a soustrait le diviseur décalé alors qu'on aurait pas du et que le résultat obtenu est négatif. La division sans restauration se passe de cette restauration du reste partiel et continue de calculer avec ce reste faux,. Par contre, elle effectue une phase de correction lors du cycle suivant. De plus, il faut corriger le quotient obtenu pour obtenir le quotient adéquat, pareil pour le reste.

Mettons que l'on souhaite soustraire le diviseur du reste partiel, mais que le résultat soit négatif. Au lieu de restaurer le reste partiel initial, on continue, en effectuant une correction au cycle suivant. Il y a donc deux cycles d'horloge à analyser. Au premier, on a le reste partiel R, dont on soustrait le diviseur D décalé de n rangs :

Si le résultat est positif, on continue la division normalement, le cycle suivant implique une soustraction normale, il n'y a rien à faire. Mais si le résultat est négatif, une division normale restaure R, puis poursuit la soustraction. Lors du second cycle, le reste partiel est décalé d'un rang vers la gauche, ce qui donne :

Maintenant, regardons ce qui se passe avec une division sans restauration. On fait la soustraction, on a R - D, qui est négatif. On décale vers la gauche, et on soustrait de nouveau D au second cycle :

Le résultat est incorrect, il faut le corriger pour obtenir le bon résultat. Pour cela, on calcule l'erreur, la différence entre les deux équations précédentes :

La correction demande donc juste de faire une addition du diviseur au cycle suivant.

Un autre point à prendre en compte est l'interprétation des bits du quotient. Avec la division avec restauration, le bit du quotient s’interprète comme suit : 0 signifie que l'on a pas soustrait le diviseur décalé par le poids du bit, 1 signifie qu'on a soustrait. Avec la division sans restauration, l'interprétation est différente : 0 signifie que l'on a additionné le diviseur décalé par le poids du bit, 1 signifie qu'on a soustrait. La différence signifie qu'il faut convertir le quotient de l'un vers l'autre pour obtenir le bon quotient. Pour cela, il faut inverser les bits du quotient, multiplier le résultat par deux et ajouter 1.

Inverser les bits du quotient peut se faire à la volée, lors du calcul, alors que les deux opérations finales se font à la toute fin du calcul, lors du dernier cycle.

Enfin, il faut tenir compte d'un cas particulier : le cas où le reste final est invalide. Cela arrive si on arrive à la fin du calcul, au dernier cycle, et que l'on effectue une soustraction mais que l'on aurait pas dû soustraire. Dans ce cas, on se retrouve avec un reste négatif. Dans ce cas, on est censé poursuivre le calcul encore un cycle pour corriger le résultat, en additionnant le diviseur. Le circuit diviseur doit détecter la situation et effectuer un cycle supplémentaire.

Pour résumer, la division sans restauration :

  • Continue le calcul en cas de reste partiel incorrect, sauf qu'au cycle suivant, on additionne le diviseur au lieu de soustraire ;
  • Inverser les bits du quotient, multiplier le résultat par deux et ajouter 1.
  • Corrige le reste avec l'addition du diviseur si celui-ci devient négatif au dernier cycle.

Les diviseurs améliorés

[modifier | modifier le wikicode]

On peut améliorer toutes les méthodes précédentes en ne traitant pas notre dividende bit par bit, mais en le manipulant par groupe de deux, trois, quatre bits, voire plus encore. Mais les circuits deviennent alors très compliqués. 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, sous peine d'obtenir des résultats erronés. Et si vous croyez que les constructeurs de processeurs n'ont jamais fait cette erreur, sachez qu'Intel en a 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 ! C'est de là que vient le fameux "Pentium FDIV bug".

Il est possible de modifier les circuits diviseurs pour remplacer l'additionneur-soustracteur par un équivalent qui fait les calculs en carry save. Les calculs sont alors drastiquement accélérés. Mais le circuit devient alors beaucoup plus complexe. Le calcul du quotient, qui demande un comparateur, est difficile du fait de l'usage de la représentation carry save.

De nos jours, les diviseurs utilisent une version améliorée de la division sans restauration, appelé l'algorithme de division SRT. C'est cette méthode qui est utilisée dans les processeurs pour la division entière ou la division flottante.


Après avoir vu les circuits de calcul pour les nombres entiers, il est temps de voir les circuits de calculs pour des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.

Unité de calcul flottante, intérieur

Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la Floating Point Unit, ce qui se traduirait en unité de calcul flottante. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs.

Les multiplications/divisions flottantes

[modifier | modifier le wikicode]

Paradoxalement, les multiplications, divisions et racines carrées sont plus simples que l'addition, avec des nombres flottants. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les additions et soustractions, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.

Avant le calcul, il y a une étape de prénormalisation, qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux), puis l'ajoute aux mantisses. Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.

Les circuits multiplieurs/diviseurs flottants

[modifier | modifier le wikicode]

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, ce qui demande un additionneur-soustracteur et un multiplieur. Il faut cependant penser à plusieurs choses pas forcément évidentes.

  • Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
  • Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
  • Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
  • Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis, dont on parlera plus bas.
Multiplieur flottant avec normalisation

La division fonctionne sur le même principe que la multiplication, si ce n'est que 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 :

La multiplication entière réalisée par l'unité de calcul flottante

[modifier | modifier le wikicode]

Vous remarquerez qu'un multiplieur flottant contient un multiplieur entier pour multiplier les mantisses. Sachez qu'il est possible de n'utiliser qu'un seul multiplieur entier pour faire à la fois les multiplications entières et flottantes. Cette optimisation a été utilisée sur plusieurs processeurs commerciaux. Par exemple, les processeurs Atom d'Intel utilisaient cette optimisation : les multiplications entières étaient réalisées dans un multiplieur entier partagé avec l'unité de calcul flottante. Il en est de même sur les processeurs Athlon d'AMD, sortis dans les années 2000.

Mais cela ne fonctionnait que sur les processeurs 32 bits, pas sur les 64 bits. Pour comprendre pourquoi, il faut savoir que les processeurs modernes gèrent des nombres flottants simple et double précision, à savoir codés sur 32 et 64 bits. Il y a un seul multiplieur, capable de gérer des flottants double précision, qui peut le plus peut le moins. Avec des flottants double précision de 64 bits, les mantisses font 52 bits, ce qui fait un multiplieur de 53 bits. C'est suffisant pour un processeur 32 bits, pas assez sur un processeur 64 bits.

Ajoutons cependant une petite nuance. Sur les anciens processeurs x86 des PC, les flottants faisaient 80 bits, avec une mantisse de 64 bits, ce qui est assez à la fois pour les processeurs 32 et 64 bits. Malheureusement, les processeurs 64 bits avaient un budget en transistor suffisant pour ne pas appliquer cette optimisation. Pour des raisons diverses, il était préférable d'avoir un multiplieur entier séparé du multiplieur flottant.

L'addition et la soustraction flottante

[modifier | modifier le wikicode]

La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.

Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).

Circuit d'addition et de soustraction flottante.

Le circuit de pré-normalisation

[modifier | modifier le wikicode]

La mise des deux opérandes au même exposant s'appelle la pré-normalisation. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.

Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il 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.

Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.

Circuit de prénormalisation d'un additionneur flottant

La normalisation et les arrondis

[modifier | modifier le wikicode]

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, et doit auissi passer par d'autres étapes dites de normalisation.

Normalisation in circuit

Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.

La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.

La normalisation

[modifier | modifier le wikicode]

La normalisation gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.

Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 0000. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !

Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (Count Leading Zero). Ce circuit permet aussi de détecter si la mantisse vaut zéro.

Circuit de normalisation.

Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d'arrondi. 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 ce qui s'appelle une mémoire de précalcul. Avec cette technique, le circuit combinatoire est remplacé par une ROM équivalente. Les entrées du circuit combinatoire deviennent l'entrée d'adresse de la ROM, les sorties du circuit deviennent la sortie de donnée de la ROM. Pour une entrée identique, les deux donneront les mêmes sorties.

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.

Les flottants logarithmiques

[modifier | modifier le wikicode]

Maintenant, nous allons fabriquer une FPU 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. L'utilité de cette représentation est de simplifier fortement les multiplications et divisions, quitte à perdre en performance sur les additions/soustractions.

Pour commencer, il faut savoir que 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, sauf que l'addition est remplacée par une soustraction :

Pour l'addition et la soustraction, le calcul se fait avec cette formule :

, avec F une fonction appelée le logarithme gaussien.

Pour la soustraction, on a la même chose, sauf que la fonction change, ce qui donne :

, avec G une fonction différente de F.

Le calcul demande donc de soustraire les deux opérandes, de calculer la fonction F ou G, puis d'additionner la première opérande. Le toute demande donc un soustracteur, un additionneur et une mémoire de précalcul pour la fonction F/G. Il est possible de mutualiser les additionneurs pour la multiplication et l'addition, en rajoutant quelques multiplexeurs.

Unité de calcul logarithmique


Les circuits qui effectuent des opérations trigonométriques existent. Ils sont un peu plus complexes que les circuits de calcul flottants basiques, ce qui explique sans doute qu'ils soient peu utilisés dans les ordinateurs actuels. De plus, 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 plus courants dans les systèmes embarqués, bien que leur utilisation reste quand même peu fréquente.

Malgré leur rareté, il est intéressant de voir comment sont conçus ces circuits de calcul trigonométrique. Il existe des circuits de calcul trigonométrique en virgule fixe, d'autres en virgule flottante. Les calculs trigonométriques ou transcendantaux sont surtout utilisés avec des nombres flottants, le cas avec des nombres à virgule fixe étant plus rare. Une partie des techniques que nous allons voir marche aussi bien avec des flottants qu'avec des nombres à virgule fixe. D'autres sont spécifiques aux nombres à virgule fixe, d'autres aux flottants. Nous préciserons du mieux que nous pouvons si telle ou telle technique marche avec les deux ou un seul.

Précisons que ce chapitre est facultatif, dans le sens où il n'introduit pas de concept ou de circuits nécessaires pour la suite de ce cours. Je recommande d'aborder ce chapitre comme s'il s'agissait d'une annexe, pour ceux qui sont vraiment motivés.

L'algorithme CORDIC

[modifier | modifier le wikicode]

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. Il fonctionne sur des nombres à virgule fixe, et plus précisément des nombres à virgule fixe codés en binaire. Mais il existe des variantes conçues pour fonctionner avec des nombres à virgule fixe codés en BCD.

CORDIC 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 | modifier le wikicode]

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 | modifier le wikicode]

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)

L'approximation par un polynôme

[modifier | modifier le wikicode]

Les premiers processeurs Intel, avant le processeur Pentium, utilisaient l'algorithme CORDIC pour calculer les fonctions trigonométriques, logarithmes et exponentielles. Mais le Pentium 1 remplaça CORDIC par une autre méthode, appelée l'approximation polynomiale. L'idée est de calculer ces fonctions avec une suite d'additions/multiplications bien précises. Précisément, le circuit calcule un polynôme de la forme a x + b x^2 + c x^3 + d x^4, + ... Les coefficients a,b,c,d,e,... sont choisit pour approximer au maximum la fonction voulue.

Si vous avez déjà lu des livres de maths avancés, vous aurez peut-être pensé à utiliser les séries de Taylor, mais celles-ci donnent rarement de bons résultats en pratiques, ce qui fait qu'elles ne sont pas utilisées. A la place, les fonctions sont approximées avec des polynômes conçus pour, qui ressemblent aux séries de Taylor, mais dont les coefficients sont un peu différents. Les coefficients sont calculés via un algorithme appelé l'algorithme de Remez, mais nous ne détaillerons pas ce point, qui va bien au-delà du cadre de ce cours.

Les coefficients sont mémorisés dans une mémoire ROM spécialisée, avec les coefficients d'une même opération placés les uns à la suite des autres, dans leur ordre d'utilisation. La ROM des coefficients est adressée par un circuit de contrôle qui lit le bon coefficient suivant l’opération demandée et l'étape associée. Le circuit de contrôle est implémenté via un microcode, concept qu'on verra dans les chapitres sur la microarchitecture du processeur.

L'usage d'une mémoire à interpolation

[modifier | modifier le wikicode]

Dans cette section, nous allons voir qu'il est possible de faire des calculs avec l'aide d'une mémoire de précalcul. Avec cette technique, le circuit combinatoire qui fait le calcul est remplacé par une ROM strictement équivalente, qui contient les résultats des calculs possibles. La technique marche immédiatement pour les calculs qui n'ont qu'une seule opérande, comme les calculs trigonométriques, le logarithme, l'exponentielle, la racine carrée, ce genre de calculs. L'opérande du calcul sert d'adresse mémoire, l'adresse contient le résultat du calcul demandé. Et on peut adapter cette technique pour les calculs à deux opérandes ou plus : il suffit de les concaténer pour obtenir une opérande unique.

ALU fabriquée à base de ROM

Cependant, la technique ne marche pas si les opérandes sont codés sur plus d'une dizaine de bits : la mémoire ROM serait trop grosse. 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, soit 128 kiloctets, et ce pour une seule opération. C'est faisable, mais n'espérez pas faire la même chose avec des opérandes de 32 ou 64 bits ! Pour cela, on va donc devoir ruser pour réduire la taille de cette ROM, et donc mémoriser moins de résultats qu'avant. Par exemple, imaginons qu'on veuille implémenter une fonction trigonométrique pour des flottants de 16 bits, avec une ROM avec des adresses de 10 bits. Il y aura 65535 flottants différents en opérandes, mais seulement 1024 résultats différents dans la ROM. Et il faut gérer la situation. Les deux sections suivantes fournissent deux solutions possibles pour cela.

Une première optimisation : éliminer les résultats redondants

[modifier | modifier le wikicode]

Pour cela, une solution serait d'éliminer les résultats redondants, où des opérandes différentes donnent le même résultat. Pour simplifier les explications, on prend des fonctions à une seule opérande : les fonctions trigonométriques comme sinus ou cosinus, tangente, ou d'autres fonctions comme logarithme et exponentielles. Il arrive que deux opérandes différentes donnent le même résultat, ce qui fait que les résultats sont légèrement redondants. Un exemple est illustré avec les identités trigonométriques basiques pour les sinus et cosinus.

  • L'identité 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 .
  • L'identité permet de calculer la moitié des sinus quand l'autre est connue.
  • La définition permet de calculer les tangentes sans avoir à utiliser de ROM séparée.
  • L'identité permet de calculer des cosinus à partir de sinus, ce qui élimine le besoin d'utiliser une mémoire séparée pour les cosinus.

Et on peut penser à utiliser d'autres identités trigonométriques, d'autres formules mathématiques pour éliminer des résultats redondants. L'idée est alors de ne stocker le résultat qu'une seule fois dans la ROM, et d'ajouter des circuits autour pour que cette optimisation soit valide. L'idée est que des opérandes différentes vont pointer vers la même adresse dans la ROM des résultats, vers le même résultat. Pour cela, des circuits combinatoires déterminent l'adresse adéquate à partir des opérandes. Ce sont ces circuits qui appliquent les identités trigonométriques précédentes. Les circuits en question dépendent de l'identité trigonométrique utilisée, voire de la formule mathématique utilisée, aussi on ne peut pas faire de généralités sur le sujet.

Une seconde optimisation : l'interpolation linéaire

[modifier | modifier le wikicode]
Interpolation memory - principe

Malgré l'utilisation d'identités mathématiques pour éliminer les résultats redondants, il arrive que la mémoire de précalcul soit trop petite pour stocker tous les résultats nécessaires. Il n'y a alors pas le choix que de retirer des résultats non-redondants. 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.

Il reste cependant possible de calculer une approximation du résultat, quand le résultat ne tombe pas sur un résultat précalculé. 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.


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).

CAN & CNA
Connecteur VGA

Les anciennes cartes graphiques incorporaient aussi un CNA dans l'interface avec l'écran. Les anciens écrans CRT avaient des entrées analogiques, connues sous le nom de connecteur VGA. C'est le fameux connecteur bleu typique des anciens écrans, mais qui est parfois présent sur des nouveaux modèles. Pour s'interfacer à l'entrée VGA, la carte graphique incorporait un circuit CNA pour transformer les pixels encodés en numérique en signaux analogiques compatibles avec l'entrée VGA.

De nos jours, les CNA et CAN sont utilisés dans les cartes son, et ils étaient utilisés dans les anciennes cartes graphiques. Elle intègrent un un CAN pour convertir le signal provenant d'un microphone en un signal numérique utilisable par l'ordinateur, ainsi qu'un CNA qui produit un signal analogique à destination des haut-parleurs. Mais nous reverrons cela dans quelques chapitres.

Le convertisseur numérique-analogique

[modifier | modifier le wikicode]

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.

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.

La relation entre entrée numérique et tension de sortie varie pas mal selon le CNA, mais la plupart sont des convertisseurs dits linéaires. L'idée est que si on incrémente l'entrée, la tension de sortie augmente toujours d'un quantum de tension et vaut . Il s'agit de la différence de tension minimale que l'on obtient en changeant l'entrée. 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. Pour résumer, la tension de sortie est proportionnelle au nombre à convertir, le coefficient de proportionnalité est le quantum de tension.

CNA linéaire.

Un 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 de CNA, les CNA uniformes 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, comme 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 réduisent le nombre de tensions à implémenter. 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 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 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

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 | modifier le wikicode]

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 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 | modifier le wikicode]

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.

Voici une animation du CAN à approximation succesive en fonctionnement :

4-bit Successive Approximation DAC


Les circuits intégrés

[modifier | modifier le wikicode]

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 | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

À 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 tension de seuil d'un transistor

[modifier | modifier le wikicode]

Le fonctionnement d'un transistor est légèrement plus complexe que ce qui a été dit auparavant. Mais pour rester assez simple, disons que son fonctionnement exact dépend de trois paramètres : la tension d'alimentation, le courant entre drain et source, et un nouveau paramètre appelé la tension de seuil.

Appliquons une tension sur la grille d'un transistor NMOS. Si la tension de grille reste sous un certain seuil, le transistor se comporte comme un interrupteur fermé. Le seuil de tension est appelé, très simplement, la tension de seuil. Au-delà de la tension de seuil, le transistor se comporte comme un interrupteur ouvert, il laisse passer le courant. La valeur exacte du courant dépend de la tension entre drain et source, soit la tension d'alimentation. Elle aussi dépend de la différence entre tension de grille et de seuil, à savoir .

Le paragraphe qui va suivre est optionnel, mais détaille un peu plus le fonctionnement d'un transistor MOS. Tout ce qu'il faut comprendre est que la tension de seuil est une tension minimale pour ouvrir le transistor. Le plus important à retenir est que l'on ne peut pas baisser la tension d'alimentation sous la tension de seuil, ce qui est un léger problème en termes de consommation énergétique. Ce détail reviendra plus tard dans ce cours, quand nous parlerons de la consommation d'énergie des circuits électroniques.

Dans les cas que nous allons voir dans ce cours, la tension d'alimentation est plus grande que . Le courant est alors maximal, il est proportionnel à . Le transistor ne fonctionne alors pas comme un amplificateur, le courant reste le même. Si la tension d'alimentation est plus petite que , le transistor est en régime linéaire : le courant de sortie est proportionnel à , ainsi qu'à la tension d'alimentation. Le transistor fonctionne alors comme un amplificateur de courant, dont l'intensité de l'amplification est commandée par la tension.

Relations entre tensions et courant d'un MOSFET à dopage N.

La technologie CMOS

[modifier | modifier le wikicode]

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.

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 NAND et NOR

[modifier | modifier le wikicode]

Passons maintenant aux portes logiques à plusieurs entrées. Pour celles-ci, on va devoir utiliser plus de transistors que pour la porte NON, ce qui demande de les organiser un minium. Une porte logique à deux entrées demande d'utiliser au moins deux transistors par entrée : un transistor PMOS et un NMOS par entrée. Rappelons qu'un transistor est associé à une entrée : l'entrée est directement envoyée sur la grille du transistor et commande son ouverture/fermeture. Pour les portes logiques à 3, 4, 5 entrées, la logiques est la même : au minimum deux transistors par entrée, un PMOS et un NMOS.

Nous allons d'abord voir le cas d'une porte NOR/NAND en CMOS. Avec elles, les transistors sont organisées de deux manières, appelées transistors en série (l'un après l'autre, sur le même fil) et transistors en parallèle (sur des fils différents). Le tout est illustré ci-dessous. Avec des transistors en série, plusieurs transistors NMOS ou deux PMOS se suivent sur le même fil, mais on ne peut pas mélanger NMOS et PMOS sur le même fil.

Transistors CMOS en série et en parallèle

Les portes NAND/NOR à deux entrées

[modifier | modifier le wikicode]

Voyons d'abord le cas des portes NAND/NOR à deux entrées. Elles utilisent deux transistors NMOS et deux PMOS.

Avec des transistors en série, deux transistors NMOS ou deux PMOS se suivent sur le même fil, mais on ne peut pas mélanger NMOS et PMOS sur le même fil. Avec des transistors en parallèle, c'est l'exact inverse. L'idée est de relier la tension d'alimentation à la sortie à travers deux PMOS transistors distincts, chacun sur son propre fil, sa propre connexion indépendante des autres. Pour la masse (0 volt), il faut utiliser deux transistors NMOS pour la relier à la sortie, avec là encore chaque transistor NMOS ayant sa propre connexion indépendante des autres. En clair, chaque entrée commande un transistor qui peut à lui seul fermer le circuit.

On rappelle deux choses : chaque transistor est associée à une entrée sur sa grille, un transistor se ferme si l'entrée vaut 0 pour des transistors PMOS et 1 pour des NMOS. Avec ces deux détails, on peut expliquer comment fonctionnent des transistors en série et en parallèle. Pour résumer, les transistors en série ferment la connexion quand toutes les entrées sont à 1 (NMOS) ou 0 (PMOS). Avec les transistors en parallèle, il faut qu'une seule entrée soit à 1 (NMOS) ou 0 (PMOS) pour que la connexion se fasse.

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. Pour gérer le cas d'une seule entrée à 1, il faut utiliser deux transistors en parallèle 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 portes NAND/NOR/ET/OU à plusieurs entrées

[modifier | modifier le wikicode]

Les portes NOR/NAND à plusieurs entrées sont construites à partir de portes NAND/NOR à deux entrées auxquelles on rajoute 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.

Les portes ET/OU sont fabriquées à partir de NAND/NOR en CMOS

[modifier | modifier le wikicode]

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

Pour ce qui est des portes ET/OU avec beaucoup d'entrées, il est fréquent qu'elles soit construites en combinant plusieurs portes ET/OU moins complexes. Par exemple, une porte ET à 32 entrées sera construite à partir de portes à seulement 4 ou 5 entrées. 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.

Une méthode générale

[modifier | modifier le wikicode]

Il existe une méthode générale pour créer des portes logiques à deux entrées. Avec elle, il faut repartir du montage avec deux transistors NMOS/PMOS en série. En théorie, il permet de relier la sortie à la tension d'alimentation/zéro volt si toutes les entrées sont à 0 (PMOS) ou 1 (NMOS). L'idée est de regarder ce qui se passe si on fait précéder l'entrée d'un transistor par une porte NON. Pour deux transistors, cela fait 4 possibilités, 8 au total si on fait la différence entre PMOS et NMOS. Voici les valeurs d'entrées qui ferment le montage à transistor en série, suivant l’endroit où on place la porte NON.

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. Il faut juste se souvenir 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.

Appliquons la méthode que je viens d'expliquer avec une porte XOR. Le résultat est sous-optimal, mieux vaut fabriquer une porte XOR en combinant d'autres portes logiques, mais c'est pour l'exemple. 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.

Dans les faits, la méthode n'est pas utilisée pour les portes XOR. A la place, les portes XOR sont construites à base d'autres portes logiques plus simples, comme des portes NAND/NOR/ET/OU. Le résultat est que l'on a un circuit à 10 transistors, contre 12 avec la méthode précédente.

Porte XOR en CMOS en 10 transistors.

Les circuits plus complexes (full adder, ...)

[modifier | modifier le wikicode]

Il est possible de fusionner plusieurs portes ET-OU-NON en un seul circuit à transistors CMOS, ce qui permet des simplifications assez impressionnantes. Pour donner un exemple, le schéma suivant compare l'implémentation d'un circuit qui 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.

Une conséquence est que des circuits assez complexes gagnent à être fabriqués directement avec des transistors. Prenons l'exemple de l'additionneur complet. Une implémentation naïve, avec 5 portes logiques, utilise beaucoup de transistors. Deux portes XOR, deux portes OU et une porte ET, cela dépasse la trentaine de transistors. Faisons le compte : 10 transistors par porte XOR, 6 pour les trois autres portes, cela fait 38 transistors. Les additionneurs des processeurs modernes sont optimisés directement au niveau des transistors, pour leur permettre d'économiser des transistors. Par exemple, l'implémentation suivante en utilise seulement 24 !

Additionneur complet fabriqué avec 24 transistors.

Et c'est sans compter que l'additionneur complet naïf n'est pas forcément le top du top en termes de performances. Là encore, une implémentation avec des transistors peut être optimisée pour être plus rapide, notamment au niveau du calcul de la retenue, ou au contraire d'é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.

La pass transistor logic

[modifier | modifier le wikicode]

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 connecte l'entrée directement sur la sortie. Le circuit interrupteur n'est autre que les portes à transmission vues il y a quelques chapitres.

La pass transistor logic a été utilisée dans des processeurs commerciaux, comme dans l'ARM1, le premier processeur ARM. Sur l'ARM1, les concepteurs ont décidé d'implémenter certains circuits avec des multiplexeurs. 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.

S'il est intéressant de voir la pass transistor logic, c'est qu'elle est souvent utilisée pour simplifier certains circuits CMOS normaux. Par exemple, il est possible d'implémenter toutes les portes logiques en CMOS normal, sauf la porte XOR qui est implémentée avec la pass transistor logic. Cela permet une petite économie de circuits, vu que la porte XOR est bien plus simple en pass transistor logic. La pass transistor logic est aussi utilisée pour simplifier les multiplexeurs et les démultiplexeurs, et certains additionneurs. Aussi, ne soyez pas étonné si nous revenons sur certains circuits vus dans les chapitres précédents, dans cette section.

La porte à transmission

[modifier | modifier le wikicode]

Le circuit de base est une porte logique que nous n'avons pas encore vu pour le moment, appelée la porte à transmission. Elle agit comme un interrupteur commandé par une entrée de commande. Pour rappel, un interrupteur fermé laisse passer le courant, alors qu'un interrupteur fermé ne le laisse pas passer. La porte à transmission fait pareil : soit elle connecte l'entrée et la sortie, soit elle les déconnecte. Pour choisir entre les deux, une porte à transmission possède une entrée de commande sur laquelle on envoie un bit de commande. La porte est fermée si le bit de commande est à 1, ouvert s'il est à 1.

Porte à transmission.

Il est possible de la voir comme une porte OUI améliorée dont la table de vérité est celle-ci :

Commande Entrée Sortie
0 0 Déconnexion
0 1 Déconnexion
1 0 0
1 1 1

Intuitivement, on se dit qu'une porte à transmission est faite avec un seul, vu qu'un transistor fonctionne déjà comme un interrupteur commandable. Mais une porte à transmission est construite avec deux transistors. La raison la plus intuitive est que la logique CMOS associe toujours un transistor PMOS à un transistor NMOS. Mais une autre raison, plus importante, est que les transistors NMOS et PMOS ne sont pas des interrupteurs parfaits. Les NMOS laissent passer les 0, mais laissent mal passer les 1 : la tension en sortie, pour un 1, est atténuée. Et c'est l'inverse pour les PMOS, qui laissent bien passer les 1 mais fournissent une tension de sortie peut adéquate pour les 0. Donc, deux transistors permettent d'obtenir une tension de sortie convenable.

Le montage de base est illustré ci-dessous. 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.

CMOS Transmission gate

Une porte logique en logique CMOS connecte directement sa sortie sur la tension d'alimentation ou la masse. Mais dans une porte logique en pass transistor logic, il n'y a ni tension d'alimentation, ni masse (O Volts). La sortie est connectée sur l'entrée, rien de plus. Et cela explique plusieurs différences entre CMOS et pass transistor logic.

La première différence est que certaines portes logiques sont impossibles avec la pass transistor logic pure. Les portes logiques CMOS peuvent générer un 1 ou un 0 distinct de ce qu'il y a sur leur entrée. Par exemple, elles peuvent sortir un 1 même si toutes leurs entrées sont à 0, car elles reliées à la tension d'alimentation. Les portes à transmission ne peuvent pas le faire. Elles se contentent de recopier une entrée sur leur sortie : impossible d'avoir un 1 en sortie avec uniquement des zéros en entrée. La conséquence est qu'il n'est pas possible de créer de porte NON, ni de porte NOR/NAND directement.

Une autre différence est que l’électricité est fournie par l'entrée, ce qui fait qu'elle se dissipe un peu lors du passage dans 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. Il faut souvent rajouter des portes OUI pour restaurer les tensions adéquates, à divers endroits du circuit. La pass transistor logic mélange donc porte OUI/NON CMOS normales avec des portes à transmission. Afin de faire des économies de circuit, on utilise parfois une seule porte NON CMOS comme amplificateur, ce qui fait que de nombreux signaux sont inversés dans les circuits, sans que cela ne change grand chose si le circuit est bien conçu.

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. De même, la pass transistor logic utilise généralement moins de transistors pour implémenter une porte logique, et un circuit électronique en général. L'exemple avec la porte XOR est assez parlant : on passe de 12 à 6 transistors par porte XOR. Des circuits riches en portes XOR, comme les circuits additionneurs, gagnent beaucoup à utiliser des portes à transmission.

Les multiplexeurs en pass transistor logic

[modifier | modifier le wikicode]

Les portes à transmission sont très utilisées dans les multiplexeurs et les démultiplexeurs. Prenons l'exemple d'un multiplexeur 2 vers 1. 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. Voici le circuit qui en découle :

Multiplexeur fabriqué avec des portes à transmission

En utilisant les portes à transmission CMOS vues plus haut, on obtient le circuit suivant :

Multiplexeur fabriqué avec des portes à transmission CMOS.

La même méthode fonctionne pour les multiplexeurs avec plus de deux entrées. Pour rappel, un multiplexeur est composé d'un décodeur qui commande une couche de portes ET, les sorties des portes ET sont combinées avec une porte OU.

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

Il est possible de remplacer les portes ET par des portes à transmission. L'idée est de ne connecter sur la sortie que l'entrée qui a été sélectionnée et de déconnecter les autres. En faisant ainsi, on peut se passer de la porte OU, qui est remplacée par un simple fil. Il n'y a qu'une seule entrée qui est connectée à la sortie à chaque instant, pas besoin d'utiliser de porte OU. Le résultat est le circuit suivant :

Multiplexeur basé sur des interrupteurs.

Les multiplexeurs en pass transistor logic sont plus simple que leurs cousins en CMOS normal. Beaucoup de circuits utilisent des multiplexeurs et nous en avons déjà vu pas mal : les circuits de décalage, les bascules, les additionneurs, quelques autres. Comment se comportent-ils si leurs MUX sont implémentés avec la pass transistor logic ? La réponse est que l'usage de la pass transistor logic ne change pas la donne pour les circuits de décalage, alors qu'elle change drastiquement la donne pour les bascules et les additionneurs. Voyons cela dans le détail.

Les bascules D avec des portes à transmission

[modifier | modifier le wikicode]
Bascule D créée avec un multiplexeur.
Bascule D créée avec un multiplexeur.

Une bascule D est, pour rappel, un circuit qui mémorise un bit. Elle peut être implémenté avec un multiplexeur 2 vers 1, en bouclant la sortie du multiplexeur sur une entrée. Pour un multiplexeur fabriqué avec des portes CMOS, boucler sa sortie sur son entrée ne pose pas de problème. Mais avec des portes à transmission, le circuit ne fonctionne pas. Le problème est qu'une porte à transmission est électriquement équivalente à un simple interrupteur, ce qui réduit le circuit à une boucle entre un interrupteur et un fil. Le courant qui circule dans le fil et l'interrupteur se dissipe rapidement du fait de la résistance du fil et disparait en quelques micro- ou millisecondes.

La solution est de rajouter une porte OUI (celle qui recopie son entrée sur sa sortie) dans la boucle pour régénérer le signal électrique. Et la manière la plus simple de fabriquer une porte OUI utilise deux portes NON qui se suivent, ce qui donne le circuit ci-dessous. 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 d'une bascule D avec des portes à transmission.

Le circuit précédent utilise seulement 10 transistors, alors qu'un multiplexeur en CMOS normal en utilise 14. Un autre avantage est que ce circuit permet d'avoir les deux sorties Q : la sortie Q inversée est prise en sortie de la première porte NON. Une variante du circuit précédent est utilisée dans les mémoires dites SRAM, qui sont utilisées pour les registres du processeur ou ses mémoires caches. Mais nous verrons cela plus en détail dans le chapitre sur les cellules mémoires.

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. 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

L'additionneur Manchester Carry Chain

[modifier | modifier le wikicode]

Les portes à transmission étaient autrefois utilisées pour simplifier les additionneurs, et plus précisément les additionneurs à propagation de retenue. Pour rappel, un additionneur à propagation de retenue additionne deux opérandes, bit par bit. Elle additionne les deux bits de poids faible, ce qui donne un bit de résultat et un bit de retenue. Le bit de retenue est alors envoyé à la colonne suivante, où deux bits sont additionnés avec la retenue, et ainsi de suite. De tels circuits sont composées en enchainant des additionneurs complets, des circuits qui additionnent trois bits : deux bits d'opérandes et une retenue.

Additionneur à propagation de retenue.

Un défaut des additionneurs à propagation de retenue est leur lenteur. Le résultat n'est connu qu'une fois que les retenues ont été propagées d'une colonne à l'autre. Et cette propagation est assez lente. Les additionneurs modernes utilisent des techniques très complexes pour résoudre ce problème, comme nous l'avons vu il y a quelques chapitres. Mais ces solutions utilisent beaucoup plus de transistors. De nombreux processeurs comme le 8086 d'Intel, ou d'autres processeurs 8-16 bits de cette époque, ne pouvaient pas se le permettre. A la place, il utilisaient une optimisation à base de portes à transmission.

L'optimisation en question s'appelle la Manchester Carry Chain. L'idée est d'optimiser les additionneurs complets de manière à ce qu'ils propagent les retenues plus rapidement. L'idée est de partir d'un additionneur construit avec un multiplexeur. Pour rappel, la retenue sortante est soit égale à la retenue entrante, soit est générée par l'additionneur. Le choix entre les deux est réalisé par un multiplexeur, qui est commandé par une porte logique qui détermine s'il faut propager la retenue ou non. Si propagation de retenue il y a, la retenue sortante est égale à la retenue entrante. Sinon, la retenue sortante est celle calculée par l'additionneur.

Additionneur complet basé sur un MUX

Avec la Manchester Carry Chain, le multiplexeur est implémenté avec des portes à transmission. Le circuit obtenu est illustré ci-dessous. L'avantage est que la propagation de la retenue est beaucoup plus rapide. Ps besoin de traverser deux à trois portes logiques, la retenue passe juste à travers un simple interrupteur, une porte à transmission. La propagation de la retenue ne rencontre pas d'obstacle, si ce n'est la résistance des fils, elle ne subit pas de délai liés au temps de propagation des portes logiques.

Manchester carry chain

Cependant, l'usage de portes à transmission a quelques défauts. Le principal est que, vu que la retenue d'entrée est envoyée sur la sortie à travers des interrupteurs, 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 portes à transmission sont utilisées comme des condensateurs, mais nous n'en parlerons pas ici.

La pass transistor logic utilise des multiplexeurs 2 vers 1

[modifier | modifier le wikicode]

La pass transistor logic est rarement utilisée, à une exception de taille : la porte XOR. Pour rappel, une porte XOR est une sorte d'inverseur commandable, à savoir un circuit prend un bit d'entrée A, et l'inverse ou non suivant la valeur d'un bit de commande B. Et cela nous dit comment implémenter une porte XOR avec un multiplexeur. Un multiplexeur choisit sa sortie parmi deux entrées : A et , le second bit B est envoyé sur l'entrée de commande ! Le circuit obtenu, est celui-ci :

XOR implémenté avec un MUX.

Il est possible de simplifier le circuit en rusant un peu, 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. Avec pass transistor logic, une porte XOR utilise 4 à 8 transistors. Le gain est clairement significatif, suffisamment pour utiliser la pass transistor logic pour la porte XOR, quitte à utiliser des portes CMOS normales pour le reste. Quelques processeurs faisaient cela dans le temps, comme le mythique processeur Z80.

La pass transistor logic implémente les autres portes logiques avec un multiplexeur 2 vers 1 couplé à quelques portes NON ! Et intuitivement, vous vous dites que les deux entrées de la porte logique correspondent aux deux entrées de donnée du multiplexeur. Sauf qu'en réalité, un bit d'entrée est envoyé sur l'entrée de commande, et l'autre bit sur une entrée de donnée du multiplexeur. Suivant ce qu'on met sur la seconde entrée du multiplexeur, on obtient une porte ET, OU, XOR, etc. Il y a quatre choix possibles : soit on envoie un 0, soit un 1, soit l'inverse du bit d'entrée, soit envoyer deux fois le bit d'entrée.

Plus haut, nous avions dit que les portes à transmission ne permettaient pas d'implémenter certaines portes logiques, car elles recopient leur entrée sur leur sortie. Impossible d'avoir un 1 en sortie si les entrées valent 0. Mais remarquez que les circuits précédents utilisent les portes NON. Ce sont ces portes NON qui fournissent l'électricité en sortie nécessaire pour avoir un 1 en sortie alors que les entrées sont à 0.
Portes logiques faites à partir de multiplexeurs

Les technologies PMOS et NMOS

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 dynamique MOS

[modifier | modifier le wikicode]

La logique dynamique permet de créer des portes logiques ou des bascules d'une manière assez intéressante. 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 | modifier le wikicode]

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 | modifier le wikicode]

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 phase a lieu quand le signal d'horloge est à 1, l'autre quand il est à 0.

Une porte NAND en logique dynamique CMOS

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Le dernier désavantage implique l'arbre d'horloge, le système d'interconnexion qui distribue le signal d'horloge à toutes les bascules d'un circuit. 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.

La logique TTL : un apercu rapide

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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


De nos jours, les portes logiques et/ou transistors sont rassemblés dans des circuits intégrés. Les circuits intégrés modernes regroupent un grand nombre de transistors qui sont reliés entre eux par des interconnexions métalliques. 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.

Les circuits intégrés : généralités

[modifier | modifier le wikicode]
Broches du processeur MOS6502.

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. Quoiqu'il en soit, il est intéressant de voir l'interface d'un circuit intégré et ce qu'il y a à l'intérieur.

L'interface d'un circuit intégré

[modifier | modifier le wikicode]

Les circuits intégrés ont, comme les portes logiques, des broches métalliques sur lesquelles on envoie des tensions. Quelques broches vont recevoir la tension d'alimentation (broche VCC), d'autres vont être reliées à la masse (broche GND), et surtout : les broches restantes vont porter des bits de données ou de contrôle. Ces dernières peuvent se classer en trois types : les entrées, sorties et entrée-sorties. Les entrées sont celles sur lesquelles on place des bits à envoyer au circuit imprimé, les sorties sont là où le circuit imprimé envoie des informations vers l'extérieur, les entrées-sorties servent alternativement de sortie ou d'entrée.

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.

L'intérieur d'un circuit intégré et sa fabrication

[modifier | modifier le wikicode]

Après avoir vu les boitiers d'un circuit imprimé et leurs broches, voyons maintenant ce qu'il y a dans le circuit imprimé. Si vous découpez le boitier d'un circuit imprimé, vous allez voir que le boitier en plastique entoure une sorte de carré/rectangle de couleur grisâtre, appelé le die du circuit imprimé, ou encore la puce électronique. Le die est un bloc de matériau semi-conducteur. C'est là où se trouvent les transistors et les interconnexions entre eux. Les broches métalliques sont connectées à des endroits bien précis du die. Le die est très petit, quelques millimètres de côté, guère plus. Il est très variable d'un circuit intégré à l'autre et il est difficile de faire des généralités dessus.

Intérieur du circuit intégré Intel C8751H.
Lingot de silicium (imparfaitement cylindrique car c'est un des premiers cylindre fabriqué).

Les dies sont fabriqués à partir de silicium, à l'exception de quelques die fabriqués avec du gernanium, peu utilisés et encore en cours de recherche. Le silicium a des propriétés semiconductrices très intéressantes, qui font que c'est le matériau le plus utilisé dans l'industrie actuellement. La fabrication d'un circuit électronique moderne part d'un lingot de silicium pur, qui a une forme cylindrique. Un tel lingot est illustré ci-contre. Le lingot est découpé en tranches circulaires sur lesquelles on vient graver le die. Les tranches circulaires sont appelées des wafers.

Wafer de silicium pur, avec quelques dies gravés dessus.
Pertes aux bords d'un wafer.

Avant d'expliquer ce qui arrive aux wafers pour qu'on vienne graver des dies dessus, précisons que la forme des waffer n'est pas très compatible avec celle des dies. Un wafer est circulaire, un die est carré/rectangulaire. L’incompatibilité se manifeste sur les bords du wafer, qui sont gâchés car on ne peut pas y mettre de die, comme indiqué dans le schéma ci-contre. Il y a donc un léger gâchis en silicium, qu'il est préférable de réduire au plus possible.

De plus, les dies gravés ne sont pas tous fonctionnels. Il n'est pas rare que certains ne fonctionnent pas à cause d'un défaut de gravure. Il faut dire que graver des transistors de quelques nanomètres de diamètre est un procédé très compliqué qui ne peut pas marcher à tous les coups. Il suffit d'un grain de poussière mal placé pour qu'un die soit irrémédiablement perdu. Lors de la fabrication, il y a un certain pourcentage moyen de dies gravés sur un wafer qui sont défectueux. Le nombre de dies fonctionnels sur le nombre total de dies gravés est appelé le Yield. Idéalement, il faudrait que le yield soit le plus élevé possible.

Pour augmenter le yield et réduire les pertes aux bords du wafer, il y a une solution qui marche pour les deux problèmes : utiliser des dies très petits, le plus petit possible. Plus les dies sont petits, plus la perte sur les bords du wafer sera faible. Mais réduire le die signifie réduire la taille du circuit intégré, et donc son nombre de transistors. Il semblerait qu'il y a donc un compromis à faire : soit avoir des circuits bourrés de transistors mais avec un yield bas, ou avoir un yield élevé pour des circuits simples. Mais il y a une solution pour obtenir le meilleur des deux mondes.

Evolution du yield en fonction de la taille des dies.

Les chiplets et circuits imprimés en 3D

[modifier | modifier le wikicode]

Il existe des boitiers qui regroupent plusieurs boitiers et/ou plusieurs dies, plusieurs puces électroniques. Ils sont appelés des circuits intégrés Multi-chip Module (MCM). Les puces électroniques d'un MCM sont appelées des chiplets, pour les différencier des autres dies. L'idée est qu'il vaut mieux combiner plusieurs dies simples que d'utiliser un gros die bien complexe.

Exemple de circuit intégré MCM : le processeur Pentium Pro.
Wireless TSV (model)

Les circuits imprimés en 3D sont une sous-classe de circuits imprimés MCM conçus en empilant plusieurs circuits plats l'un au-dessus de l'autre, dans le même boitier. Ils sont composés de plusieurs couches, chacune contenant des transistors MOS, empilées les unes au-dessus des autres. Les différentes couches sont connectées entre elles par des fils métalliques qui traversent les différentes couches, au nom à coucher dehors : Through-silicon via (TSV), Cu-Cu connections, etc. Le nom de la technique en anglais est 3DS die stacking.

Le 3DS die stacking regroupe un grand nombre de technologies différentes, qui partagent la même idée de base, mais dont l'implémentation est fortement différente. Mais les différences sont difficiles à expliquer ici, car la fabrication de circuits imprimés est un domaine complexe, faisant intervenir physique des matériaux et ingénierie.

Les avantages du 3DS die stacking est qu'on peut mettre plus de circuits dans un même boitier, en l'utilisant en hauteur plutôt qu'en largeur. Par contre, la dissipation de la chaleur est plus compliquée. Un circuit électronique chauffe beaucoup et il faut dissiper cette chaleur. L'idéal pour dissiper la chaleur est d'avoir une surface plane, avec un volume faible. Plus le rapport surface/volume d'un circuit à semi-conducteur est élevé, mieux c'est pour dissiper la chaleur, physique de la dissipation thermique oblige. Et empiler des couches augmente le volume sans trop augmenter la surface. D'où le fait que la gestion de la température est plus compliquée.

Le 3DS die stacking est surtout utilisé sur les mémoires, bien moins sur les autres types de circuits. Elle est surtout utilisée pour les mémoires FLASH, mais quelques mémoires RAM en utilisent. Pour les mémoires RAM proprement dit, deux standards incompatibles s'opposent. D'un côté la technologie High Bandwidth Memory, de l'autre la technologie Hybrid Memory Cube.

3DS die stacking

L'avantage de cette technique pour les mémoires est qu'elle permet une plus grande capacité, à savoir qu'elles ont plus de gibioctets. De plus, elle ne nuit pas aux performances de la mémoire. En effet, la performance des mémoires/circuits dépend un peu de la longueur des interconnexions : plus elles sont longues, plus le temps pour lire/écrire une donnée est important. Et il vaut mieux avoir de courtes interconnexions en hauteur, que de longues interconnexions sur une surface.

Il y a quelques processeurs dont la mémoire cache utilise le 3DS die stacking, on peut notamment citer les processeurs AMD de microarchitecture Zen 3, Zen 4 et Zen 5. Le premier processeur disposant d'une mémoire cache en 3D a été le R7 5800X3D. Il succédait aux anciens processeurs de microarchitecture Zen 3, qui disposaient d'un cache L3 de 32 mébioctets. Le 5800X3D ajoutait 64 mébioctets, ce qui fait au total 96 mébioctets de mémoire cache L3. Et surtout : la rapidité du cache était la même sur le 5800X3D et les anciens Zen 3. À peine quelques cycles d'horloge de plus pour un cache dont le temps d'accès se mesure en 50-100 cycles d'horloge.

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

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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é. On ne peut pas empiler deux transistors l'un sur l'autre, du moins pas facilement. Il y a bien des technologiques pour faire ça, mais elles sont complexes et nous les omettons ici. Les transistors sont donc répartis sur une surface plane, qui a une forme approximativement rectangulaire et qui a une certaine aire. L'aire en question est la même pour tous les processeurs, qui font tous la même taille, leur circuits imprimés sont les mêmes.

Les transistors sont des structures en 3D, mais ils sont posés sur une surface en 2D. En clair, on n'empile pas les transistors les uns sur les autres, on les mets les uns à côté des autres. Leur épaisseur peut se réduire avec le temps, mais cela n'a pas d'importance pour la loi de Moore. Par contre, ils ont souvent une largeur et une longueur qui sont très proches, et qui diminuent avec l'évolution des technologies de fabrication. Pour simplifier, la taille des transistors est aussi appelée la finesse de gravure. Elle s'exprime le plus souvent en nanomètres.

Doubler le nombre de transistors signifie qu'on peut mettre deux fois plus de transistors sur une même surface. Pour le dire autrement, la surface occupée par un transistor a été divisée par deux. On s'attendrait à ce que leur taille soit divisée par deux tous les 2 ans, comme le dit la loi de Moore. Mais c’est là une erreur de raisonnement.

Rappelez-vous que la taille d'un processeur reste la même, ils gardent la même surface carrée d'un modèle à l'autre. Si on divise la taille des transistors par deux, l'aire prise par un transistor sur cette surface carrée sera divisée par 4, donc on pourra en mettre 4 fois plus. Incompatible avec la loi de Moore ! En réalité, diviser une surface carrée/rectangulaire par deux demande de diviser la largeur et la longueur par . Ainsi, la finesse de gravure est divisée par , environ 1,4, tous les deux ans. Une autre manière de le dire est 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 transistors CMOS.

La fin de la loi de Moore

[modifier | modifier le wikicode]

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 | modifier le wikicode]

Le processeur est le circuit de l'ordinateur qui effectue des calculs sur des nombres codés en binaire, c’est la pièce maitresse de l'ordinateur. C'est un circuit assez complexe, 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 | modifier le wikicode]

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 | modifier le wikicode]

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

L'invention des processeurs multicœurs

[modifier | modifier le wikicode]

Avec l'avancée des processus de fabrication, il est devenu possible de mettre plusieurs processeurs sur une même puce de silicium, et c'est ainsi que sont nés les processeurs multicœurs. Pour simplifier, les processeurs multicœurs regroupent plusieurs processeurs, soit sur une même puce de silicium, soit dans un même boitier. Les processeurs en question sont appelés des cœurs. Il arrive donc qu'un processeur multicœurs ait en réalité 8 cœurs/processeurs sur la même puce, ou 4, ou 2, parfois 16, 32, 64, rarement plus. Les processeurs multicœurs contenant 2 processeurs sont aujourd'hui obsolète, la norme est entre 4 et 16.

Les fabricants ont généralement plusieurs modèles d'un même processeur : un modèle entrée de gamme peu cher et peu performant, un modèle haut de gamme très cher et performant, et un modèle milieu de gamme aux prix et performances entre les deux précédents. Et ces trois modèles n'ont pas le même nombre de cœurs. Et bien sachez qu'en réalité, tous ces processeurs sortent de la même usine et sont fabriqués de la même manière, avec le même nombre de cœurs. Par exemple, imaginez qu'un modèle entrée de gamme ait 4 cœurs , le milieu de gamme 8 cœurs, et le haut de gamme en ait 16. Et bien ils sont fabriqués à partir d'un modèle haut de gamme à 16 cœurs, dont on désactive certains cœurs pour obtenir les modèles bas et milieu de gamme.

Après leur fabrication, les processeurs subissent des tests pour vérifier si le processeur fonctionne normalement. Et il arrive qu'un cœur soit défectueux et ne fonctionne pas, mais que les autres fonctionnent parfaitement. Par exemple, si on prend un processeur à 8 cœurs , il se peut que deux d'entre eux ne fonctionne pas et les 6 autres soient fonctionnels. Dans ce cas, on en fait un modèle milieu ou entrée de gamme en désactivant les cœurs défectueux. La désactivation est généralement matérielle, en coupant des fils pour déconnecter les cœurs défectueux.

La révolution des chiplets

[modifier | modifier le wikicode]

Les processeurs multicœurs modernes utilisent la technique des chiplets. Pour donner un exemple, prenons celui du processeur POWER 5, autrefois utilisé sur d'anciens ordinateurs Macintosh. Chaque coeur avait son propre boitier rien que pour lui. Il en a existé deux versions. La première était dite double cœur, à savoir qu'elle intégrait deux processeurs dans la même puce. Le seconde version étiat quadruple coeur, avec 4 processeurs dans un même boitier, avec 4 dies. La dernière version est illustrée ci-dessous. On voit qu'il y a quatre boitier rouges, un par coeur, et quatre autres en vert qui correspondent à de la mémoire cache (le cache L3).

POWER 5 MCM.

Un autre exemple est celui des processeurs AMD récents, d'architectures Zen 2/3/4/5. Ils incorporent deux puces dans le même boitier : une puce qui contient les processeurs, les cœurs, et une autre pour les interconnexions avec le reste de l'ordinateur. La puce pour les interconnexions gère l'interface avec la mémoire RAM, les bus PCI-Express pour la carte graphique, et quelques autres. Les deux puces n'ont pas la même finesse de gravure, ni les mêmes performances.

AMD@7nm(12nmIO)@Zen2@Matisse@Ryzen 5 3600@100-000000031 BF 1923SUT 9HM6935R90062 DSCx2@Infrared

Certains processeurs AMD Epyc avaient plusieurs chiplets pour les processeurs/coeurs, combinés avec un chiplet pour les interconnexions. L'image ci-dessous montre un processeur AMD Epyc 7702, avec un chiplet central pour les interconnexions, et les chiplets autour qui contiennent chacun 4 cœurs.

AMD Epyc 7702.
Schéma fonctionnel de l'AMD Epyc.

La conception d'un circuit intégré

[modifier | modifier le wikicode]
Étapes de conception d'un circuit intégré.

La conception d'un circuit intégré se fait en une série d'étapes assez complexes, dont certaines sont aidées par ordinateur. Inutile de voir dire que concevoir un circuit intégré est généralement assez complexe et demande des compétences très variées. Concevoir une puce électronique doit se faire à plusieurs niveaux d'abstraction, que nous allons détailler dans ce qui suit. Nous allons grandement simplifier le tout en donnant une description assez sommaire.

La conception logique

[modifier | modifier le wikicode]

La première étape est de créer une sorte de cahier des charge, de spécification qui décrit comment fonctionne le circuit. La spécification décrit son architecture externe, à savoir comment le circuit se comporte. Elle décrit comment le circuit réagit quand on envoie telle donnée sur telle entrée, qu'est-ce qu'on retrouve sur ses sorties si... , etc. Pour un circuit combinatoire, cela revient à écrire sa table de vérité. Mais il va de soit que pour des circuits complexes, la spécification est beaucoup plus complexe.

La seconde étape est d'implémenter le circuit en utilisant les circuits vus dans les chapitres précédents, à savoir des registres, bascules, portes logiques, décodeurs, multiplexeurs, additionneurs et autres circuits basiques. La conception se fait en utilisant un langage de description matérielle, qui a des ressemblances superficielles avec un langage de programmation.

Le langage de desciption matériel est ensuite utilisé pour produire une description du circuit assez haut niveau, appelée le Register-transfer level (RTL), qui combine registres, portes logiques et autres circuits combinatoires basiques. Les circuits de base utilisés lors de cette étape sont appelés des cellules standard. La RTL ressemble aux schémas vus dans les chapitres précédents, et ce n'est pas un hasard : de tels schémas sont des RTL simples de circuits eux-mêmes simples.

La conception physique

[modifier | modifier le wikicode]
Design physique, la troisième étape.

La troisième étape traduit la RTL en un plan à appliquer sur le die physique, à graver dessus. Elle traduit les portes logiques en montages à base de transistors, comme vu dans le chapitre précédents. Les autres cellules standards sont elles aussi directement traduites en un montage à base de transistors, conçu à l'avance par des ingénieurs spécialisé, qui est potentiellement optimisé qu'un montage à base de portes logiques.

Les cellules sont placées sur la puce par un algorithme, qui cherche à optimiser l'usage du die. Les interconnexions métalliques entre transistors sont ajoutées, de même que le signal d'horloge, la masse et la tension d'alimentation. L'arbre d'horloge est généré à cette étape, de même que l'arbre qui transmet la tension d'alimentation aux portes logiques. Le résultat est une sorte de description physique du die.

Description physique d'un amplificateur opérationnel basique.


Les circuits intégrés sont connectés au monde extérieur, par l'intermédiaire de leurs broches. Broches qui peuvent servir d'entrée ou de sortie. Nous allons étudier les sorties des circuits intégrés, car il y a des choses importantes à dire dessus. Dans ce chapitre, nous allons voir qu'il existe trois types de sorties différentes. L'intérêt est qu'interconnecter des circuits intégrés entre eux demande de savoir comment ces sorties fonctionnent. Nous détaillerons les interconnexions dans les chapitres sur les bus et les liaisons point à point, où les acquis du présent chapitre seront réutilisés. De plus, la section sur le OU câblé à la fin du chapitre sera utile dans le chapitre sur les mémoires ROM.

Les trois types de sorties : totem-pole, trois états et à drain ouvert

[modifier | modifier le wikicode]

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. Et les trois donnent des bus très différents.

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.

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.

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.

Sortie à drain ouvert.

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. Vous comprendrez en quoi ces sorties sont utiles quand nous parlerons des mémoires et des bus de communication, 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 | modifier le wikicode]

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.

Les tampons trois-états ressemblent aux portes à transmission, à un détail près : ce sont des composants actifs, qui régénèrent le signal d'entrée. Là où les portes à transmission sont électriquement équivalentes à un interrupteur, ce n'est pas le cas des tampons trois-états. Les tampons trois-états sont reliés à la tension d'alimentation et à la masse, ils amplifient un peu le signal d'entrée si besoin.

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 | modifier le wikicode]

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 | modifier le wikicode]

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é.

Pour comprendre comment cela fonctionne, rappelons qu'une sortie en collecteur ouvert est connectée à un transistor relié à la masse. En explicitant ce transistor dans les schémas du dessus, on obtient le schéma ci-dessous. Vous remarquerez qu'il ressemble très fortement au schéma d'une porte logique NOR en technologie NMOS, même le transistor NMOS est remplacé par un transistor bipolaire.

ET ou OU cable

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.

Les multiplexeurs fabriqués avec un OU câblé

[modifier | modifier le wikicode]

Un exemple d'utilisation est la fabrication de multiplexeurs. Pour rappel, un multiplexeur est composé d'un décodeur combiné à une couche de portes ET suivies par une porte OU à plusieurs entrées.

Multiplexeur 2 vers 4 conçu à partir d'un décodeur.
Multiplexeur conçu avec un OU câblé.

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. Utiliser un ou câblé permettait aussi de remplacer les portes ET par des portes à transmission, plus simples.

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.


L'architecture d'un ordinateur

[modifier | modifier le wikicode]

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.

La mémoire est le composant qui mémorise des informations, des données. Dans la majorité des cas, la mémoire est composée de plusieurs cases mémoire, chacune mémorisant plusieurs bits, le nombre de bits étant identique pour toutes les cases mémoire. Dans le cas le plus simple, une case mémoire mémorise un octet, un groupe de 8 bits. Mais les mémoires modernes mémorisent plusieurs octets par case mémoire : elles ont des cases mémoires de 16, 32 ou 64 bits, soit respectivement 2/4/8 octets. De rares mémoires assez anciennes utilisaient des cases mémoires contenant 1, 2, 3, 4, 5, 6 7, 13, 17, 23, 36 ou 48 bits. Mais ce n'était pas des mémoires électroniques, aussi nous allons les passer sous silence.

Tout ce qu'il faut savoir est que la quasi-totalité des mémoires électronique a un ou plusieurs octets par case mémoire. Pour simplifier, vous pouvez imaginer qu'une mémoire RAM est un regroupement de registre, chacun étant une case mémoire. C'est une description pas trop mauvaise pour décrire les mémoires RAM, qu'on abordera dans ce qui suit.

Contenu d'une mémoire, case mémoire de 16 bits (deux octets)
Case mémoire N°1 0001 0110 1111 1110
Case mémoire N°2 1111 1110 0110 1111
Case mémoire N°3 0001 0000 0110 0001
Case mémoire N°4 1000 0110 0001 0000
Case mémoire N°5 1100 1010 0110 0001
... ...
Case mémoire N°1023 0001 0110 0001 0110
Case mémoire N°1024 0001 0110 0001 0110

Dans ce cours, il nous arrivera de partir du principe qu'il y a un octet par case mémoire, par souci de simplification. Mais ce ne sera pas systématique. De plus, il nous arrivera d'utiliser le terme adresse pour parler en réalité de la case mémoire associée, par métonymie.

La capacité mémoire

[modifier | modifier le wikicode]

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 maximal de bits qu'une mémoire peut contenir. Elle est le produit entre le nombre de cases mémoire, et la taille en bit d'une case mémoire.

Toutes les mémoires actuelles utilisant des cases mémoire d'un ou plusieurs octets, ce qui 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 avec le nombre d'octets qu'elle contient. 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, méga ou giga octets : l’acheteur croit implicitement avoir une capacité exprimée en kibi, mébi ou gibi octets, et se retrouve avec un disque dur qui contient moins de mémoire que prévu.

Lecture et écriture : mémoires ROM et RWM

[modifier | modifier le wikicode]

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.

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 le programme à exécuter et éventuellement d'autres informations. Mais son rôle principal est de mémoriser le programme à exécuter. La mémoire RWM stocke des données temporaires, manipulées en lecture et écriture par le processeur. Les deux sont lues directement par le processeur

Pour les mémoires RWM, nous allons nous concentrer sur une mémoire électronique appelée 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, à mémoriser temporairement 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. Pour les systèmes les plus simples, la mémoire RWM ne sert à rien de plus.

Architecture avec une ROM et une RAM.

La mémoire ROM stocke le programme à exécuter et est accessible directement par le processeur. Mais elle peut aussi stocker 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.

Pour donner un exemple de données stockées en ROM, on peut prendre l'exemple des anciennes consoles de jeu 8 et 16 bits. Les jeux vidéos sur ces consoles étaient placés dans des cartouches de jeu, précisément dans une mémoire ROM à l'intérieur de la cartouche de jeu. La ROM mémorisait non seulement le code du jeu, le programme du jeu vidéo, mais aussi les niveaux et les sprites et autres données graphiques.

Une conséquence est que les consoles 8/16 bits n'avaient pas besoin de beaucoup de RAM, comparé aux ordinateurs de l'époque, vu qu'une grande partie des données utiles étaient dans une ROM directement accessible par le processeur. À l'opposé, les micro-ordinateurs devaient copier les données d'un jeu depuis une disquette dans la mémoire RAM, ce qui demandait d'avoir plus de RAM. Le passage au support CD sur les consoles 32 bits a eu la même conséquence. Le processeur ne pouvant pas lire directement le CD à sa guise, il fallait copier les données du CD en RAM. D'où l'apparition de temps de chargement assez longs, inexistants sur support cartouche.

L'adressage mémoire

[modifier | modifier le wikicode]

Sur une mémoire RAM ou ROM, on ne peut lire ou écrire qu'une case mémoire, qu'un registre à la fois : une lecture ou écriture ne peut lire ou modifier qu'une seule case mémoire. Techniquement, le processeur doit préciser à quel case mémoire il veut accéder à chaque lecture/écriture. Pour cela, chaque case mémoire 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 cases mémoire.

Exemple : on demande à la mémoire de sélectionner la case mémoire d'adresse 1002 et on récupère son contenu (ici, 17).

L'adresse mémoire est générée par le processeur. Le processeur peut parfaitement calculer des adresses, en extraire du programme qu'il exécute, ou bien d'autres choses. Nous détaillerons d'ailleurs les mécanismes pour dans les chapitres portant sur les modes d'adressage du processeur. Les adresses générées par le processeur sont alors envoyées à la RAM ou la ROM via une connexion dédiée, un ensemble de fils qui connecte le processeur à la mémoire : le bus d'adresse mémoire. L'adresse sélectionne une case mémoire, le processeur peut alors récupérer la donnée dedans pour une lecture, écrire une donnée pour l'écriture. Pour cela, un second ensemble de fil connecte le processeur à la RAM/ROM, mais cette fois-ci pour échanger des données. Il s'agit du bus de données mémoire. Les deux sont souvent regroupés sous le terme de bus mémoire.

Un ordinateur contient toujours une RAM et une ROM, ce qui demande aux bus mémoire de s'adapter à la présence de deux mémoires. Il y a alors deux solutions. Avec la première, il y a un seul bus mémoire partagé entre la RAM et la ROM, comme illustré ci-dessous. Une autre solution utilise deux bus séparés : un pour la RAM et un autre pour la ROM. Nous verrons les différences pratiques entre les deux à la fin du chapitre.

Bus mémoire : bus d'adresse et de données.

Plus haut, nous avions dit qu'il y a une adresse par case mémoire, chaque case mémoire contenant un ou plusieurs octets. Mais les processeurs modernes partent du principe que la mémoire a un octet par adresse, pas plus. Et ce même si la mémoire reliée au processeur utilise des cases mémoires de 2, 3, 4 octets ou plus. D'ailleurs, la majorité des mémoires RAM actuelle a des cases mémoires de 64 bits, soit 8 octets par case mémoire. Les raisons à cela sont multiple, mais nous les verrons en détail dans le chapitre sur l'alignement mémoire. Toujours est-il qu'il faut distinguer les adresses mémoire et les adresses d'octet gérées par le processeur.

Le processeur génère des adresses d'octet, qui permettent de sélectionner un octet bien précis. L'adresse d'octet permet de sélectionner un octet parmi tous les autres. Mais la mémoire ne comprend pas directement cette adresse d'octet. Heureusement, l'octet en question est dans une case mémoire bien précise, qui a elle-même une adresse mémoire bien précise. L'adresse d'octet est alors convertie en une adresse mémoire, qui sélectionne la case mémoire adéquate, celle qui contient l'octet voulu. La case mémoire entière est lue, puis le processeur ne récupère que les données adéquates. Pour cela, des circuits d'alignement mémoire se chargent de faire la conversion entre adresses du processeur et adresse mémoire. Nous verrons cela dans le détail dans le chapitre sur l'alignement mémoire.

Il existe des mémoires qui n'utilisent pas d'adresses mémoire, mais passons : ce sera pour la suite du cours.

Dans les ordinateurs, l'unité de traitement porte le nom de processeur, ou encore de Central Processing Unit, abrévié en CPU. Un processeur 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. Tout ordinateur contient au moins un processeur. Je dis au moins un, car un ordinateur peut avoir plusieurs processeurs.

Le processeur effectue des instructions, dont des calculs

[modifier | modifier le wikicode]

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, comme l'addition, la soustractions, la multiplication, la division. 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. Et il y en d'autres.

L'important est de retenir qu'un processeur fait beaucoup de calculs. La plupart des processeurs actuels supportent au minimum l'addition, la soustraction et la multiplication. Quelques processeurs ne gèrent pas la division, qui est une opération très gourmande en circuit, peu utilisée, très lente. Il arrive que des processeurs très peu performants ne gèrent pas la multiplication, mais c'est assez rare. Les autres instructions ne sont pas très intuitives, aussi passons-les sous silence pour le moment, nous n'aurons besoin de les comprendre que dans la section du cours sur le processeur.

L'intérieur d'un processeur n'est pas très compliqué. Il contient évidemment des circuits de calcul qui sont regroupés dans une ou plusieurs unité de calcul. Nous avons déjà vu dans les chapitres précédents comment fabriquer une unité de calcul simple, dans un chapitre dédié, et il s'agit de la même unité de calcul qu'on trouve dans le processeur. Elle est cependant complétée par d'autres circuits, pour les multiplications/division/autres.

L'unité de calcul d'un processeur est associée à des registres et une interface de communication avec la mémoire RAM. Le tout est interconnecté, afin de pouvoir échanger des données. Il faut aussi ajouter des circuits pour commander le tout, qui sont regroupés dans l'unité de contrôle. L'unité de contrôle lit les instructions en mémoire, puis commande l'unité de calcul, les registres et la mémoire pour que l'instruction soit exécutée correctement. L'unité de contrôle est assez complexe et aura droit à plusieurs chapitres dédié dans la suite de ce cours, le réseau d'interconnexion et les registres auront droit à un chapitre dédié.

Microarchitecture d'un processeur

Un processeur contient des registres et communique avec la mémoire

[modifier | modifier le wikicode]

Tout processeur contient des registres pour fonctionner, leur utilité dépendant du registre considéré. Pour rappel, ce sont de petites mémoires très rapides et de faible capacité, capables de mémoriser un nombre, ou du moins une petite suite de quelques bits. Les registres du processeur peuvent servir à plein de choses : stocker des données afin de les manipuler plus facilement, stocker l'adresse de la prochaine instruction, stocker l'adresse d'une donnée à aller chercher en mémoire, etc.

Les registres les plus simples à comprendre contiennent les opérandes et les résultats des opérations de calcul, appelons-les registres de données. La capacité des registres de données dépend fortement du processeur, et elle détermine la taille des données manipulée par le processeur. Par exemple, un processeur avec des registres de données de 8 bits ne peut pas gérer des données plus grandes qu'un octet, sauf en trichant de manière logicielle. De même, un processeur ayant des registres de 32 bits ne peut pas gérer des opérandes de plus de 32 bits, idem pour les résultats ce qui fait que les débordements d'entiers apparaissent quand un résultat dépasse les 32 bits.

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 des PC utilisent des registres de 64 bits, même s'il existe toujours des processeurs de faible performance avec des registres relativement petits, de 8 à 16 bits.

Notons qu'un processeur incorpore souvent des instructions pour copier des données provenant de la mémoire RAM dans un registre, et des instructions qui font l'inverse (d'un registre vers la mémoire). Sans cela, les registres seraient un peu difficiles à utiliser. Les instructions en question sont appelées LOAD (copie RAM vers registre) et STORE (copie registre vers RAM). Les échanges de données entre RAM et registres sont fréquents, les instructions LOAD et STORE sont tout aussi importante que les instructions de calcul. Tout cela pour dire qu'il ne faut pas confondre instruction avec opération mathématique, la notion d'instruction est plus large. Mais cela sera certainement plus claire quand on verra l'ensemble des instructions que peut gérer un processeur, dans un chapitre dédié.

Mais les registres de données ce ne sont pas les seuls. Pour pouvoir fonctionner, tout processeur doit mémoriser un certain nombre d’informations nécessaires à son fonctionnement : il faut qu'il se souvienne à quel instruction du programme il en est, qu'il connaisse la position en mémoire des données à manipuler, etc. Et ces informations sont mémorisées dans des registres spécialisés, appelés des registres de contrôle. Ils sont intégrés dans l'unité de contrôle et ne sont pas relié aux unités de calcul, contrairement aux autres registres.

La plupart ont des noms assez barbares (registre d'état, program counter) et nous ne pouvons pas en parler à ce moment du cours car nous n'en savons pas assez sur le fonctionnement d'un processeur pour expliquer à quoi ils servent. Il y a cependant une exception, un registre particulier présent sur presque tous les ordinateurs existants au monde, qu'il est important de voir maintenant : le program counter.

Le processeur exécute un programme, une suite d'opérations

[modifier | modifier le wikicode]

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. Un 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 l'ordinateur est rendu programmable : modifier le contenu de la mémoire permet de changer le programme exécuté. Mine de rien, cette idée de stocker le 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.

Les instructions sont exécutées dans un ordre bien précis, les unes après les autres. L'ordre en question est décidé par le programmeur. Sur la grosse majorité des ordinateurs, les instructions sont placées les unes à la suite des autres dans l'ordre où elles doivent être exécutées. Un programme informatique n'est donc qu'une vulgaire suite d'instructions stockée quelque part dans la mémoire de l'ordinateur.

Exemple de programme informatique
Adresse Instruction
0 Copier le contenu de l'adresse 0F05 dans le registre numéro 5
1 Charger le contenu de l'adresse 0555 dans le registre numéro 4
2 Additionner ces deux nombres
3 Charger le contenu de l'adresse 0555
4 Faire en XOR avec le résultat antérieur
... ...
5464 Instruction d'arrêt

Pour exécuter une suite d'instructions dans le bon ordre, le processeur détermine à chaque cycle quelle est la prochaine instruction à exécuter. Le processeur mémorise l'adresse de la prochaine instruction dans un registre spécialisé appelé Program Counter. Cette 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 assez simplement. Il suffit de prendre l'adresse de l'instruction en cours, et en ajoutant la longueur de l'instruction (le nombre de case mémoire qu'elle occupe). En clair, il suffit d'incrémenter le program counter de la longueur de l'instruction. Le program counter fait partie de l'unité de contrôle.

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.

Un ordinateur peut avoir plusieurs processeurs

[modifier | modifier le wikicode]

La plupart des ordinateurs n'ont qu'un seul processeur, ce qui fait qu'on désigne avec le terme d'ordinateurs mono-processeur. Mais il a existé (et existe encore) des ordinateurs multi-processeurs, avec plusieurs processeurs sur la même carte mère. L'idée était de gagner en performance : deux processeurs permettent de faire deux fois plus de calcul qu'un seul, quatre permettent d'en faire quatre fois plus, etc. C'est très courant sur les supercalculateurs, des ordinateurs très puissants conçus pour du calcul industriel ou scientifique, mais aussi sur les serveurs ! Dans le cas le plus courant, ils utilisent plusieurs processeurs identiques : on utilise deux processeurs Core i3 de même modèle, ou quatre Pentium 3, etc.

Pour utiliser plusieurs processeurs, les programmes doivent être adaptés. Pour cela, il y a plusieurs possibilités :

  • Une première possibilité, assez intuitive, est d’exécuter des programmes différents sur des processeurs différents. Par exemple, on exécute le navigateur web sur un processeur, le lecteur vidéo sur un autre, etc.
  • La seconde option est de créer des programmes spéciaux, qui utilisent plusieurs processeurs. Ils répartissent les calculs à faire sur les différents processeurs. Un exemple est la lecture d'une vidéo sur le web : un processeur peut télécharger la vidéo pendant le visionnage et bufferiser celle-ci, un autre processeur peut décoder la vidéo, un autre décoder l'audio. De tels programmes restent des suites d'instructions, mais ils sont plus complexes que les programmes normaux, aussi nous les passons sous silence.
  • La troisième option est d’exécuter le même programme sur les différents processeurs, mais chaque processeur traite son propre ensemble de données. Par exemple, pour un programme de rendu 3D, quatre processeurs peuvent s'occuper chacun d'une portion de l'image.
Architecture de Von Neumann Princeton multi processeurs

De nos jours, les ordinateurs grand public les plus utilisés sont dans un cas intermédiaire, ils ne sont ni mono-, ni multi-processeur. Ils n'ont qu'un seul processeur, dans le sens où si on ouvre l'ordinateur et qu'on regarde la carte mère, il n'y a qu'un seul processeur. Mais ce processeur est en réalité assez similaire à un regroupement de plusieurs processeurs dans le même boitier. Il s'agit de processeurs multicœurs, qui contiennent plusieurs cœurs, chaque cœur pouvant exécuter un programme tout seul.

La différence entre cœur et processeur est assez difficile à saisir, mais pour simplifier : un cœur est l'ensemble des circuits nécessaires pour exécuter un programme. Chaque cœur dispose de toute la machinerie électronique pour exécuter un programme, à savoir des circuits aux noms barbares comme : un séquenceur d'instruction, des registres, une unité de calcul. Par contre, certains circuits d'un processeur ne sont présents qu'en un seul exemplaire dans un processeur multicœur, comme les circuits de communication avec la mémoire ou les circuits d’interfaçage avec la carte mère.

Suivant le nombre de cœurs présents dans notre processeur, celui-ci sera appelé un processeur double-cœur (deux cœurs), quadruple-cœur (4 cœurs), octuple-cœur (8 cœurs), etc. Un processeur double-cœur est équivalent à avoir deux processeurs dans l'ordinateur, un processeur quadruple-cœur est équivalent à avoir quatre processeurs dans l'ordinateur, etc. Ces processeurs sont devenus la norme dans les ordinateurs grand public et les logiciels et systèmes d'exploitation se sont adaptés.

Les coprocesseurs

[modifier | modifier le wikicode]

Quelques ordinateurs assez anciens disposaient de coprocesseurs, des processeurs qui complémentaient un processeur principal. Les ordinateurs de ce type avaient un processeur principal, le CPU, qui était secondé par un ou plusieurs coprocesseurs. En théorie, le coprocesseur exécute des calculs que le CPU n'est pas capable de faire. Il y a cependant quelques exceptions, où les coprocesseurs effectuent des calculs que le CPU est capable de faire. Mais passons cela sous silence pour le moment et voyons à quoi peuvent servir ces coprocesseurs.

Les coprocesseurs arithmétiques sont de loin les plus simples à comprendre. Ils permettent de faire certains calculs que le processeur ne peut pas faire. Les plus connus d'entre eux étaient utilisés pour implémenter les calculs en virgule flottante; à une époque où les CPU de l'époque ne géraient que des calculs entiers (en binaire ou en BCD). Sans ce coprocesseur, les calculs flottants étaient émulés en logiciel, par des fonctions et libraires spécialisées, très lentes. Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Certaines applications conçues pour le coprocesseur étaient capables d'en tirer profit : des logiciels de conception assistée par ordinateur, par exemple. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants.

Un autre exemple de coprocesseur est celui utilisé sur la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques.

Les coprocesseurs les plus connus, au-delà des coprocesseurs arithmétiques, sont les coprocesseurs pour le rendu 2D/3D et les coprocesseurs sonores. Ils ont eu leur heure de gloire sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles de cette génération ou antérieure. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un co-processeur sonore. Le processeur principal était un Motorola 68000, alors que le co-processeur sonore était un processeur Z80.

Enfin, il faut aussi citer les coprocesseurs pour l'accès aux périphériques. L'accès aux périphériques est quelque chose sur lequel nous passerons plusieurs chapitres dans ce cours. Mais sachez que l'accès aux périphériques peut demander pas mal de puissance de calculs. Le CPU principal peut faire ce genre de calculs par lui-même, mais il n'est pas rare qu'un coprocesseur soit dédié à l'accès aux périphériques.

Un exemple assez récent est celui, là encore, de la Nintendo 3DS. Elle disposait d'un processeur principal de type ARM9, du coprocesseur pour les divisions, et d'un second processeur ARM7. L'ARM 7 était le seul à communiquer avec les périphériques et les entrées-sorties. Il était utilisé presque exclusivement pour cela, ainsi que pour l'émulation de la console GBA. Il est donc utilisé comme coprocesseur d'I/O, mais n'est pas que ça.

Co-processeur pour l'accès aux entrées-sorties.

Maintenant que nous venons de voir différents types de coprocesseurs, passons maintenant aux généralités sur ceux-ci. Le CPU peut soit exécuter des programmes en parallèle du coprocesseur, soit se mettre en pause en attendant que le coprocesseur finisse son travail. Dans l'exemple des coprocesseurs arithmétiques, le processeur principal passe la main au coprocesseur et attend sagement qu'i finisse son travail. Les deux processeurs se passent donc la main pour exécuter un programme unique. On parle alors de coprocesseurs fortement couplés. Pour les autres coprocesseurs, le CPU et le coprocesseur travaillent en parallèle et exécutent des programmes différents. On a un programme qui s’exécute sur le coprocesseur, un autre qui s’exécute sur le CPU. On parle alors de coprocesseurs faiblement couplés. C'est le cas pour les coprocesseurs d'accès au périphérique, pour ceux de rendu 2D/3D, etc.

Dans les deux cas, les programmes doivent être codés de manière à tirer parti du coprocesseur. Sans aide de la part du logiciel, le coprocesseur est inutilisable. Et c'est un défaut qui a été responsable de la disparition des coprocesseurs dans les ordinateurs grand public. La présence du coprocesseur étant optionnelle, les programmeurs devaient en tenir compte. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution est de recourir à l'émulation logicielle des instructions du coprocesseur en son absence. Dans les deux cas, c'était beaucoup de complications pour pas grand-chose. Aussi, les fonctions des coprocesseurs ont aujourd'hui été intégrées dans les processeurs modernes, ce qui les rendait redondants.

À l'inverse, le hardware d'une console est toujours le même d'un modèle à l'autre, contrairement à la forte variabilité des composants sur PC. Les programmeurs n'hésitaient pas à utiliser le coprocesseur, qui était là avec certitude, ils n'avaient pas à créer deux versions de leurs jeux vidéo, ni à émuler un coprocesseur absent, etc. Ajoutons que les concepteurs de consoles n'hésitent pas à utiliser des processeurs grand public dans leurs consoles, quitte à les compléter par des coprocesseurs. Au lieu de créer un processeur sur mesure, autant prendre un processeur déjà existant et le compléter avec un coprocesseur pour être plus puissant que la concurrence. Ce qui explique que les coprocesseurs graphiques et sonores ont eu leur heure de gloire sur les anciennes consoles de jeux vidéo.

Les entrées-sorties

[modifier | modifier le wikicode]

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.

Dans ce qui va suivre, nous allons parfois parler de périphériques au lieu d'entrées-sorties. Les deux termes ne sont pas synonymes. En théorie, les périphériques, sont les composants connectés sur l'unité centrale. Exemple : les claviers, souris, webcam, imprimantes, écrans, clés USB, disques durs externes, les câbles Ethernet de la Box internet, etc. les entrées-sorties incluent les périphériques, mais aussi d'autres composants comme les cartes d'extensions ou des composants installés sur la carte mère. Les cartes d'extension sont les composants qui se connectent sur la carte mère via un connecteur, comme les cartes son ou les cartes graphiques. D'autres composants sont soudés à la carte mère mais sont techniquement des entrées-sorties : les cartes sons soudées sur les cartes mères actuelles, par exemple. Mais par simplicité, nous parlerons de périphériques au lieu d'entrées-sorties.

L'interface avec le reste de l'ordinateur

[modifier | modifier le wikicode]

Les entrées-sorties sont très diverses, fonctionnent très différemment les unes des autres. Mais du point de vue du reste de l'ordinateur, les choses sont relativement standardisées. Du point de vue du processeur, les entrées-sorties sont juste des paquets de registres ! Tous les périphériques, toutes les entrées-sorties contiennent des registres d’interfaçage, qui permettent de faire l'intermédiaire entre le périphérique et le reste de l'ordinateur. Le périphérique est conçu pour réagir automatiquement quand on écrit dans ces registres.

Registres d'interfaçage.

Les registres d’interfaçage sont assez variés. Les plus évidents sont les registres de données, qui permettent l'échange de données entre le processeur et les périphériques. Pour échanger des données avec le périphérique, le processeur a juste à lire ou écrire dans ces registres de données. 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. Si le processeur veut envoyer une donnée à un périphérique, il a juste à écrire dans ces registres. Inversement, s'il veut lire une donnée, il a juste à lire le registre adéquat.

Mais le processeur ne fait pas que transmettre des données au périphérique. Le processeur lui envoie aussi des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. En clair, ce sont l'équivalent des instructions du processeur, mais pour le périphérique. Par exemple, les commandes envoyées à une carte graphique peuvent être : affiche l'image présente à cette adresse mémoire, calcule le rendu 3D à partir des données présentes dans ta mémoire, etc. Pour recevoir les commandes, le périphérique contient des registres de commande 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 occupé, qu'il y a un problème, qu'il y a une erreur de configuration, etc.

Les adresses des registres d’interfaçage

[modifier | modifier le wikicode]

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.

Il existe deux organisations possible pour les adresses des registres d’interfaçages. La première possibilité est de séparer les adresses pour les registres d’interfaçage et les adresses pour la mémoire. 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.

Espaces d'adressages séparés entre mémoire et périphérique

L'autre méthode mélange les adresses mémoire et des entrées-sorties. Si on prend par exemple un processeur de 16 bits, où les adresses font 16 bits, alors les 65536 adresses possibles seront découpées en deux portions : une partie ira adresser la RAM/ROM, l'autre les périphériques. On parle alors d'entrées-sorties mappées en mémoire. L'avantage est que le processeur n'a pas besoin d'avoir des instructions séparées pour les deux.

IO mappées en mémoire

Le pilote de périphérique

[modifier | modifier le wikicode]

Utiliser un périphérique se résume donc à lire ou écrire les valeurs adéquates dans les registres d’interfaçage. Les registres en question ont une adresse, similaire à l'adresse mémoire des RAM/ROM. Les adresses en question ne sont pas forcément mélangées, la relation entre adresses mémoire et adresses de périphériques est compliquée et sera vue dans la suite du chapitre. Communiquer avec un périphérique est similaire à ce qu'on a avec les mémoires, c'est simple : lire ou écrire dans des registres.

Le problème est que le système d'exploitation ne connaît pas toujours le fonctionnement d'un périphérique : 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 » périphérique est très simple : il suffit de savoir quoi mettre dans les registres, et c'est le pilote qui s'en charge.

Le bus de communication

[modifier | modifier le wikicode]

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.

Les bus d'adresse, de données et de commande

[modifier | modifier le wikicode]

Pour permettre au processeur (ou aux périphériques) de communiquer avec la mémoire, il y a trois prérequis qu'un 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.

Contenu d'un bus, généralités.

Tous les ordinateurs ne sont pas organisés de la même manière, pour ce qui est de leurs bus. Dans les grandes lignes, on peut distinguer deux possibilités : soit l'ordinateur a un seul bus, soit il en a plusieurs.

Les bus systèmes

[modifier | modifier le wikicode]

Si l'ordinateur dispose d'un bus unique, celui-ci est appelé le bus système, aussi appelé backplane bus. Il s'agissait de l'organisation utilisée sur les tout premiers ordinateurs, pour sa simplicité. Elle était parfaitement adaptée aux anciens composants, qui allaient tous à la même vitesse. De nos jours, les ordinateurs à haute performance ne l'utilisent plus trop, mais elle est encore utilisée sur certains systèmes embarqués, en informatique industrielle dans des systèmes très peu puissants.

Bus système basique.

De tels bus avaient pour avantage que la communication entre composant était simple. Le processeur peut communiquer directement avec la mémoire et les périphériques, les périphériques peuvent communiquer avec la mémoire, etc. Il n'y a pas de limitations quant aux échanges de données.

Un autre avantage est que le processeur ne doit gérer qu'un seul bus, ce qui utilise peu de broches. 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. 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 ils ont aussi des désavantages. Par exemple, il faut gérer les accès au bus de manière à ce que le processeur et les entrées-sorties ne se marchent pas sur les pieds, en essayant d'utiliser le bus en même temps. De tels conflits d'accès au bus système sont fréquents et ils réduisent la performance, comme on le verra dans le chapitre sur les bus. De plus, un bus système a 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.

Un bus système contient un bus d'adresse, de données et de commande. Le bus d'adresse ne sert pas que pour l'accès à la mémoire RAM/ROM, mais aussi pour l'accès aux entrées-sorties. En théorie, un bus système se marie bien avec des entrées-sorties mappées en mémoire. Il y a moyen d'implémenter un système d'adresse séparés avec, mais c'est pas l'idéal.

Architecture Von Neumann avec les bus.

Les bus spécialisés

[modifier | modifier le wikicode]

Pour éliminer ces problèmes, beaucoup d'ordinateurs disposent de plusieurs bus, plus ou moins spécialisés. Nous verrons des exemples de tels systèmes à la fin du chapitre. Pour le moment, citons un exemple assez courant : le cas où on a un bus séparé pour la mémoire, et un autre séparé pour les entrées-sorties. Le bus spécialisé pour la mémoire est appelé le bus mémoire, l'autre bus n'a pas de nom précis, mais nous l’appellerons le bus d'entrées-sorties. Une telle organisation implique d'avoir des adresses séparées pour les registres d’interfaçage et la mémoire. Pas d'entrée-sortie mappée en mémoire !

Bus mémoire séparé du bus pour les IO

Les avantages de tels bus sont nombreux. Par exemple, le processeur peut accéder à la mémoire pendant qu'il attend qu'un périphérique lui réponde sans trop de problèmes. De plus, l'on a pas à gérer les conflits d'accès au bus entre la mémoire et les périphériques. Mais surtout, les bus peuvent être adaptés et simplifiés. Par exemple, le bus pour les entrées-sorties peut se passer de bus d'adresse, avoir un bus de commande différent de celui de la mémoire, avoir des bus de données de taille différentes, etc. Il est ainsi possible d'avoir un bus mémoire capable de lire/écrire 16 bits à la fois, alors que la communication avec les entrées-sorties se fait octet par octet ! Plutôt que d'avoir un seul bus qui s'adapte aux mémoires et entrées-sorties, on a des bus spécialisés.

L'avantage principal de cette adaptation est que la mémoire et les périphériques ne vont pas à la même vitesse du tout. Il est alors possible d'avoir un bus mémoire ultra-rapide et qui fonctionne à haute fréquence, pendant que le bus pour les entrées-sorties est un bus plus simple, moins rapide. Au lieu d'avoir un bus système moyen en vitesse, on a deux bus qui vont chacun à la vitesse adéquate.

Mais il y a d'autres défauts. Par exemple, il faut câbler deux bus distincts sur le processeur. Le nombre de broches nécessaires augmente drastiquement. Et cela peut poser problème si le processeur n'a pas beaucoup de broches à la base. Aussi, les processeurs avec peu de broches utilisent de préférence un bus système, plus simple à câbler, bien que moins performant. Un autre problème est que les entrées-sorties ne peuvent pas communiquer avec la mémoire directement, elles doivent passer par l'intermédiaire du processeur. De tels échanges ne sont pas forcément nécessaires, mais les performances s'en ressentent s’ils le sont.

Les bus avec répartiteur

[modifier | modifier le wikicode]

Il existe une méthode intermédiaire, qui garde deux bus séparés pour la mémoire et les entrées-sorties, mais élimine les problèmes de brochage sur le processeur. L'idée est d'intercaler, entre le processeur et les deux bus, un circuit répartiteur. Il récupère tous les accès et distribue ceux-ci soit sur le bus mémoire, soit sur le bus des périphériques. Le ou les répartiteurs s'appellent aussi le chipset de la carte mère.

C'était ce qui était fait à l'époque des premiers Pentium. À 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.

IO mappées en mémoire avec séparation des bus

Niveau adresses des registres d'interfacage, il est possible d'avoir soit des adresses unifiées avec les adresses mémoire, soit des adresses séparées.

Les architectures Harvard et Von Neumann

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Organisation des espaces d'adressage sur une archi harvard modifiée

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.

Étude de quelques exemples d'architectures

[modifier | modifier le wikicode]

Les ordinateurs avec un CPU, une RAM et une ROM, sont des ordinateurs très simples. Tous les ordinateurs modernes, mais aussi dans les smartphones, les consoles de jeu et autres, utilisent une architecture grandement modifiée et améliorée, avec un grand nombre de périphériques, des systèmes d'exploitation sur des disques durs/SSD, un grand nombre de mémoires différentes, etc. Seuls les systèmes assez anciens, ainsi que les systèmes embarqués ou d'informatique industrielle, se contentent de l'architecture de base CPU/RAM/ROM. Aussi, nous allons voir quelques exemples de systèmes anciens, précisément des consoles de jeu.

L'architecture de la console de jeu NES

[modifier | modifier le wikicode]

Dans cette section, nous allons étudier l'exemple de la console de Jeu Famicom, aussi appelée la NES en occident. La console de base a une architecture très simple, illustrée ci-dessous, avec seulement un CPU, de la RAM, et quelques entrées-sorties. On voit qu'elle est centrée sur un processeur Ricoh 2A03, similaire au processeur 6502, un ancien processeur autrefois très utilisé et très populaire. Le processeur est associé à 2 KB de mémoire RAM et à une mémoire ROM.

Architecture de la NES

La mémoire ROM se trouve dans la cartouche de jeu, et non dans la console. Elle contient le programme du jeu, comme on pourrait s'y attendre. Sur certaines cartouches, on trouve une RAM utilisée pour les sauvegardes, qui est adressée par le processeur directement. Première variation par rapport à l'architecture de base : on a plusieurs RAM, une généraliste et une autre pour les sauvegardes.

Les entrées-sorties sont au nombre de deux : une carte son et une carte vidéo. La carte son est le composant qui s'occupe de commander les haut-parleurs et de gérer tout ce qui a rapport au son. La carte graphique est le composant qui es t en charge de calculer les graphismes, tout ce qui s'affiche à l'écran. La carte graphique est connectée à 2 KB de RAM, séparée de la RAM normale, via un bus séparé. De plus, la carte graphique est connectée via un autre bus à une ROM/RAM qui contient les sprites et textures du jeu, qui est dans la cartouche. Sur cette console, les cartes son et graphique ne sont PAS des co-processeurs. Elles ne sont pas programmables, ce sont des circuits électroniques fixes, non-programmables. C'est totalement différent de ce qu'on a sur les consoles modernes, aussi le préciser est important.

L'organisation des bus est assez simple, bien qu'elle se démarque de l'architecture de base avec un bus système : on ne trouve pas un seul bus de communication, mais plusieurs. Déjà, il s'agit d'une architecture Harvard, car la ROM et la RAM utilisent des bus différents. De plus, on a un bus qui connecte le processeur aux autres entrées-sorties, séparé des bus pour les mémoires. Ce bus est relié à la carte graphique et la carte sonore. Mais il n'est pas le seul bus dédié aux périphériques : les manettes sont connectées directement sur le processeur, via un bus dédié !

L'architecture de la SNES

[modifier | modifier le wikicode]

L'architecture de la SNES est plus complexe que pour la NES. L'architecture est illustrée ci-dessous. La RAM a augmenté en taille et passe à 128 KB. Pareil pour la RAM de la carte vidéo, qui passe à 64 KB. On remarque un changement complet au niveau des bus : il n'y a plus qu'un seul bus sur lequel tout est connecté : ROM, RAM, entrées-sorties, etc. La seule exception est pour les manettes, qui sont encore connectées directement sur le processeur, via un bus séparé. La console a donc un bus système, mais qui est malgré tout complété par un bus pour les manettes, chose assez originale.

Un autre changement est que la carte graphique est maintenant composée de deux circuits séparés. Encore une fois, il ne s'agit pas de coprocesseurs, mais de circuits non-programmables. Par contre, la carte son est remplacée par deux coprocesseurs audio ! De plus, les deux processeurs sont connectés à une mémoire RAM dédiée de 64 KB, comme pour la carte graphique. L'un est un processeur 8 bits (le DSP), l'autre est un processeur 16 bits.

Architecture de la SNES

Un point très intéressant : certains jeux intégraient des coprocesseurs dans leurs cartouches de jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenait un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. Le Cx4 faisait plus ou moins la même chose, il était spécialisé dans les calculs trigonométriques, et diverses opérations de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs d'utiliser et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche.

Les system on chip et microcontrôleurs

[modifier | modifier le wikicode]

Parfois, on décide de regrouper la mémoire, les bus, le CPU et les ports d'entrée-sortie dans un seul circuit intégré, un seul boitier. L'ensemble forme alors ce qu'on appelle un System on Chip (système sur une puce), abrévié en SoC. Le nom est assez explicite : un SoC comprend un système informatique complet sur une seule puce de silicium, microprocesseurs, mémoires et périphériques inclus. Ils incorporent aussi des timers, des compteurs, et autres circuits très utiles.

Le terme SoC regroupe des circuits imprimés assez variés, aux usages foncièrement différents et à la conception distincte. Les plus simples d’entre eux sont des microcontrôleurs, qui sont utilisés pour des applications à base performance. Les plus complexes sont utilisés pour des applications qui demandent plus de puissance, nous les appellerons SoC haute performance. La relation entre SoC et microcontrôleurs est assez compliquée à expliquer, la terminologie n'est pas clairement établie. Il existe quelques cours/livres qui séparent les deux, d'autres qui pensent que les deux sont très liés. Dans ce cours, nous allons partir du principe que tous les systèmes qui regroupent processeur, mémoire et quelques périphériques/entrées-sorties sont des SoC. Les microcontrôleurs sont donc un cas particulier de SoC, en suivant cette définition.

Les microcontrôleurs

[modifier | modifier le wikicode]

Un exemple d'intégration assez similaire aux SoC est le cas des microcontrôleurs, des composants utilisés dans l'embarqué ou d'informatique industrielle. Leur nom trahit leur rôle. Ils sont utilisés pour contrôler de l'électroménager, des chaines de fabrication dans une usine, des applications robotiques, les alarmes domestiques, les voitures. De manière générale, on les trouve dans tous les systèmes dits embarqués et/ou temps réel. Ils ont besoin de s'interconnecter à un grand nombre de composants et intègrent pour cela un grand nombre d'entrée-sorties. Les microcontrôleurs sont généralement peu puissants, et doivent consommer peu d'énergie/électricité.

Microcontrôleur Intel 8051.

Un microcontrôleur tend à intégrer des entrées-sorties assez spécifiques, qu'on ne retrouve pas dans les SoC destinés au grand public. Un microcontrôleur est typiquement relié à un paquet de senseurs et son rôle est de commander des moteurs ou d'autres composants. Et les entrées-sorties intégrées sont adaptées à cette tâche. Par exemple, ils tendent à intégrer de nombreux convertisseurs numériques-analogiques pour gérer des senseurs. Ils intègrent aussi des circuits de génération de signaux PWM spécialisés pour commander des moteurs, le processeur peut gérer des calculs trigonométriques (utiles pour commander la rotation d'un moteur),etc.

SoC basé sur un processeur ARM, avec des entrées-sorties typiques de celles d'un µ-contrôleur. Le support du bus CAN, d'Ethernet, du bus SPI, d'un circuit de PWM (génération de signaux spécifiques), de convertisseurs analogique-digital et inverse, sont typiques des µ-contrôleurs.

Fait amusant, on en trouve dans certains périphériques informatiques. Par exemple, les anciens disques durs intégraient un microcontrôleur qui contrôlait plusieurs moteurs/ Les moteurs pour faire tourner les plateaux magnétiques et les moteurs pour déplacer les têtes de lecture/écriture étaient commandés par ce microcontrôleur. Comme autre exemple, les claviers d'ordinateurs intègrent un microcontrôleur connecté aux touches, qui détecte quand les touches sont appuyées et qui communique avec l'ordinateur. Nous détaillerons ces deux exemples dans les chapitres dédiés aux périphériques et aux disques durs, tout deviendra plus clair à ce moment là. La majorité des périphériques ou des composants internes à un ordinateur contiennent des microcontrôleurs.

Les SoC haute performance

[modifier | modifier le wikicode]

Les SoC les plus performants sont actuellement utilisés dans les téléphones mobiles, tablettes, Netbook, smartphones, ou tout appareil informatique grand public qui ne doit pas prendre beaucoup de place. La petite taille de ces appareils fait qu'ils gagnent à regrouper toute leur électronique dans un circuit imprimé unique. Mais les contraintes font qu'ils doivent être assez puissants. Ils incorporent des processeurs assez puissants, surtout ceux des smartphones. C'est absolument nécessaire pour faire tourner le système d'exploitation du téléphone et les applications installées dessus.

Niveau entrées-sorties, ils incorporent souvent des interfaces WIFI et cellulaires (4G/5G), des ports USB, des ports audio, et même des cartes graphiques pour les plus puissants d'entre eux. Les SoC incorporent des cartes graphiques pour gérer tout ce qui a trait à l'écran LCD/OLED, mais aussi pour gérer la caméra, voire le visionnage de vidéo (avec des décodeurs/encodeurs matériel). Par exemple, les SoC Tegra de NVIDIA incorporent une carte graphique, avec des interfaces HDMI et VGA, avec des décodeurs vidéo matériel H.264 & VC-1 gérant le 720p. Pour résumer, les périphériques sont adaptés à leur utilisation et sont donc foncièrement différents de ceux des microcontrôleurs.

Hardware d'un téléphone. On voit qu'il est centré autour d'un SoC, complété par de la RAM, un disque dur de faible capacité, de quoi gérer les entrées utilisateurs (l'écran tactile, les boutons), et un modem pour les émissions téléphoniques/2G/3G/4G/5G.

Un point important est que les processeurs d'un SoC haute performance sont... performants. Ils sont le plus souvent des processeurs de marque ARM, qui sont différents de ceux utilisés dans les PC fixe/portables grand public qui sont eux de type x86. Nous verrons dans quelques chapitres en quoi consistent ces différences, quand nous parlerons des jeux d'instruction du processeur. Autrefois réservé au monde des PCs, les processeurs multicœurs deviennent de plus en plus fréquents pour les SoC de haute performance. Il n'est pas rare qu'un SoC incorpore plusieurs cœurs. Il arrive même qu'ils soient foncièrement différents, avec plusieurs cœurs d'architecture différente.

La frontière entre SoC haute performance et microcontrôleur est de plus en plus floue. De nombreux appareils du quotidien intègrent des SoC haute performance, d'autres des microcontrôleurs. Par exemple, les lecteurs CD/DVD/BR et certains trackers GPS intègrent un SoC ou des processeurs dont la performance est assez pêchue. À l'opposé, les systèmes domotiques intègrent souvent des microcontrôleurs simples. Malgré tout, les deux cas d'utilisation font que le SoC/microcontrôleur est connecté à un grand nombre d'entrées-sorties très divers, comme des capteurs, des écrans, des LEDs, etc.

Hardware d'un tracker GPS.


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.

La raison à cela est que plus une mémoire peut contenir de données, plus elle est lente. On doit faire le choix entre une mémoire de faible capacité et très performante, ou une mémoire très performante mais très petite. Les cas intermédiaires, avec une capacité et des performances intermédiaires, existent aussi. 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.

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. Les mémoires d'un ordinateur moderne sont les suivantes :

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 Kibi- ou mébi-octets Mémoire incorporée dans le processeur, sauf pour d'anciens processeurs
Mémoire RAM 1 microsecondes Gibioctets Mémoire primaire
Mémoires de masse (Disque dur, disque SSD, autres) 1 millisecondes Dizaines à centaines de gibioctets Mémoire secondaire

Précisons cependant que le compromis capacité-performance n'est pertinent que quand on compare des mémoires avec des capacités très différentes, avec au moins un ordre de grandeur de différence. Entre un ordinateur avec 16 gibioctets de RAM et un autre avec 64 gibioctets, les différences de performances sont marginales. Par contre, la différence entre un cache de quelques mébioctets et une RAM de plusieurs gibioctets, la différence est très importante. Ce qui fait que l'ensemble des mémoires de l'ordinateur est organisé en plusieurs niveaux, avec des registres ultra-rapides, des caches intermédiaires, une mémoire RAM un peu lente, et des mémoires de masse très lentes.

La distinction entre mémoire primaire et secondaire

[modifier | modifier le wikicode]

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 : les mémoires primaires directement accessibles par le processeur, et la mémoire secondaire accessible comme les autres périphériques. La mémoire primaire, correspond aux mémoire RAM et ROM de l'ordinateur, dans laquelle se trouvent 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.

Les mémoires secondaires sont généralement confondues avec les mémoires de masse, 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é, les mémoires de masse 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. C'est la raison pour laquelle mémoires de masse et mémoires secondaires sont souvent confondues.

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.

Les technologies de fabrication des mémoires secondaires sont à part

[modifier | modifier le wikicode]

Les mémoires de masse sont par nature des mémoires non-volatiles, à savoir qui ne s'effacent pas quand on coupe l'alimentation électrique, à l'opposé des mémoires RAM qui elles s'effacent quand on coupe le courant. Et ce fait nous dit quelque chose de très important : les mémoires de masse ne sont pas fabriquées de la même manière que les mémoires volatiles.

Les mémoires volatiles sont presque toutes électroniques, à quelques exceptions qui appartiennent à l'histoire de l'informatique. Elles sont fabriquées avec des transistors, que ce soit des transistors CMOS ou bipolaire. Et quand on cesse de l'alimenter en courant, les transistors repasse en état inactif, de repos, qui est soit fermé ou ouvert. Ils ne mémorisent pas l'état qu'ils avaient avant qu'on coupe le courant. On ne peut donc pas fabriquer de mémoire non-volatile avec des transistors ! Et ce genre de chose vaut pour les ancêtres du transistors, comme les thrysistors, les triodes, les tubes à vide et autres : ils permettaient de fabriquer des mémoires volatiles, mais rien d'autres.

Les mémoires ROM ne sont pas concernées par ce problème vu que ce sont de simples circuits combinatoires, qui n'ont pas besoin d'avoir de capacité de mémorisation proprement dit. Elles sont donc non-volatiles, mais le fait qu'on ne puisse pas modifier leur contenu rend la solution aisée.

Aussi, pour fabriquer des mémoires de masse, on doit utiliser des technologies différentes, on ne peut pas utiliser de transistors CMOS ou bipolaire normaux. Et le moins qu'on puisse dire est que les technologies des mémoires de masse sont très nombreuses, absolument tous les supports de mémorisation possibles ont été essayés et commercialisés. L'évolution des technologies de fabrication est difficile à résumer pour les mémoires de masse. Mais dans les grandes lignes, on peut distinguer quatre grandes technologies.

La solution la plus ancienne était d'utiliser un support papier, avec les cartes perforées. Mais cette solution a rapidement été remplacée par l'usage de d'un support de mémorisation magnétique, à savoir que chaque bit était attribué à un petit morceau de matériau magnétique. Le matériau magnétique peut être magnétisé dans deux sens N-S ou S-N, ce qui permet d'encoder un bit. C'est ainsi que sont nées les toutes premières mémoire de masse magnétique : les bandes magnétiques (similaires à celles utilisées dans les cassettes audio), les tambours magnétiques, les mémoires à tore de ferrite, et quelques autres. Par la suite, sont apparues les disquettes et les disques durs.

Par la suite, les CD-ROM, puis les DVD sont apparus sur le marchés. Ils sont regroupés sous le terme de mémoires optiques, car leur fonctionnement utilise les propriétés optiques du support de mémorisation, on les lit en faisant passer un laser très fin dessus. Ils n'ont cependant pas remplacé les disques durs, leur usage était tout autre. En effet, les mémoires optiques ne peuvent pas être effacées et réécrites. Sauf dans le cas des CD/DVD réincriptibles, mais on ne peut les effacer qu'un nombre limité de fois, mettons une dizaine. De plus, il faut les effacer intégralement avant de réécrire complétement leur contenu. Cette limitation fait qu'ils n'étaient pas utilisés pour mémoriser le système d'exploitation ou les programmes installés.

Toutes ces mémoires sont totalement obsolètes de nos jours, à l'exception des disques durs magnétiques. Et encore ces derniers tendent à disparaitre. Les mémoires de masse actuelles sont toutes... électroniques ! J'ai dit plus haut qu'il n'était pas possible de fabriquer des mémoires de masse/secondaires avec des transistors CMOS, je n'ai pas mentit. Les mémoires électronique actuelle sont des mémoires FLASH, qui sont fabriquées avec des transistors CMOS à grille flottante. Leur fonctionnement est différent des transistors CMOS normaux, ils ont une capacité de mémorisation que les transistors CMOS normaux n'ont pas. Par contre, leur procédé de fabrication est différent, ils ne sont pas fabriqués dans les mêmes usines que les transistors CMOS normaux.

Le démarrage de l'ordinateur à partir d'une mémoire secondaire

[modifier | modifier le wikicode]

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.

Ce schéma illustre l'organisation mémoire d'un ordinateur moderne, en très simplifié. On voit qu'il y a un disque dur (mémoire secondaire), qui contient le système d'exploitation. La RAM et la ROM sont toutes deux reliées au processeur par un bus unique. La ROM contient le firmware/BIOS, ainsi qu'un chargeur d'amorcage qui permet de charger l'OS dans la RAM. Le processeur, quant à lui, contient divers circuits que vous ne connaissez pas encore. Contentons-nous de dire qu'il contient plusieurs mémoires caches, ainsi que des registres (en violet).

L'ajout des mémoires caches et des local stores

[modifier | modifier le wikicode]
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és 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).

La hiérarchie mémoire d'un ordinateur moderne est une variante de la hiérarchie à deux niveaux de la section précédente (primaire et secondaire) à laquelle on a rajouté une ou plusieurs mémoires intermédiaires. Le niveau intermédiaire entre les registres et la mémoire principale regroupe deux types distincts de mémoires : les mémoires caches et les local stores. Les premiers sont des mémoires qui ne sont pas adressables et fonctionnent très différemment des mémoires RAM et ROM normales. Leur fonctionnement sera expliqué rapidement dans la section suivante, et détaillé dans un chapitre à part. À l'opposé, les local store sont des mémoires RAM adressables, très semblables à la mémoire principale, mais avec une plus fiable capacité et une vitesse plus importante.

Le rajout de ces niveaux supplémentaires est une question de performance. Les processeurs anciens pouvaient se passer de mémoires caches. Mais au fil du temps, les processeurs ont gagné en performances plus rapidement que la mémoire RAM et les processeurs ont incorporé des mémoires caches pour compenser la différence de vitesse entre processeur et mémoire RAM. Les caches sont beaucoup plus utilisés que les local store, ces derniers étant absent des processeurs commerciaux modernes, sauf peut-être dans quelques CPU dédiés aux applications embarquées. Ils sont présents dans les cartes graphiques modernes, l'ont été dans le CPU de la console Playstation 3, mais guère plus. À l'inverse, tous les processeurs disposent d'une ou plusieurs mémoires cache depuis au moins les années 90.

Hiérarchie mémoire

Les mémoires caches

[modifier | modifier le wikicode]

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. 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 le 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.

Le fonctionnement interne d'un cache sera expliqué dans le chapitre dédié aux mémoires caches. Pour le moment, tout ce qu'on peut dire est que la majorité des processeurs utilise des caches dit partiellement associatifs. Ils contiennent en leur sein une ou plusieurs mémoire RAM de petite taille, qui sont entourées par des circuits qui font fonctionner le tout comme un cache. Un cache est donc plus complexe qu'une RAM normale, du fait des circuit en plus. Il est plus gourmand en transistors, en consommation énergétique, etc.

Plus haut, on a vu que les mémoires secondaires ne sont pas fabriqués avec les mêmes technologies que les mémoires volatiles/RAM. Il en est de même avec les mémoires caches, ce qui explique la différence de performance entre RAM et cache. Les caches sont plus rapides, non seulement car ils sont plus petits, mais aussi car ils ne sont pas fabriqués comme des mémoires RAM. Les mémoires RAM actuelles sont des mémoires dites DRAM, alors que les caches sont fabriqués avec des mémoires dites SRAM. La différence sera expliquée dans quelques chapitres, retenez simplement que les procédés de fabrication sont différents. La SRAM est rapide, mais a une faible capacité, la DRAM est lente et de forte capacité. La raison est que 1 bit de SRAM prend beaucoup de place et utilise beaucoup de circuits, alors que les DRAM sont plus économes en circuits et en espace.

Les caches peuvent ou non être intégrés au processeur. Il a existé des caches séparés du processeur, connectés sur la carte mère. Un exemple était le cache du processeur Pentium 2, qui avait son propre "socket". Mais de nos jours, les caches sont incorporés au processeur, pour des raisons de performance. Les caches devant être très rapides, avec des temps d'accès proches 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 le cache, si le cache est en dehors du processeur. En intégrant les 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.

Les local store et les caches RAM-configurables

[modifier | modifier le wikicode]

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.

Les local stores sont plus économes en circuits et consomment moins d'énergie que les caches à taille équivalente. En effet, ils n'ont pas besoin de circuits compliqués pour gérer automatiquement les échanges avec la RAM, contrairement aux caches. Ils sont adressables, ce qui est assez simple à implémenter avec un décodeur et des registres. 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.

Il faut noter que certains caches peuvent être configurés pour fonctionner comme des local store. En effet, une mémoire cache est souvent fabriquée en prenant une ou plusieurs mémoires SRAM adressables et en ajoutant des circuits autour. Mais il est possible d'utiliser les mémoires SRAM adressables telles quelles, en les adressant directement. Il s'agit de la technique du 'cache RAM-configurable.

L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes. Elles incorporent un ou plusieurs processeurs multicœurs, dont le cache L1 de données est un cache RAM-configurable. Les CPU commerciaux incorporent aussi des caches de ce type, bien que cela ne soit utilisé que lors du démarrage de l'ordinateur. Au démarrage, le BIOS n'a pas immédiatement accès à la mémoire RAM principale, qui demande d'être configurée du fait de technicalités des mémoires DDR. Aussi, le BIOS utilise alors le cache du processeur comme une mémoire RAM. Les registres de configuration du CPU sont configurés de manière à ce que le cache soit utilisé comme local store. Du code s'exécute, vérifie la présence de mémoire RAM, configure le contrôleur DDR, fait quelques manipulations, puis met le cache à l'état normal.

Les principes de localité spatiale et temporelle

[modifier | modifier le wikicode]

Utiliser au mieux la hiérarchie mémoire demande placer les données accédées souvent, ou qui ont de bonnes chances d'être accédées dans le futur, dans la mémoire 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écute : 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és 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.

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 solution 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 parcours 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. Quoi qu’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 chapitre, nous allons définir ce qui fait qu'un ordinateur est plus rapide qu'un autre. En clair, nous allons étudier 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 performance du processeur

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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. Par exemple, certaines sections de code ne sont exécutées que si une condition bien spécifique est remplie, d'autres sont répétées en boucle, etc. 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 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 | modifier le wikicode]

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 :

Pour simplifier les calculs, on peut remarquer que l'inverse du CPI n'est autre que 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 :

L'équation nous dit quelque chose d'assez intuitif : plus la fréquence du processeur est élevée, plus il est puissant. 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 car l'IPC varie énormément suivant les opérations, le programme, diverses optimisations matérielles, etc.

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.

Diminuer le nombre d'instructions à exécuter dépend surtout du programmeur ou des compilateurs, et la conception du processeur n'a actuellement que peu d'impact à l'heure actuelle. Les deux autres solutions sont fortement impactées par la loi de Moore, et nous en parlerons au chapitre suivant.

La performance d'une mémoire

[modifier | modifier le wikicode]

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 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

Le temps d’accès d'une mémoire

[modifier | modifier le wikicode]

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 | modifier le wikicode]

Enfin, toutes les mémoires n'ont pas le même débit binaire. Le débit binaire d'une mémoire est la quantité de données qu'on peut lire ou écrire 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.

Le temps de balayage

[modifier | modifier le wikicode]

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. 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 toutes les cases mémoire fonctionnent bien. Il arrive que les mémoires RAM aient des pannes : certaines cases mémoire/adresses tombent en panne après quelques années d'utilisation, et deviennent inaccessibles. Lorsque cela arrive, tout se passe bien tant que les adresses défectueuses ne sont pas lues ou écrites. 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 adresses défectueuses. Il est possible de vérifier l'intégrité de la mémoire avec des logiciels spécialisés, qui vérifient chaque adresse 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 adresse par adresse, 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 adresse, avant de la lire : si la donnée lue et écrite ne sont pas la même, l'adresse est défectueuse. 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.

La performance d'un bus

[modifier | modifier le wikicode]

La performance d'un bus est quelque chose de complexe à décrire. Mais le critère principal est le débit binaire. Le débit binaire est la quantité de données que le bus peut transmettre d'un composant à un autre, par seconde. Il se mesure en octets par seconde ou en bits par seconde. Les bus haute performance sont capables de transmettre un grand nombre de données par seconde, alors que ceux de basse performance ne peuvent échanger qu'un petit nombre de données sur le bus.

Le débit binaire d'un bus est influencé par deux autres paramètres : sa largeur et sa fréquence. La fréquence du bus est assez simple à comprendre : le bus est cadencé par une horloge, qui a une certaine fréquence. À chaque cycle, il transfère plusieurs bits à la fois. Le nombre de bits transmis en même temps est appelé la largeur du bus. Par exemple, un bus d'une largeur de 16 bits peut transférer deux octets par cycle d'horloge. La largeur du bus correspond au nombre de fils utilisés pour transférer les données. Si un bus peut transférer 8 bits par cycle, cela signifie que ce bus dispose de 8 fils, un par bit, chaque fil peut transmettre un bit par cycle. Le débit binaire est le produit de la largeur du bus par sa fréquence.

Les limites de la performance des applications : le roofline model

[modifier | modifier le wikicode]

Plus haut, nous avons parlé des performances du processeur et de la mémoire de manière isolée. Dans les faits, les programmes qui s'exécutent sur un processeur utilisent les deux, et à des degrés divers. Il y a un continuum entre des programmes qui accèdent beaucoup à la mémoire et font peu de calculs, et les programmes opposé qui font beaucoup de calculs mais accèdent peu à la RAM. Un programme très gourmand en calculs profitera d'un processeur rapide, même si la mémoire RAM est lente. Et inversement, un programme qui accède beaucoup à la mémoire a besoin d'une mémoire RAM rapide, même si le processeur ne suit pas.

Dans le même genre, les personnes afficionados de jeux vidéos ont sans doute entendu parler du bottleneck CPU/GPU pour désigner les jeux vidéo dont le framerate est limité soit le CPU ou par la carte graphique. La performance est alors la responsabilité partagée du processeur et de la carte graphique, mais l'un des deux sera le facteur limitant.

Pour quantifier ce genre de compromis, Samuel Williams, Andrew Waterman, et David Patterson, ont inventé le roffline model, initialement été décrit dans cet article scientifique :

Nous allons décrire ce modèle dans ce chapitre. Il est souvent vu dans les chapitres sur les architectures parallèles dans les rares cours d'architecture des ordinateur qui en font mention, mais il s'agit bel et bien d'un modèle qui marche sur les architectures à un seul cœur/processeur.

Le modèle de base

[modifier | modifier le wikicode]

Le modèle introduit le concept d'intensité calculatoire. Il s'agit du nombre d'opérations réalisées pour un octet lu/écrit depuis la mémoire RAM. Elle varie suivant le programme considéré, tous les programmes n'ont pas la même intensité calculatoire. En clair, il s'agit du nombre d’opérations réalisé par un programme, divisé par le débit binaire mémoire. Le débit binaire utilisé est celui de la mémoire RAM, pas des caches, car la mémoire est supposée partagée.

À forte intensité calculatoire, on fait beaucoup de calculs comparé aux accès mémoires. On demande donc plus au processeur qu'à la mémoire. À basse intensité calculatoire, on accède beaucoup à la mémoire et on fait peu d'opérations. La mémoire est donc le facteur limitant. Globalement, au-delà d'une certaine intensité calculatoire, c'est le processeur qui sera limitant (et inversement, ce sera la mémoire). Il existe un point d'équilibre où la mémoire et la performance des CPU sont tous deux des facteurs limitants, le système est parfaitement équilibré.

Le roofline donne la performance totale, qui est limitée par le débit de la mémoire, par la performance maximale des CPUs parallèles exprimée en MIPS/FLOPS, et par l'intensité calculatoire. Le modèle est un simple diagramme en deux dimensions, avec l'intensité calculatoire en abscisse, et la performance en ordonnée. Plus l'intensité calculatoire augmente, plus les performances augmentent, à débit binaire égal. La mémoire est alors le facteur limitant, et on fait alors plus de calcul à débit binaire égal. Mais au-delà d'une intensité calculatoire bien précise, le débit binaire n'est plus le facteur limitant, mais c'est le processeur qui limite les performances. On a atteint un plateau dépendant des CPUs.

Roofline model

Les calculs qui permettent d'obtenir la courbe du modèle

[modifier | modifier le wikicode]

Pour obtenir la courbe, rien de plus simple. Le modèle part du principe qu'il y a une puissance de calcul maximale indépassable, exprimée en FLOPS ou en MIPS. Il s'agit de la limite maximale obtenable en ne tenant compte que du processeur, pas du débit de la mémoire. Elle correspond à la portion plate de la courbe. Notons la puissance de calcul maximale permise par le CPU .

Maintenant, la performance est aussi limitée par le débit binaire de la mémoire. Si l'on a un débit binaire de , alors la performance maximale se calcule en multipliant ce débit binaire par l'intensité calculatoire. Ce dernier est un nombre de calculs par octet lu/écrit, on multiplie par le nombre total d'octets lus/écrits : on a bien une puissance de calcul. En notant le résultat, on a :

, avec I l'intensité calculatoire et le débit binaire.

La puissance réelle dépend des deux limites. Elle ne peut pas dépasser la performance max permise par le CPU, pas plus qu'elle ne peut dépasser celle permise par le débit de la RAM. En clair, la performance maximale possible est la plus petite valeur entre les deux :

Roofline model avec les notations

Les limites du modèle

[modifier | modifier le wikicode]

Il faut préciser que le modèle donne une limite maximale pour la performance. Dans les faits, les applications ne l'atteindront pas. Elle auront une performance inférieure à la limite maximale, pour une intensité arithmétique donnée. La performance réelle sera parfois très proche, parfois très éloignée de la performance maximale.

Performance réelle de plusieurs applications dans le Roofline model.

Les raisons à cela sont multiples. La première est tout simplement que le processeur n'utilise pas son plein potentiel, sans que ce soit lié à la mémoire ou aux caches. Par exemple, il n'arrive pas à alimenter ses circuits de calculs pour des raisons diverses et variées. Le plafond est alors plus bas qu'il n'y parait et quelques optimisations logicielles permettent de faire remonter le plafond effectif.

Roofline model avec trois plafonds différents selon l'usage qui est fait du processeur.

Il est aussi possible que le programme considéré n'utilise pas bien le débit binaire de la mémoire, une partie est gâchée par des accès mémoire inutiles. Diverses optimisations logicielles ou matérielles permettent alors de se rapprocher du maximum théorique dans la portion limitée par la mémoire. Sans ces optimisations, la courbe a une pente décalée vers la droite, car le programme fait moins d'accès mémoire pour une intensité arithmétique inchangée.

Roofline model bandwidth ceilings

Notons que le débit binaire considéré dans le modèle est celui de la mémoire RAM. L'usage de mémoires caches change la donne d'une manière assez originale. Une mauvaise utilisation des caches fait que l'intensité arithmétique stagnera à un niveau maximal. En clair, cela se traduit par des barrières verticales sur le diagramme, que le programme ne pourra pas dépasser. Le programme restera à gauche, dans la partie limitée par la barrière. Et celle-ci est systématiquement dans la portion gauche de la courbe, celle limitée par la mémoire.

Roofline model locality walls

Pour résumer, le modèle peut aider les programmeurs à savoir quoi optimiser, s'ils savent faire des mesures adéquates sur un grand nombre de hardware différents. Mais il nous dit plusieurs choses importantes : un programme peut être limité soit par le CPU, soit par le débit binaire de la mémoire, soit par une mauvaise utilisation des caches. Par un programme ne se comportera de la même manière qu'un autre, les compromis seront différents du fait d'intensité arithmétiques différentes. Et suivant la machine, un même programme se comportera très différemment. Il y a donc une grande variabilité des performances d'un programme et d'une machine à l'autre.

De plus, les programmeurs doivent faire face à des compromis lorsqu'ils optimisent. Par exemple, optimiser l'intensité arithmétique en améliorant l'utilisation des mémoires caches ou en réduisant les accès mémoire a du sens, mais seulement si la performance est limitée par la mémoire. Mais une telle optimisation ne servira à rien si le facteur limitant est la performance du processeur. Dans le passé, c'était surtout la performance des mémoire et du processeur qui étaient limitante. Mais de nos jours, le problème tient surtout dans les caches et la bonne utilisation de la hiérarchie mémoire, du moins pour une majorité de programmes. Les situations sont assez variables, mais les grandes lignes du hardware actuel sont là : les processeurs sont des monstres de puissance théorique, les mémoires RAM ont un débit absolument énorme, mais on se heurte aux barrières liées aux mémoires caches.


Il va de soi que les nouveaux processeurs sont plus puissants que les anciens, pareil pour les mémoires. 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. Et cela se voit dans les données empiriques. Il est intéressant de regarder comment les mémoires et processeurs ont évolué dans le temps.

Pour les processeurs, la loi de Moore a des conséquences qui sont assez peu évidentes. Certes, on peut mettre plus de transistors dans un processeur, mais en quoi cela se traduira par de meilleures performances ? Pour comprendre l'influence qu'à eu la loi de Moore sur les processeurs modernes, regardons ce graphique, qui montre les relations entre nombre de transistors, fréquence du processeur, performance d'un seul cœur, nombre de cœurs, et consommation énergétique.

50 years of microprocessor trend data, par Karl Rupp.

Globalement, on voit que le nombre de transistors augmente de façon exponentielle : doubler tous les X années donne une courbe exponentielle, d'où l'échelle semi-logarithmique du graphique. Mais pour le reste, quelque chose s'est passé en 2005 : les courbes n'ont pas la même pente avant et après 2005. Que ce soit la fréquence, la performance d'un seul cœur, la consommation électrique, tout. Et le nombre de cœurs explose au même moment. Tout cela fait penser que toutes ces caractéristiques étaient liées entre elles et augmentaient exponentiellement, mais il y a un après 2005. Reste à expliquer pourquoi, ce qui est le sujet de ce chapitre, sans compter qu'on détaillera tout ce qui a trait à la consommation énergétique.

La miniaturisation des transistors est la cause des tendances technologiques

[modifier | modifier le wikicode]

Avant toute chose, nous devons faire quelques rappels sur les transistors MOS, sans lesquels les explications qui vont suivre seront compliquées. Un transistor MOSFET a de nombreuses caractéristiques : ses dimensions, mais aussi d'autres paramètres plus intéressants. Par exemple, il est intéressant de regarder la consommation d'énergie d'un transistor, à savoir combien de watts ils utilise pour faire ce qu'on lui demande. Pour cela, il faudra parler rapidement de certaines de ses caractéristiques comme sa capacité électrique. Rien de bien compliqué, rassurez-vous.

Les caractéristiques d'un transistor : finesse de gravure, capacité, etc

[modifier | modifier le wikicode]
Transistor CMOS - 1

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. Les dimensions d'un transistors sont au nombre de deux : la distance entre source et drain, la distance entre grille et semi-conducteur. Les deux sont regroupées sous le terme de finesse de gravure, bien que cela soit un terme impropre.

Nous avons dit plus haut qu'un transistor MOS est composé de deux (semi-)conducteurs 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. L'intérieur d'un condensateur est formé de deux couches de métal conducteur, séparées par un isolant électrique. Les charge s'accumulent dans les couche de métal quand on charge le condensateur en électricité. L'intérieur d'un transistor MOS est donc similaire à celui d'un condensateur, si ce n'est qu'une couche métallique est remplacée par un morceau de semi-conducteur. 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.

Condensateur et accumulation des charges électrique sur les plaques métalliques.

Tout condensateur possède une caractéristique importante : sa capacité électrique. Il s'agit simplement de la quantité d'électrons/charges qu'il peut contenir en fonction de la tension. Il faut savoir que la quantité de charge contenue dans un condensateur est proportionnelle à la tension, la capacité est le coefficient de proportionnalité entre les deux. Tout cela est sans doute plus clair avec une équation :

, avec Q la quantité de charges contenues dans le condensateur, U la tension, et C la capacité.
Charge/décharge d'un condensateur.

La capacité d'un transistor MOS a une influence directe sur la fréquence à laquelle il peut fonctionner. Pour changer l'état d'un transistor MOS, il faut soit charger la grille, soit la décharger. Et pour remplir le transistor, il faut fournir une charge égale à celle donnée par l'équation précédente.

Si on met ce processus en équations, on s’aperçoit qu'on se trouve avec des charges ou décharges exponentielles. Mais par simplicité, on considère que le temps de charge/décharge d'un condensateur est proportionnel à sa capacité (pour être précis, proportionnel au produit 5 RC, avec R la résistance des fils). Tout ce qu'il faut retenir est que plus la capacité est faible, plus le transistor est rapide et plus il peut fonctionner à haute fréquence.

Les lois de Dennard, ce qui se cache derrière la loi de Moore

[modifier | modifier le wikicode]

La loi de Moore est le résultat d'une tendance technologique bien précise : les dimensions d'un transistors se réduisent avec les progrès de la miniaturisation. Elles sont réduites de 30% tous les deux ans. Pour le dire autrement, elles sont multipliées par 0.7 tous les deux ans.

Évolution de la finesse de gravure au cours du temps pour les processeurs.

Les processeurs sont des composants qui ont actuellement une forme carrée, les transistors sont tous placés sur un plan et ne sont pas empilés les uns sur les autres. Ils occupent donc une certaine aire sur la surface du processeur. Si la taille des transistors est réduite de 30% tous les 2 ans, l'aire que prend un transistor sur la puce est quand à elle divisée par 30% * 30% 50%. En conséquence, on peut mettre deux fois plus de transistors sur la même puce électronique : on retrouve la loi de Moore.

Cela a aussi des conséquences sur la tension d'alimentation nécessaire pour faire fonctionner le transistor. Sans rentrer dans les détails, la tension est elle aussi proportionnelle aux dimensions du transistor. La raison technique, que vous comprendrez si vous avez eu des cours d'électricité avancés durant votre scolarité, est que le champ électrique ne change pas dans le transistor, et que la tension est le produit du champ électrique par la distance. Là encore, la tension d'alimentation est réduite de 30% tous les deux ans.

Condensateur plan

La miniaturisation a une influence directe sur la capacité électrique du transistor. 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 et de la distance entre la grille et le semi-conducteur. On peut calculer cette capacité comme suit, avec A 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).

Le coefficient e (la permittivité électrique) reste le même d'une génération de processeur à l'autre, même si les fabricants ont réussi à le faire baisser un peu grâce à matériaux particuliers. Mais laissons cela de côté : dans les faits, seuls les coefficients S et d vont nous intéresser. Si la finesse de gravure diminue de 30%, la distance d diminue du même ordre, la surface A diminue du carré de 30%, c’est-à-dire qu'elle sera approximativement divisée par 2. La capacité totale sera donc divisée par 30% tous les deux ans.

Réduire la capacité des transistors a un impact indirect très fort sur la fréquence à laquelle on peut faire fonctionner le transistor. En effet, la période de l'horloge correspond grosso modo au temps qu'il faut pour remplir ou vider l'armature de la grille, et on sait que le temps de charge/décharge d'un condensateur est approximativement proportionnel à sa capacité. La capacité et le temps de charge/décharge est donc réduit de 30% tous les deux ans. La fréquence étant inversement proportionnelle au temps de remplissage du condensateur, elle est donc augmentée de 1/0.7 = 40%.

Tout ce qu'on vient de dire plus a été formalisé sous 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. Une réduction de la finesse de gravure impacte plusieurs paramètres : le nombre de transistors d'une puce électronique, sa tension d'alimentation, sa fréquence, et quelques autres paramètres qu'on détaillera plus bas comme la capacité d'un transistor ou ses courants de fuite.

Paramètre Coefficient multiplicateur (tous les deux ans) Variation en pourcentage
Finesse de gravure 0.7 - 30%
Aire occupée par un transistor 0.5 - 50%
Nombre de transistors par unité de surface 2 + 100%
Tension d'alimentation 0.7 - 30%
Capacité d'un transistor 0.7 - 30%
Fréquence (1/0.7) = 1.4 + 40%

La fin des lois de Dennard

[modifier | modifier le wikicode]

Les lois de Dennard ont cessé de s'appliquer aux alentours de 2005/2006. Les dimensions d'un transistors sont toujours réduites de 30% tous les deux ans, la loi de Moore est encore valable, mais pour ce qui est de la fréquence et de la tension d'alimentation, c'est autre chose. Les raisons à cela sont multiples, et il faut revenir au fonctionnement d'un transistor MOS pour comprendre pourquoi.

Un MOSFET est composé d'une grille métallique, d'une couche de semi-conducteur, et d'un isolant entre les deux. L'isolant empêche la fuite des charges de la grille vers le semi-conducteur. 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 courants de fuite dépendent d'une tension appelée tension de seuil. Il s'agit de la tension minimale pour avoir un courant passant entre la source et le drain. Sous cette tension, le transistor se comporte comme un interrupteur fermé, peut importe ce qu'on met sur la grille. Au-dessus de cette tension, le courant se met à passer entre source et drain, il se comporte comme un interrupteur ouvert. Le courant est d'autant plus fort que la tension sur la grille dépasse la tension de seuil. 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). Et l'incapacité à réduire cette tension a eu des conséquences fâcheuses.

Nous verrons plus bas que la consommation d'énergie d'un processeur dépend de sa fréquence et de sa tension. Les lois de Dennard nous disent que Si la seconde baisse, on peut augmenter la première sans changer drastiquement la consommation énergétique. Mais si la tension d'alimentation stagne, alors la fréquence doit faire de même. Vu que 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.

L'explication est convaincante, mais nous détaillerons celle-ci plus bas, avec les vraies équations qui donnent la consommation d'énergie d'un processeur. Nous en profiterons aussi pour voir quelles sont les technologies utilisées pour réduire cette consommation d'énergie, les deux étant intrinsèquement liés.

La consommation d'un circuit électronique CMOS

[modifier | modifier le wikicode]

Tout ordinateur consomme une certaine quantité de courant pour fonctionner, et donc une certaine quantité d'énergie. On peut dire la même chose de tout circuit électronique, que ce soit une mémoire, un processeur, ou quoique ce soit d'autre d'électronique. 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.

Pour donner un exemple, 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, les 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.

La consommation d'un circuit électronique CMOS

[modifier | modifier le wikicode]

Pour comprendre pourquoi, on doit parler de ce qui fait qu'un circuit électronique consomme de l'énergie. Une partie de la consommation d'énergie d'un circuit vient du fait qu'il consomme de l'énergie en changeant d'état. On appelle cette perte la consommation dynamique. A l’opposé, la consommation statique vient du fait que les circuits ne sont pas des dispositifs parfaits et qu'ils laissent fuiter un peu de courant.

Pour commencer, rappelons qu'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. Charger ou décharger un condensateur demande de l'énergie. Les lois de la physique nous disent que la quantité d'énergie que peut stocker un condensateur est égale au produit ci-dessous, avec C la capacité du condensateur et U la tension d'alimentation. Il s'agit de l'énergie qu'il faut fournir quand on charge ou décharger un condensateur/MOSFET.

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 par seconde d'un transistor est proportionnel à la fréquence. L'énergie dissipée en une seconde (la puissance P) par un transistor est approximée par l'équation suivante :

On peut alors multiplier par le nombre de transistors d'une puce électronique, ce qui donne :

Cette équation nous donne la consommation dynamique, à savoir celle liée à l'activité du processeur/circuit. De plus, l'équation précédente permet de comprendre comment la consommation dynamique a évolué grâce à la loi de Moore. Pour cela, regardons comment chaque variable a évolué et faisons les comptes :

  • le nombre de transistors fait *2 tous les deux ans ;
  • la capacité est réduite de 30% tous les deux ans  ;
  • la tension d'alimentation est réduite de 30% tous les deux ans ;
  • la fréquence augmente de 40% tous les deux ans.

L'augmentation du nombre de transistors est parfaitement compensé par la baisse de la tension d'alimentation : multiplication par deux d'un côté, divisé par 2 (30% au carré) de l'autre. Idem avec la capacité et la fréquence : la capacité est multipliée par 0.7 tous les deux ans, la fréquence est multipliée par 1.4 de l'autre, et . En clair, la consommation dynamique d'un processeur ne change pas dans le temps, du moins tant que les lois de Dennard sont valides. Ce qui n'est plus le cas depuis 2005.

Tout ce qui est dit plus haut part du principe que le transistor MOS est un dispositif parfait. Dans les faits, ce n'est pas le cas. En effet, la couche d'isolant entre la grille et le semi-conducteur est très petite. 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. La consommation d'énergie qui résulte de ce courant de fuite est la consommation statique. Elle s'appelle statique car elle a lieu même si les transistors ne changent pas d'état.

La diminution de la finesse de gravure a tendance à augmenter les courants de fuite. Elle diminue la consommation dynamique, mais augmente la consommation statique. Vu que les processeurs anciens 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.

Quantifier la consommation statique est assez compliqué, et les équations deviennent généralement très complexes. Mais une simplification nous dit que les courants de fuite dépendent de la tension de seuil. Une équation très simplifiée est la suivante :

, avec la tension de seuil, et K une constante.

La consommation statique est le produit des courants de fuite et de la tension d'alimentation, avec d'autres facteurs de proportionnalités qui ne nous intéressent pas ici.

, avec U la tension d'alimentation.

La loi de Kommey

[modifier | modifier le wikicode]

Avant 2005, la réduction de la finesse de gravure permettait de diminuer la consommation d'énergie, tout en augmentant la puissance de calcul. Mais depuis, la consommation statique a fini par rattraper la consommation dynamique. Et cela a une conséquence assez importante. 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. 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.
Loi de Kommey

Il est aussi intéressant d'étudier la performance par watts, à savoir le nombre de Millions d'instructions par secondes pour chaque watt/kilowatt dépensé pour faire le calcul. Avant l'année 2005, 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 2005, 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.

Le dark silicon et le mur du TDP

[modifier | modifier le wikicode]

Malgré les nombreuses améliorations de la performance par watt dans le temps, les processeurs actuels chauffent beaucoup et sont contraints par les limites thermiques. Et ces limites thermiques se marient assez mal avec le grand nombre de transistors présents sur les processeurs modernes. Dans un futur proche, il est possible que les contraintes thermiques limitent le nombre de transistors actifs à un instant donné. Pour le dire autrement, il serait impossible d'utiliser plus de 50% des transistors d'un CPU en même temps, ou encore seulement 30%, etc. Ce genre de scénarios ont reçu le nom d'"utilization wall" dans la communauté académique.

Il y aurait donc un certain pourcentage du processeur qui ne pourrait pas être activée en raison des performances thermiques, portion qui porte le nom de dark silicon. Mais le dark silicon n'est pas du circuit inutile. Il faut bien comprendre que ce dark silicon n'est pas une portion précise de la puce. Par exemple, imaginons que 50% d'un processeur soit du dark silicon : cela ne veut pas dire que 50% du CPU ne sert à rien, mais que les deux moitié du processeur se passent le flambeau régulièrement, ils sont utilisés à tour de rôle.

En soi, le fait que tous les transistors ne soient pas actifs en même temps n'est pas un problème. Les processeurs modernes n'utilisent pas tous leurs circuits en même temps, et certains restent en pause tant qu'on ne les utilise pas. Par exemple, sur un processeur multicœurs, il arrive que certains cœurs ne soient pas utilisés, et restent en veille, voire soient totalement éteints. Par exemple, sur un processeur octo-coeurs, si seuls 4 cœurs sur les 8 sont utilisés, alors 50% du processeur est techniquement en veille.

L'existence du dark silicon implique cependant qu'il faut construire les processeurs en tenant compte de sa présence, du fait que les contrainte thermiques empêchent d'utiliser une portion significative des transistors à un instant t. Pour cela, l'idée en vogue actuellement est celle des architectures hétérogènes, qui regroupent des processeurs très différents les uns des autres sur la même puce, avec chacun leur spécialisation.

Le premier cas existe déjà à l'heure actuelle. Il s'agit de processeurs qui regroupent deux types de cœurs : des cœurs optimisés pour la performance, et des cœurs optimisés pour la performance. Un exemple est celui des processeurs Intel de 12ème génération et plus, qui mélangent des P-core et des E-core, les premiers étant des coeurs très performants mais gourmands en énergie, les autres étant économes en énergie et moins performants. Les noms complets des coeurs trahissent le tout : Efficiency core et Performance core. L'utilité des E-core est d'exécuter des programmes peu gourmands, généralement des tâches d'arrière-plan. Les processeurs ARM de type BIG.little faisaient la même chose, mais avec un cœur de chaque type.

Le second cas est celui des processeurs regroupant un ou plusieurs cœurs normaux, généralistes, complétés par plusieurs accélérateurs spécialisés dans des tâches précises. Par exemple, on peut imaginer que le processeur incorpore un circuit spécialisé dans les calculs cryptographiques, un circuit spécialisé dans le traitement d'image, un autre dans le traitement de signal, un autre pour accélérer certains calculs liés à l'IA, etc. De tels circuits permettent des gains de performance dans des tâches très précises, qui ne se mélangent pas. Par exemple, si on lance un vidéo, le circuit de traitement d'image/vidéo sera activé, mais l'accélérateur cryptographique et l'accélération d'IA seront désactivés.

En soi, le fait d'adapter l'architecture des ordinateurs pour répondre à des contraintes thermiques n'est pas nouvelle. Nous verrons plus bas que l'apparition des processeurs multicœurs dans les années 2000 est une réponse à des contraintes technologiques assez strictes concernant la température, par exemple. La fin de la loi de Dennard a grandement réduit l'amélioration de performance par watt des processeurs à un seul cœur, rendant les processeurs multicœurs plus intéressants. Mais expliquer pourquoi demande d'expliquer pas mal de choses sur la performance d'un processeur simple cœur et comment celle-ci évolue avec la loi de Moore.

L'évolution de la performance des processeurs

[modifier | modifier le wikicode]

Les processeurs ont gagné en performance avec le temps. Les raisons à cela sont doubles, mais liées aux lois de Dennard. La première raison est la hausse de la fréquence. C'était une source importante avant 2005, avant que les lois de Dennard cessent de fonctionner. L'autre raison, est la loi de Moore. Mettre plus de transistors permet de nombreuses optimisations importantes, comme utiliser des circuits plus rapides, mais plus gourmands en circuits. Voyons les deux raisons l'une après l'autre.

La fréquence des processeurs a augmenté

[modifier | modifier le wikicode]

Les processeurs et mémoires ont vu leur fréquence augmenter au fil 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 ! L'augmentation a été exponentielle, mais plus faible que le nombre de transistors. Les nouveaux processeurs ont augmenté en fréquence, grâce à l'amélioration de la finesse de gravure. On est maintenant à environ 3 GHz à mi-2020.

Plus haut, on a dit que la fréquence augmentait de 40% tous les deux ans, tant que la loi de Dennard restait valide, avant 2005. 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). Pour mettre en avant l'influence de la conception du processeur, il est intéressant de calculer une fréquence relative, à savoir la fréquence à finesse de gravure égale. Elle est difficile à calculer, mais on peut l'utiliser pour comparer des processeurs entre eux, à condition de prendre la fréquence d'un processeur de référence.

Quelques observations montrent qu'elle a subi pas mal de variation d'un processeur à l'autre. La raison est que diverses techniques de conception permettent de gagner en fréquence facilement, la plus importante étant le pipeline. Augmenter la fréquence relative était une approche qui a eu son heure de gloire, mais qui a perdu de sa superbe. Avant les années 2000, augmenter la fréquence permettait de gagner en performance assez facilement. De plus, la fréquence était un argument marketing assez saillant, et l'augmenter faisait bien sur le papier. Aussi, les fréquences ont progressivement augmenté, et ont continué dans ce sens, jusqu’à atteindre une limite haute avec le Pentium 4 d'Intel.

Fréquences relatives processeurs Intel pré-Pentium 4

Le Pentium 4 était une véritable bête en termes de fréquence pour l'époque : 1,5 GHz, contre à peine 1Ghz pour les autres processeurs de l'époque. La fréquence avait été doublée par rapport au Pentium 3 et ses 733 MHz. Et cette fréquence était juste la fréquence de base du processeur, certains circuits allaient plus vite, d'autres moins vite. Par exemple, l'unité de calcul intégrée dans le processeur allait deux fois plus vite, avec une fréquence de 3 GHz ! Et à l'inverse, certaines portions du processeur allaient au quart de cette fréquence, à 750 MHz. Bref, le Pentium 4 gérait plusieurs fréquences différentes : certaines portions importantes pour la performance allaient très vite, d'autres allaient à une vitesse intermédiaire pour faciliter leur conception, d'autres allaient encore moins vite, car elles n'avaient pas besoin d'être très rapides.

Le processeur était spécialement conçu pour fonctionner à très haute fréquence, grâce à diverses techniques qu'on abordera dans les chapitres suivants (pipeline très long, autres). Mais tout cela avait un cout : le processeur chauffait beaucoup ! Et c'était un défaut majeur. De plus, les techniques utilisées pour faire fonctionner le Pentium 4 à haute fréquence (le superpipeline) avaient beaucoup de défauts. Défauts compensés partiellement par des techniques innovantes pour l'époque, comme son replay system qu'on abordera dans un chapitre à part, mais sans grand succès. Les versions ultérieures du Pentium 4 avaient une fréquence plus base, mais avec un processeur complétement repensé, avec un résultat tout aussi performant. Ces versions chauffaient quand même beaucoup et les contraintes thermiques étaient un vrai problème.

En conséquence, la course à la fréquence s'est arrêtée avec ce processeur pour Intel, et l'industrie a suivi. La raison principale est 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. Et cette contrainte s'est manifestée en 2005, après la sortie du processeur Pentium 4. Le point d'inflexion en 2005, à partir duquel la fréquence a cessée d'augmenter drastiquement, s'explique en grande partie par cette contrainte. 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 doivent limiter la fréquence de leurs processeurs. Une autre raison est que la fréquence dépend des transistors, mais 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.

La performance à fréquence égale a augmenté : la loi de Pollack

[modifier | modifier le wikicode]

Si la miniaturisation permet d'augmenter la fréquence, elle permet aussi d'améliorer la performance à fréquence égale. Rappelons que la performance à fréquence égale se mesure avec deux critères équivalents : l'IPC et le CPI. Le CPI est le nombre de cycles moyen pour exécuter une instruction. L'IPC est son inverse, à savoir le nombre d'instruction exécutable en un seul cycle. Les deu sont l'inverse l'un de l'autre. 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.

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. 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. En clair, la performance d'un processeur augmente de 40% grâce à la loi de Pollack, et de 40% par l'augmentation de fréquence. On a donc une augmentation de 80% tous les deux ans, donc une multiplication par 1,8 tous les deux ans, soit moins que la hausse de transistors.

L'augmentation du nombre de cœurs

[modifier | modifier le wikicode]

Après 2005, l'augmentation de la fréquence a stagné et l'augmentation des performances semblait limitée par la seule loi de Pollack. Mais l'industrie a trouvé un moyen de contourner la loi de Pollack. En effet, cette dernière ne vaut que pour un seul processeur. Mais en utilisant plusieurs processeurs, la performance est, en théorie, la somme des performances individuelles de chacun d'entre eux. Et c'est la réaction qu'à eux l'industrie.

C'est pour cela que depuis les années 2000, le nombre de cœurs a augmenté assez rapidement. 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%.

Comparaison entre fréquence et nombre de coeurs des processeurs, depuis

L'évolution de la performance des mémoires

[modifier | modifier le wikicode]

Après avoir vu le processeur, voyons comment la loi de Moore a impacté l'évolution des mémoires. Beaucoup de cours d'architecture des ordinateurs se contentent de voir l'impact de la loi de Moore sur le processeur, mais mettent de côté les mémoires. Et il y a une bonne raison à cela. Le fait est que les ordinateurs modernes ont une hiérarchie mémoire très complexe, avec beaucoup de mémoires différentes. Et les technologies utilisées pour ces mémoires sont très diverses, elles n'utilisent pas toutes des transistors MOS, la loi de Moore ne s'applique pas à toutes les mémoires.

Déjà, évacuons le cas des mémoires magnétiques (disques durs, disquettes) et des mémoires optiques (CD/DVD), qui ne sont pas fabriquées avec des transistors MOS et ne suivent donc pas la loi de Moore. Et de plus, elles sont en voie de disparition, elles ne sont plus vraiment utilisées de nos jours. Il ne reste que les mémoires à semi-conducteurs qui utilisent des transistors MOS, mais pas que. Seules ces dernières sont concernées par la loi de Moore, mais certaines plus que d'autres. Mais toutes les mémoires ont vu leur prix baisser en même temps que leur capacité a augmenté dans le temps, que ce soit à cause de la loi de Moore pour les mémoires à semi-conducteurs, l'amélioration exponentielle des technologies de stockage magnétique pour les disques durs.

Historical-cost-of-computer-memory-and-storage OWID

L'impact de la loi de Moore dépend de la mémoire considérée et de sa place dans la hiérarchie mémoire. Les mémoires intégrées au processeur, comme le cache ou les registres, sont des mémoires SRAM/ROM fabriquées intégralement avec des transistors MOS, et donc soumises à la loi de Moore. Leurs performances et leur capacité suivent l'évolution du processeur qui les intègre, leur fréquence augmente au même rythme que celle du processeur, etc. Aussi on peut considérer qu'on en a déjà parlé plus haut. Reste à voir les autres niveaux de la hiérarchie mémoire, à savoir la mémoire RAM principale, la mémoire ROM et les mémoires de masse. Dans les grandes lignes, on peut distinguer deux technologies principales : les mémoires DRAM et les mémoires FLASH.

Les mémoires FLASH ont suivi la loi de Moore

[modifier | modifier le wikicode]

Les mémoires FLASH sont utilisées dans les mémoires de masse, comme les clés USB, les disques durs de type SSD, les cartes mémoires, et autres. Le passage à la mémoire FLASH a fait qu'elles sont plus rapides que les anciennes mémoires magnétiques, pour une capacité légèrement inférieure. Les mémoires FLASH sont aussi utilisées comme mémoire ROM principale ! Par exemple, les PC actuels utilisent de la mémoire FLASH pour stocker le firmware/BIOS/UEFI. De même, les systèmes embarqués qui ont besoin d'un firmware rapide utilisent généralement de la mémoire FLASH, pas de la mémoire ROM proprement dite (on verra dans quelques chapitres que la mémoire FLASH est un sous-type de mémoire ROM, mais laissons cela à plus tard).

Elles sont basées sur des transistors MOS modifiés, appelés transistors à grille flottante. Un transistor à grille flottante peut être vu comme une sorte de mélange entre transistor et condensateur, à savoir qu'il dispose d'une grille améliorée, dont le caractère de condensateur est utilisé pour mémoriser une tension, donc un bit. Un transistor à grille flottante est utilisé pour mémoriser un bit sur les mémoires dites SLC, deux bits sur les mémoires dites MLC, trois bits sur les mémoires TLC, quatre sur les mémoires QLC. Non, ce n'est pas une erreur, c'est quelque chose de permis grâce aux technologies de fabrication d'une mémoire FLASH, nous détaillerons cela dans le chapitre sur les mémoires FLASH.

Vu que les mémoires FLASH sont basées sur des transistors MOS modifiés, vous ne serez pas trop étonnés d’apprendre que la loi de Moore s'applique à la mémoire FLASH. La taille des transistors à grille flottante suit la loi de Moore : elle diminue de 30% tous les deux ans.

Taille d'une cellule de mémoire FLASH (de type NAND).

La conséquence est que l'aire occupée par un transistor à grille flottante est divisée par deux tous les deux ans. Le résultat est que la capacité des mémoires FLASH augmente de 50 à 60% par an, ce qui fait un doublement de leur capacité tous les deux ans.

Aire d'une cellule de mémoire FLASH (de type NAND).

Les mémoires RAM ne sont pas concernées par la loi de Moore

[modifier | modifier le wikicode]
Circuit qui mémorise un bit dans une mémoire DRAM moderne.

Les mémoires DRAM sont utilisées pour la mémoire principale de l'ordinateur, la fameuse mémoire RAM. A l'intérieur des mémoires DRAM actuelles, chaque bit est mémorisé en utilisant un transistor MOS et un condensateur (un réservoir à électron). Leur capacité et leur performance dépend aussi bien de la miniaturisation du transistor que de celle du condensateur. Elles sont donc partiellement concernées par la loi de Moore.

Pour ce qui est de la capacité, les DRAM suivent la loi de Moore d'une manière approximative. La raison est que gagner en capacité demande de réduire la taille des cellules mémoire, donc du transistors et du condensateur à l'intérieur. La miniaturisation des transistors suit la loi de Moore, mais la réduction de la taille du condensateur ne la suit pas. Dans le passé, la capacité des DRAM augmentait légèrement plus vite que la loi de Moore, avec un quadruplement tous les trois ans (4 ans pour la loi de Moore), mais tout a considérablement ralentit avec le temps.

É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.

Niveau performances, la loi de Moore ne s'applique tout simplement pas. La raison à cela est que la performance des DRAM est dominée par la performance du condensateur, pas par celle du transistor. Miniaturiser des transistors permet de les rendre plus rapides, mais le condensateur ne suit pas vraiment. Aussi, les performances des mémoires DRAM stagnent.

Rappelons que la performance d'une mémoire RAM/ROM dépend de deux paramètres : son débit binaire, et son temps d'accès. Il est intéressant de comparer comment les deux ont évolué. Pour les mémoires RAM, le débit binaire a augmenté rapidement, alors que le temps d'accès a baissé doucement. Les estimations varient d'une étude à l'autre, mais disent que le temps d'accès des mémoires se réduit d'environ 10% par an, alors que le débit binaire a lui augmenté d'environ 30 à 60% par an. Une règle approximative est que le débit binaire a grandi d'au moins le carré de l'amélioration du temps d'accès.

Pour comprendre pourquoi temps d'accès et débit binaire n'ont pas évolué simultanément, il faut regarder du côté de la fréquence de la mémoire RAM. Les mémoires modernes sont cadencées avec un signal d'horloge, ce qui fait qu'il faut tenir compte de la fréquence de la mémoire. Le débit et les temps d'accès dépendent fortement de la fréquence de la mémoire. Plus la fréquence est élevée, plus les temps d'accès sont faibles, plus le débit est important.

Le calcul du débit binaire d'une mémoire est simplement le produit entre fréquence et largeur du bus mémoire. Il se trouve que la largeur du bus de données n'a pas beaucoup augmenté avec le temps. Les premières barrettes de mémoire datée des années 80, les barrettes SIMM, avaient un bus de données de 8 bits pour la version 30 broches, 32 bits pour la version 72 broches. Le bus mémoire était déjà très important. Dans les années 2000, la démocratisation des barrettes mémoires DIMM a permis au bus de données d'atteindre 64 bits, valeur à laquelle il est resté actuellement. Il est difficile d'augmenter la largeur du bus, ca cela demanderait d'ajouter des broches sur des barrettes et des connecteurs déjà bien chargées. L'augmentation du débit binaire ne peut venir que de l'augmentation de la fréquence.

Les premières mémoires utilisées dans les PCs étaient asynchrones, à savoir qu'elles n'avaient pas de fréquence ! Elles se passaient de fréquence d'horloge, et le processeur se débrouillait avec. Il s'agissait des premières mémoires DRAM d'Intel, les mémoires EDO, Fast-page RAM et autres, que nous verrons dans quelques chapitres. Elles étaient utilisées entre les années 70 et 90, où elles étaient le type de mémoire dominant. Elles étaient assez rapides pour le processeur, mémoire et processeur avaient des performances comparables.

Dans les années 1990, la SDRAM est apparue. La terme SDRAM regroupe les premières mémoires RAM synchrones, cadencées par un signal d'horloge. La fréquence des SDRAM était de 66, 100, 133 et 150 MHz. Les RAM à 66 MHz sont apparues en premier, suivies par les SDRAM à 100MHz et puis par les 133 MHz, celles de 150 MHz étaient plus rares. Lors de cette période, la relation entre fréquence, temps d'accès et débit binaire était assez claire. Le temps d'accès est proportionnel à la fréquence, à peu de choses près. Le temps d'accès est de quelques cycles d'horloge, bien qu'il dépende des barrettes de mémoire utilisées. Le débit est le produit entre fréquence et largeur du bus. Donc plus la fréquence est grande, meilleures sont les performances globales. Et la fréquence de la mémoire et celle du bus mémoire étaient identiques.

Depuis les années 2000, les mémoires RAM utilisent des techniques de Double data rate ou de Quad data rate qui permettent d'atteindre de hautes fréquences en trichant. La triche vient du fait que la fréquence de la mémoire n'est plus égale à la fréquence du bus mémoire depuis les années 2000. Nous verrons cela en détail dans le chapitre sur le bus mémoire. Pour le moment, nous allons nous contenter de dire que l'idée derrière cette différence de fréquence est d'augmenter le débit binaire des mémoires, mais sans changer leur fréquence interne.

Année Type de mémoire Fréquence de la mémoire (haut de gamme) Fréquence du bus Coefficient multiplicateur entre les deux fréquences
1998 DDR 1 100 - 200 MHz 200 - 400 MHz 2
2003 DDR 2 100 - 266 MHz 400 - 1066 MHz 4
2007 DDR 3 100 - 266 MHz 800 - 2133 MHz 8
2014 DDR 4 200 - 400 MHz 1600 - 3200 MHz 8
2020 DDR 5 200 - 450 MHz 3200 - 7200 MHz 8 à 16

Le débit binaire est proportionnel à la fréquence du bus mémoire, alors que les temps d'accès sont proportionnel à la fréquence de la mémoire. La fréquence de la mémoire n'a pas beaucoup augmentée et reste très faible, les temps d'accès ont donc fait de même. Par contre, le débit binaire est lui très élevé, car dépendant de la fréquence du bus mémoire, qui a beaucoup augmenté. Au final, les mémoires modernes ont donc un gros débit, mais un temps de latence très élevé.

La comparaison des performances des CPU et mémoires RAM : le memory wall

[modifier | modifier le wikicode]

La performance du processeur et de la mémoire doivent idéalement être comparables. Rien ne sert d'avoir un processeur puisant si la mémoire ne suit pas. A quoi bon avoir un processeur ultra-puissant s'il passe 80% de son temps à attendre des données en provenance de la mémoire ? Si la performance des mémoires RAM stagne, alors que les processeurs gagnent en performance de manière exponentielle, on fait face à un problème.

Pour nous rendre compte du problème, il faut comparer la performance du processeur avec celle de la mémoire. Et c'est loin d'être facile, car les indicateurs de performance pour le processeur et la mémoire sont fondamentalement différents. La performance d'une mémoire dépend de son débit binaire et de son temps d'accès, la performance du processeur dépend de son CPI et de sa fréquence. Mais on peut comparer la vitesse à laquelle ces indicateurs grandissent. Pour donner un chiffre assez parlant, quelques estimations estiment que le temps d'accès de la mémoire, exprimé en cycles d'horloge du processeur, double tous les 2,6 ans.

L'évolution dans le temps du ratio des fréquences processeur/mémoire

[modifier | modifier le wikicode]

Une autre possibilité est comparer la fréquence des processeurs et des mémoires, pour voir si la fréquence de la mémoire a suivi celle du processeur. On s'attendrait à une augmentation de 40% de la fréquence des mémoires tous les deux ans, comme c'est le cas pour les processeurs. Mais la fréquence des mémoires n'a pas grandit au même rythme et a été beaucoup plus faible que pour les processeurs. Faisons un historique rapide.

Lors de l'époque des mémoires asynchrones, mémoire et processeur avaient des performances comparables. Les accès mémoire se faisaient globalement en un cycle d'horloge, éventuellement deux ou trois cycles, rarement plus. Bien qu'asynchrone, on peut considérer qu'elles allaient à la même fréquence que le processeur, mais cela ne servait à rien de parler de fréquence de la mémoire ou de fréquence du bus mémoire.

Pour la période des mémoires SDRAM, le tableau ci-dessous fait une comparaison des fréquences processeur-mémoire. La comparaison avec les processeurs était assez simple, car la fréquence du bus mémoire et la fréquence de la mémoire sont identiques. On voit que la fréquence de la mémoire est déjà loin derrière la fréquence du processeur, elle est 3 à 5 fois plus faible.

Année Fréquence du processeur (haut de gamme) Fréquence de la mémoire (haut de gamme)
1993 Intel Pentium : 60 à 300 MHz 66 Mhz
1996-1997
  • Intel Pentium 2 (1996) : 233 MHz à 450 MHz
  • AMD K5 (1996) : 75 MHz à 133 MHz
  • AMD K6 (1997) : 166 MHz à 300 MHz
100 MHz
1999
  • Intel Pentium 3 : 450 MHz à 1,4 GHz
  • AMD Athlon : 500 MHz à 1400 MHz
133 MHz

Avec l'invention des mémoires DDR, la comparaison est rendue plus compliquée par la dissociation entre fréquence du bus et fréquence de la mémoire. La comparaison est la suivante :

Année Type de mémoire Fréquence de la mémoire (haut de gamme) Fréquence du bus Fréquence du processeur (haut de gamme)
1998 DDR 1 100 - 200 MHz 200 - 400 MHz 200 - 1000 MHz
2003 DDR 2 100 - 266 MHz 400 - 1066 MHz 700 - 1500 MHz
2007 DDR 3 100 - 266 MHz 800 - 2133 MHz 1500 - 3000 MHz
2014 DDR 4 200 - 400 MHz 1600 - 3200 MHz 1600 - 4200 MHz
2020 DDR 5 200 - 450 MHz 3200 - 7200 MHz 1700 - 4500 MHz

Le constat est assez clair : le processeur est plus rapide que la mémoire, et l'écart se creuse de plus en plus avec le temps. Il s'agit d'un problème assez important, qui dicte l'organisation des ordinateurs modernes. Les mémoires sont actuellement très lentes comparé au processeur. On parle souvent de memory wall, pour décrire ce problème, nous utiliserons le terme mur de la mémoire. Pour cela, diverses solutions existent. Et la plus importante d'entre elle est l'usage d'une hiérarchie mémoire.

Les solutions contre le mur de la mémoire : hiérarchie mémoire et RAM computationnelle

[modifier | modifier le wikicode]

Le mur de la mémoire est un problème avec lequel les architectures modernes doivent composer. Le mur de la mémoire a deux origines. La première est que processeur et mémoire sont strictement séparés et que tout traitement doit lire des opérandes en mémoire, pour ensuite écrire des résultats en mémoire RAM. La seconde est que les transferts entre processeurs et mémoire sont assez lents, ce qui fait que l'idéal est de réduire ces transferts le plus possible.

La solution la plus souvent retenue est l'usage de mémoires caches intégrées au processeur, pour réduire le trafic entre DRAM et CPU. Les mémoires caches ne sont pas des DRAM, ce qui permet de contourner le problème du mur de la mémoire, causé par la technologie de fabrication des DRAM. Les caches sont des mémoires à semi-conducteur fabriquées avec des transistors CMOS, ce qui fait que leurs performances augmentent au même rythme que le processeur. Elles sont intégrées dans le processeur, même s'il a existé des mémoires caches connectées sur la carte mère.

Mais une autre solution consiste à faire l'inverse, à savoir ajouter des capacités de calcul dans la mémoire RAM. L'idée est de faire les calculs dans la mémoire directement : pas besoin de transférer les opérandes du calcul de la mémoire vers le CPU, ni de transférer les résultats du CPU vers la RAM. L'idée est alors de déléguer certains calculs à la DRAM, voire carrément de fusionner le CPU et la DRAM ! On parle alors de in-memory processing, que nous traduirons par le terme quelque peu bâtard de RAM computationnelle.

Cependant, l'implémentation d'une RAM computationnelle pose quelques problèmes d'ordre pratique. Le premier est au niveau de la technologie utilisée pour les transistors. Comme on vient de le voir, les technologies utilisées pour fabriquer la mémoire sont très différentes de celles utilisées pour fabriquer des circuits logiques, les circuits d'un processeur. Les processeurs utilisent des transistors CMOS normaux, les mémoires FLASH des transistors MOS à grille flottante, les DRAM utilisent un mélange de transistors MOS et de condensateurs. Et au niveau des procédés de fabrication, de gravure des puce, de photolithographie, de la technique des semi-conducteurs, les trois procédés sont totalement différents. Aussi, fusionner DRAM et CPU pose des problèmes de fabrication assez complexes.

Notons que ce problème n'a pas lieu avec la mémoire utilisée pour les registres et les caches, car elle est fabriquée avec des transistors. Nous avons vu il y a quelques chapitres comment créer des registres à partir de transistors et de portes logiques, ce qui utilise le même procédé technologique que pour les CPU. Les mémoires caches utilisent des cellules de mémoire SRAM fabriquées uniquement avec des transistors CMOS, comme nous le verrons dans quelques chapitres. Registres et SRAM sont donc fabriqués avec le même procédé technologique que les processeurs, ce qui fait que l'intégration de registres/caches dans le processeur est assez simple. Ce n'est pas le cas pour la fusion DRAM/CPU.

La majorité des architectures à RAM computationnelle ont été des échecs commerciaux. La raison est qu'il s'agit d'architectures un peu particulières, qui sont formellement des architectures dites à parallélisme de données, assez difficiles à exploiter. La tentative la plus connue était le projet IRAM de l'université de Berkeley. L'idée était de fusionner un processeur et une mémoire sur la même puce, les deux étant intégrés dans le même circuit. Démarré en 1996, il a été abandonnée en 2004.


Le chapitre précédent nous a appris que la consommation d'un processeur dépend de sa fréquence et de sa tension d'alimentation, la tension d'alimentation ayant un effet largement supérieur à celui de la fréquence. 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. La plupart des processeurs calibrent leur tension et leur fréquence de manière à avoir le meilleur compromis possible entre performance et consommation électrique.

Pour réduire la consommation énergétique, les ingénieurs ont inventé diverses techniques assez intéressantes. Globalement, elles se classent en deux catégories, suivant la méthode utilisée pour réduire la consommation d'énergie. Les premières consistent à tout simplement mettre en veille les circuits inutilisés du processeur, ou du moins à faire en sorte qu'ils ne consomment pas d'énergie. La seconde technique, plus complexe, adapte la tension et la fréquence en fonction des besoins.

Éviter la consommation des circuits inutilisés

[modifier | modifier le wikicode]

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.

Un exemple : le core parking des processeurs multicoeurs

[modifier | modifier le wikicode]

Un exemple assez intuitif se manifeste sur les processeurs multicoeurs, dont nous avons parlé il y a quelques chapitres. Pour rappel, et pour simplifier, ils regroupent plusieurs processeurs sur une même puce, les processeurs sont appelés des cœurs et chjacun peut exécuter un logiciel/programme. L'explication fonctionne, modulo quelques simplifications de design qui partagent certains circuits entre cœurs.

L'idée est de désactiver les cœurs inutilisés. Suivant le nombre de programmes/threads exécutés, l'ensemble des coeurs peut être utilisés, ou alors seulement une partie. Dans le second cas, il est possible d'éteindre les coeurs inutilisés pour économiser de l'énergie. Une telle optimisation porte le nom de core parking. Elle est gérée par le système d'exploitation, ou par le processeur lui-même sur les processeurs récents. Sur les processeurs Intel, le core parking est géré par le processeur lui-même, les processeurs des autres marques font la même chose de nos jours.

Il est possible d'activer ou de désactiver le core parking, même si le processeur le gère. Le core parkingest alors géré par le processeur lui-même, mais il reste configurable en modifiant quelques registres de contrôle. D'ailleurs, les modes de consommation d'énergie intégrés à windows (basse consommation, équilibré et performance) configurent ce réglagee différemment : le core parking est désactivé pour le mode "performance". Quelques logiciels permettent de désactiver le core parking, et cette fonctionnalité est parfois désactivée sur quelques PC Gaming où les paramètres Windows sont réglés pour. La rumeur veut que le PC soit plus réactif une fois ce réglage désactivé, car rallumer un coeur prendrait un peu de temps si nécessaire, mais cela reste une rumeur.

Le core parking désactive les cœurs inutilisés : ils ne sont plus alimentés en courant/tension, et le signal d'horloge est coupé. La même logique peut s'appliquer à des circuits plus petits, à l'intérieur d'un coeur. Par exemple, si seulement une partie de la mémoire cache du processeur est inutilisée, elle est désactivée. Ainsi, on s'assure que la partie utile du cache est alimentée en courant, pas celle qui est inutilisée. Cela demande de partitionner le cache en morceaux activables ou désactivables selon les besoins, mais ca vaut le coup. Nous en reparlerons dans le chapitre sur le cache.

Toujours est-il que les deux techniques, couper la fréquence et/ou l'alimentation, doivent alors s'implémenter en ajoutant des circuits pour couper le courant ou l'horloge. Voyons comment ils fonctionnent.

Le Power Gating et le Clock Gating

[modifier | modifier le wikicode]
Power Gating.

La première solution cesse d'alimenter ce qui ne fonctionne pas, ce qui est en pause ou inutilisé, pour éviter de consommer du courant inutilement : 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 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'évaluation gardée

[modifier | modifier le wikicode]

L'évaluation gardée est une technique assez proche du clock gating et du power gating, dans l'esprit. Elle s'applique dans le cas d'un circuit dont les sorties sont utiles sous certaines conditions, mais inutiles dans d'autres. Un tel circuit peut en théorie utiliser une clock ou power gate pour l'éteindre quand on ne l'utilise pas. Mais le temps de réaction d'une clock/power gate n'est pas compatible avec un circuit à l'utilisation rapidement changeante.

Une solution alternative active ou désactive le circuit avec un signal de commande, relié à un registre d'entrée. Chose très importante : le registre est à entrée Enable. Concrètement, si l'entrée Enable est activée, l'entrée est recopiée sur la sortie, le registre est alors transparent et tout se passe comme s'il n'était pas là. Mais quand le signal Enable passe à 0, alors l'ancienne valeur de l'entrée est mémorisée dans le registre. Si le circuit est utilisé, l'entrée Enable est à 1 et le circuit fonctionne normalement. Mais si on veut désactiver le circuit, on met l'entrée à 0 : le circuit est figé avec l'ancienne valeur de l'entrée.

L'intérêt est que cela fige le circuit dans l'état dans lequel il était. N'oublions pas que ce qui consomme de l'énergie, c'est de faire changer d'état les transistors ! Si on éteignait ou remettait le circuit à zéro, cela demanderait un petit peu d'énergie pour faire la transition. Ici, au lieu d'éteindre le circuit, on fige ce qu'il y a sur l'entrée, et donc l'état des transistors dans tout le circuit ! Pas de transition pour éteindre le circuit, sauf peut-être un petit peu dans le registre.

Evaluation gardée

Le défaut est que le circuit fournit sur sa sortie un résultat, qu'il faut ignorer. Mais heureusement, il arrive que la sortie d'un circuit ne soit tout simplement pas prise en compte. Le cas le plus simple est celui où le circuit est suivi par un multiplexeur. Dans ce cas, si la sortie du circuit n'est pas choisie par le multiplexeur et qu'on peut le savoir à l'avance, le circuit peut être figé par évaluation gardée. Un exemple est celui des processeurs, qui disposent de plusieurs circuits de calculs : des circuits de calcul spécialisés dans les additions/soustraction, d'autres dans les multiplications, d'autres dans les opérations logiques, et autres ; qui sont suivies par un multiplexeur pour choisir le résultat adéquat. Suivant le calcul à effectuer, qui est connu à l'avance, on sait quel circuit de calcul choisir et comment configurer le multiplexeur.

Les techniques basées sur la relation entre tension et fréquence

[modifier | modifier le wikicode]

Un point important est fréquence et tension d'alimentation sont liées. Il est possible de démontrer que la fréquence d'un circuit imprimé dépend de la tension d'alimentation et suit une relation approximative qui ressemble à ceci :

, avec U la tension d'alimentation et une tension minimale appelée la tension de seuil en-dessous de laquelle les transistors ne fonctionnent plus correctement.

L'équation précédente se reformule en :

La conséquence immédiate est que baisser la fréquence permet de faire baisser l'autre. Et inversement, faire baisser la tension implique de faire baisser la fréquence, sans quoi les transistors ne peuvent plus suivre.

La distribution de fréquences et de tensions d'alimentations multiples

[modifier | modifier le wikicode]

Une première solution prend en compte le fait que certaines portions du processeur sont naturellement plus rapides que d'autres. La technique a été utilisée sur certains processeurs multicoeurs, où certains coeurs sont naturellement cadencés à une fréquence inférieur. De tels processeurs regroupent 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".

Mais ce principe peut aussi s'appliquer à l'intérieur d'un coeur, qu'il soit de haute ou basse performance. 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 diviseurs de fréquence dans l'arbre d'horloge. Le processeur est divisé en domaines d'horloge séparés, chacun allant à sa propre fréquence. Les circuits sont répartis dans chaque domaine d’horloge suivant ses besoins. Nous avions abordé les domaines d'horloge dans le chapitre sur les circuits synchrones et asynchrones, dans la dernière section nommée "La distribution de l'horloge dans un circuit complexe".

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 | modifier le wikicode]

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.

Il est possible de réduire la tension d'alimentation, si on réduit la fréquence en même temps. La technologie consistant à diminuer la tension d'alimentation suivant les besoins s'appelle le Dynamic Voltage Scaling, de son petit nom : DVS. Elle donne des gains bien plus importants que le DFS, vu que la consommation dynamique dépend du carré de la tension d'alimentation.

Le seul problème avec cette technique est la tension de seuil, qui dépend de la physique des transistors et qui est relativement constante, proche de 0,4 Volts. Plus la tension d'alimentation des processeurs diminue, plus elle se rapproche de la tension de seuil. En soi, pas de problème pour ce qui est de la fréquence maximale, qui reste globalement la même du fait de l'impact de la loi de Moore. Par contre, le DVS est limité par la tension de seuil. Comparons par exemple une tension de 2 volts et de 1 Volt. Avec 2 volts, on a une marge de 1,6 V que le DVS peut utiliser. Mais avec une tension d'alimentation de 1 Volt, il ne reste qu'une marge de 0,6 V à exploiter pour le DVS. La réduction de consommation liée au DVS est donc de plus en plus limité avec le temps, avce la réduction naturelle de la tension d'alimentation.

L'implémentation exacte agit sur les multiplieurs de fréquence et les régulateurs de tension. Pour rappel, un processeur recoit une fréquence sur sa broche d'horloge, mais qui est plus faible que sa fréquence réelle. Mais un circuit dédié multiplie cette fréquence par un certain coefficient pour obtenir sa fréquence réelle. Avec le DFS, ce multiplieur de fréquence est configurable, dans le sens où on peut configurer son coefficient multiplicatif, en choisir un parmi une liste de quelques coefficients prédéterminés. Il y a la même chose avec la tension, sauf que c'est là le fait des régulateurs de tension intégré au processeur, qui sont un peu l'équivalent du multiplieur de fréquence mais pour la tension.

La configuration de la fréquence et de la tension peut se faire de plusieurs manières. Une méthode simple est une configuration logicielle. Le processeur contient de DVFS un registre dans lequel le système d'exploitation écrit les valeurs de configuration nécessaire. L'OS ne peut pas choisir directement la fréquence et le voltage, mais il peut choisir un niveau de DFVS, un numéro qui correspond à un couple tension-fréquence précis. De nos jours, cette solution n'est plus trop utilisée, le réglage étant le fait du processeur lui-même. Tous les processeurs modernes incorporent un circuit qui est spécialisé dans la régulation de la tension et de la fréquence. Il monitore l'utilisation du processeur, de ses circuits, sa température, et qui décide du couple tension-fréquence en fonction de. Il contient des tables qui permettent de savoir quelle couple tension-fréquence est le plus optimal en fonction de utilisation.

Sur les processeurs multicoeurs, la régulation du couple tension-fréquence se fait cœur par cœur, avec cependant une régulation globale. Il est possible d'éteindre les cœurs non-utilisés, de faire tourner les cœurs peu utilisés à basse fréquence, pendant que les cœurs très utilisés sont à fréquence maximale. De nos jours, la fréquence maximale n'est atteinte que dans ces circonstances bien précises : il faut que seul un cœur soit actif, et les autres en veille ou très peu occupés. Si tous les cœurs sont très occupé, la fréquence des cœurs atteint un plateau inférieur à la fréquence maximale. Mais si un seul cœur est utilisé, alors il ira à la fréquence maximale. La raison tient dans les limitations thermique, et au problème du dark silicon.

La régulation tension-fréquence doit aussi tenir compte des domaines d'horloge. Dans le cas le plus simple, la réduction de la fréquence est la même pour chaque domaine d'horloge, de même que la réduction de la tension. Elle ne tient pas vraiment compte des domaines d'horloge, du moins dans le sens où la régulation est globale, même si chaque domaine d'horloge voit sa fréquence impactée. Mais il est aussi possible de réguler le couple tension-fréquence indépendamment pour chaque domaine d'horloge, afin de gérer plus finement la réduction de fréquence. Il est même possible de vérifier l'utilisation de chaque domaine d'horloge, et de régler leur fréquence suivant s'ils font beaucoup de calculs ou pas.


Les bus et liaisons point à point

[modifier | modifier le wikicode]

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 connecteurs, mais aussi des circuits électroniques soudés à la carte mère.

Architecture matérielle d'une carte mère

Les connecteurs sont là où on branche les périphériques, la carte 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.

Une observation plus poussée de la carte mère vous permettra de remarquer quelques puces électroniques soudées à la carte mère. Elles ont des fonctions très diverses mais 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. Il y a aussi le BIOS, des générateurs de fréquence, des circuits support pour le processeur et les mémoires, et d'autres, que nous détaillerons dans la suite du chapitre.

Enfin, et surtout, une carte mère est là où se trouvent les bus qui interconnectent tout ce beau monde. Si vous observez la carte mère de près, vous verrez des lignes métalliques, de couleur cuivre, qui ne sont autre que les bus en question. Une ligne métallique est un fil qui transmet un courant électrique, qui sert à faire passer un bit d'un composant/connecteur à un autre.

L'organisation de ce chapitre est la suivante : nous allons d'abord voir les circuits soudés sur la carte mère, avant de parler de la manière dont les bus sont organisés sur la carte mère. À la fin de ce chapitre, 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.

Le Firmware : BIOS et UEFI

[modifier | modifier le wikicode]

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é 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 standards : 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, l'EFI et l'UEFI

[modifier | modifier le wikicode]

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 (Basic Input-Output System). 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. De nos jours, il tend à être remplacé par l'EFI et l'UEFI, qui utilise un standard différent, mais n'est pas différent dans les grandes lignes.

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, notamment le fait que la table des partitions utilisée par le BIOS 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, ce qui 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.

Les normes 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 du 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. Cependant, l'UEFI gère la rétrocompatibilité avec les anciens BIOS, ce qui fait qu'ils conservent les anciennes fonctionnalités du BIOS. Aussi, tout ce qui est dit dans cette section sera aussi valide pour l'UEFI, dans une certaine mesure.

En plus du BIOS système, les cartes d'extension peuvent avoir un BIOS. Par exemple, 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. On peut aussi 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. Le contenu des BIOS d'extension dépend fortement du périphérique en question, contrairement au BIOS système dont le contenu est relativement bien standardisé.

L'accès au BIOS

[modifier | modifier le wikicode]
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 | modifier le wikicode]

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à où se trouve le BIOS. Pour info, cette adresse est l'adresse maximale en mémoire réelle moins 16 octets. Le BIOS s’exécute et initialise l'ordinateur avant de laisser la main au système d'exploitation.

Le BIOS commence la séquence de démarrage avec le POST (Power On Self Test), qui effectue quelques vérifications. Il commence par vérifier que le BIOS est OK, en vérifiant une somme de contrôle présente à la fin du BIOS lui-même. Il vérifie ensuite l'intégrité des 640 premiers kibioctets de la mémoire.

Ensuite, les périphériques sont détectés, testés et configurés pour garantir leur fonctionnement. Pour cela, le BIOS explore la mémoire haute pour détecter les 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.

Pour détecter les BIOS d'extension, le BIOS lit la mémoire haute par pas de 2 kibioctets. En clair, il analyse toutes les adresses multiples de 2 kibioctets, dans la mémoire haute. Par exemple, le BIOS regarde s'il y a un BIOS vidéo aux adresses mémoire 0x000C:0000h et 0x000E:0000h. Il y recherche 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.

Ensuite, le BIOS effectue quelques opérations de configuration assez diverses, aux description assez barbares (initialiser le vecteur d'interruption de l'ordinateur, passage en mode protégé, et bien d'autres). 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.

Si tout fonctionne bien, 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. Le BIOS utilise une interface assez basique, limitée à du texte, alors que l'UEFI gère une vraie interface graphique, avec un affichage pixel par pixel.

Par la suite, le BIOS démarre le système d'exploitation. Le processus est totalement différent entre BIOS et UEFI. Dans les deux cas, le BIOS lit une structure de données sur le disque dur, qui contient toutes les informations pertinentes pour lancer le système d'exploitation. Elle est appelée le MBR pour le BIOS, la GPT pour l'UEFI. La première est limitée à des partitions de 2 téraoctets, pas la seconde, la structure n'est pas la même, de même que le mécanisme de boot. Mais 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.

Les paramètres du BIOS

[modifier | modifier le wikicode]

Si vous avez déjà fouillé dans l'interface graphique du BIOS, vous avez remarqué que celui-ci fournit beaucoup d'options de configuration. Ils sont peu nombreux sur les PC constructeurs, mais en grand nombre sur les PC montés à la main. La raison est que les PC constructeurs utilisent des BIOS personnalisés, qui réduisent volontairement le nombre d'options accessibles à l'utilisateur. Le but est de réduire les appels au SAV ou les retour garanties faisant suite à une mauvaise manipulation du BIOS. Les cartes mères vendues à l'unité, dans les PC fait maison, n'ont pas ces contraintes et fournissent toutes les options disponibles.

Mais le fait qu'il y ait des options dans le BIOS devrait nous poser une question. Le BIOS est stocké dans une mémoire ROM, qu'on peut lire, mais pas modifier. Vous me rétorquerez sans doute que le fait qu'on puisse flasher le BIOS contredit cela, que le BIOS est stocké dans une FLASH, pas dans de la ROM. Mais malgré tout, cela n'est pas compatible avec une modification rapide des options du BIOS : on n'efface pas toute la FLASH du BIOS en modifiant une simple option. Alors où sont stockés ces paramètres de configuration ?

La réponse est qu'ils sont stockés dans une mémoire FLASH ou EEPROM séparée du BIOS, appelée la Non-volatile BIOS memory. L'implémentation exacte dépend du BIOS. Au tout début, sur les premiers PC IBM et autres, il s'agissait d'une EEPROM séparée. Mais rapidement, elle a été fusionnée avec d'autres mémoires naturellement présentes sur la carte mère. Elle a ensuite été fusionnée avec la CMOS RAM, une mémoire RAM que nous verrons plus bas dans la suite du chapitre. De nos jours, elle est intégrée dans le chipset de la carte mère, avec la CMOS RAM et bien d'autres composants.

Une fonction obsolète : la gestion des périphériques

[modifier | modifier le wikicode]

Autrefois, la gestion des périphériques était intégralement le fait du BIOS. 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 ». Pour cela, il intégrait des morceaux de code dédiés qui servaient à gérer le disque dur, la carte graphique, etc. Intuitivement, si je dis morceau de code, les programmeurs se disent qu'il doit s'agir de fonctions logicielles, en référence à une fonctionnalité commune de tous les langages de programmation modernes. Pour être plus précis, il s'agit en réalité de routines d'interruptions, mais la différence sera expliquée dans un chapitre ultérieur portant justement sur les interruptions, dans lequel nous étudierons rapidement les interruptions du BIOS.

Les circuits de surveillance matérielle : RESET et NMI

[modifier | modifier le wikicode]

Les circuits de surveillance matérielle vérifient en permanence la tension d'alimentation, la fréquence d'horloge, les températures, le bon fonctionnement de la mémoire, et bien d'autres choses. En cas de problèmes, ils peuvent redémarrer l'ordinateur ou l'éteindre. Pour cela, ces circuits communiquent directement avec le processeur, grâce à deux entrées sur processeur : l'entrée RESET, et l'entrée d'interruption non-masquable NMI (Non-Maskable Interrupt). L'entrée RESET a nom un assez transparent et on comprend qu'elle redémarre le processeur. L'entrée d'interruption non-masquable mérite cependant quelques explications.

L'entrée RESET du processeur

[modifier | modifier le wikicode]

Le processeur dispose d'une entrée RESET qui, comme son nom l'indique, le réinitialise quand on met le niveau logique ou front adéquat dessus. Lorsqu'on envoie le signal adéquat sur l'entrée RESET, le processeur remet à zéro tous ses registres et lance la procédure d'initialisation du program counter. Il peut être initialisé de deux façons différentes. Avec la première, il est initialisé à une valeur fixe, déterminée lors de la conception du processeur, souvent l'adresse 0. L'autre solution ajoute une indirection, elle précise l'adresse d’initialisation dans le firmware. Le processeur lit le firmware à une adresse fixée à l'avance, pour récupérer l'adresse de la première instruction et effectuer un branchement.

L'entrée RESET est lié de près ou de loin au bouton d'alimentation de l'ordinateur. Quand vous allumez votre ordinateur, cette broche RESET est activée, le processeur démarre. Aussi, si vous appuyez environ 10/15 secondes sur le bouton d'alimentation, l'ordinateur redémarre. C'est parce que l'entrée RESET a été activé par l'appui continu du bouton. Attention cependant, certains ordinateurs ou certaines consoles de jeu vidéo avaient un bouton RESET directement connectée au signal RESET : appuyer et relâcher le bouton active le signal RESET sans délai.

Le signal RESET n'est pas produit directement par le bouton d'allumage. En effet, au démarrage de l’ordinateur, le processeur ne peut pas être RESET immédiatement. Il doit attendre que la tension d'alimentation se stabilise, que le signal d'horloge se stabilise, etc. Pour cela, des circuits aux noms barbares de Power On Reset et d'Oscilator startup timer vérifient la stabilité de la tension et de l'horloge. Le signal RESET n'est généré que si les conditions adéquates sont remplies.

Le signal RESET est donc produit au démarrage, mais il peut aussi être produit lors du fonctionnement de l'ordinateur, pour redémarrer en urgence l'ordinateur en cas de gros problème matériel. Par exemple, en cas de défaillance de l'alimentation, un signal RESET peut être produit pour limiter les dégats. Nous verrons aussi l'exemple du watchdog timer dans ce qui suit. Pour résumer, le signal RESET est produit en combinant plusieurs signaux, provenant de circuits de surveillance matérielle dispersés sur la carte mère. Nous verrons ceux-ci dans la suite du chapitre, car nous les verrons indépendamment les uns des autres.

Vous vous demandez sans doute pourquoi j'ai parlé de redémarrage d'urgence en cas de problème, au lieu d'un banal redémarrage. La raison est que les deux ne sont pas du tout les mêmes, surtout au regarde de l'entrée RESET. Le redémarrage via l'entrée de RESET est aussi appelée un reset hardware, et on l'oppose au reset logiciel. Un reset logiciel est simplement le reset qui a lieu quand vous redémarrez votre ordinateur normalement, en demandant à Windows/linux de redémarrer. Il n'implique pas l'usage de l'entrée RESET, du moins il n'est pas censé le faire. Les méthodes pour RESET un processeur de manière logicielle dépendent beaucoup du processeur considéré. Sur les processeurs x86, il a une demi-douzaine de méthodes différentes.

Une méthode courante sur les CPU x86 et ARM utilise un registre de reset. Les processeur écrivent dans ce registre pour déclencher un RESET. Suivant ce qu'ils écrivent dedans, ils peuvent déclencher tel ou tel type de reset (cold reset, warm reset, éteindre l'ordinateur, autres). Avec l'UEFI, le firwmare dispose d'une fonction faite pour, que le système d'exploitation peut appeler à volonté. Il peut aussi tenter d'exécuter des branchements spécifiques. Mais un tel reset logiciel ne fait pas usage de l'entrée RESET proprement dit.

Il y avait cependant une exception de taille, où le logiciel pouvait déclencher un reset hardware, par l'intermédiaire d'un circuit appelé le contrôleur de clavier 8279. Le contrôleur de clavier 8279, comme son nom l'indique, est un circuit connecté au port clavier/souris. Les anciens PC avaient des ports PS/2 pour le clavier et la souris, et le contrôleur de clavier 8279 était situé de l'autre côté du connecteur. Il s'agissait d'un microcontroleur qui recevait les touches appuyées sur le clavier, configurait les LED pour le verr num, l'arret defil et autres. Mais une des broche de sortie de ce microcontroleur était directement reliée à l'entrée RESET du processeur, il y avait juste quelques portes logiques entre les deux. En configurant le controleur de clavier, on pouvait lui faire faire un RESET via l'entrée RESET !

L'entrée NMI d'interruption non-masquable

[modifier | modifier le wikicode]

L'entrée RESET est plus rarement utilisée lorsqu'une défaillance matérielle irrécupérable est détectée. Le résultat de telles défaillances est que l'ordinateur est arrêté de force, redémarré de force, ou affiche un écran bleu. Redémarrer l'ordinateur est possible avec un signal RESET, mais pas l'affichage d'un écran bleu ou éteindre l'ordinateur. Pour cela, le processeur dispose d'une seconde entrée, séparée du RESET, appelée l'entrée NMI. Pour simplifier, il s'agit d'une entrée pour l'arrêt d'urgence. Lorsqu'on envoie le signal adéquat sur l'entrée NMI, le processeur stoppe immédiatement ce qu'il est en train de faire, puis exécute un programme d'arrêt urgence. Ce dernier détecte l'origine de l'erreur et réagit en conséquence : soit en réparant l'erreur, soit en affichant un écran bleu, soit en éteignant l'ordinateur.

Nous verrons dans quelques chapitres le concept d'interruption, et précisément d'interruption non-masquable. Pour simplifier, une interruption est ce qui permet de stopper le processeur pour lui faire exécuter un programme d'urgence, avant de reprendre l'exécution. L'acronyme NMI signifie Non Maskable Interrupt, ce qui veut dire interruption non-masquable, non-masquable dans le sens où l'interruption ne peut pas être ignorée ou retardée.

Les deux entrées NMI et RESET semblent foncièrement différentes et s'utilisent dans des circonstances distinctes. Cependant, quelques circuits sur la carte mère sont reliées aux deux entrées. C'est le cas des circuits qui surveillent la tension d'alimentation et l'horloge. Un ordinateur ne peut pas fonctionner si la tension d'alimentation n'est pas stable, idem pour la fréquence d'horloge, idem si les températures sont trop élevées. Si les conditions ne sont pas remplies, l'ordinateur ne peut pas démarrer. De même, en cas de défaillance de la tension d'alimentation ou de la fréquence, le processeur doit être éteint via l'entrée NMI. Aussi, divers circuits vérifient ces paramètres en permanence. Ils autorisent le démarrage de l’ordinateur via l'entrée RESET, mais peuvent aussi le redémarrer ou activer l'entrée NMI si besoin. Les entrées NMI et RESET sont donc reliées à des circuits communs.

Le watchdog timer est reliée à l'entrée RESET

[modifier | modifier le wikicode]

Un autre exemple d'utilisation de l'entrée RESET du processeur est lié au watchdog timer. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre automatiquement l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui est connecté à l'entrée RESET du processeur. Si le compteur/décompteur déborde (au sens débordement d'entier), alors il génère un signal RESET pour redémarrer le processeur. On part du principe que si l'ordinateur ne réinitialise par le watchdog timer, c'est qu'il a planté.

Le processeur réinitialise le watchdog timer régulièrement, ce qui signifie que le compteur est remis à zéro avant de déborder, et le système n'est pas censé redémarrer. Pour cela, il utilise l'entrée NMI, d'une manière assez particulière. Régulièrement, un timer séparé envoie un 1 sur l'entrée NMI. Le processeur exécute alors son programme d'urgence, qui cherche la source de l'interruption. Il remarque alors que l'activation de l'entrée NMI provient de ce timer. Le programme d'urgence réinitialise alors le watchdog timer.

Le Watchdog Timer et l'ordinateur.

La gestion des fréquences et les timers

[modifier | modifier le wikicode]

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. Il 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.

Les multiplieurs et diviseurs de fréquence

[modifier | modifier le wikicode]

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

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.

Une carte mère peut aussi contenir des diviseurs de fréquences, pour générer des fréquences très basses. C'était surtout le cas avec les anciens systèmes, où certains bus allaient à quelques kiloHertz. Les diviseurs de fréquences sont fabriqués avec des compteurs, comme nous l'avions vu dans le chapitre sur les timers et diviseurs de fréquence. Vu que nous avons déjà abordé ces composants, nous ne reviendrons pas dessus.

Le générateur de fréquence : un oscillateur à Quartz

[modifier | modifier le wikicode]

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.

Oscillateur à Quartz, sur une carte mère.

La quasi-totalité des générateurs de fréquences des ordinateurs modernes sont des oscillateurs à quartz, similaires à celui présent dans les montres électroniques. Ils sont fabriqués en combinant un amplificateur avec un cristal de Quartz. Ils 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. Le cristal de Quartz a 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 timers intégrés à la carte mère

[modifier | modifier le wikicode]

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 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 un tâche précise à 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 fonctionnalité 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, qui se trouvent sur la carte mère ou dans le processeur, tout dépend du timer. Sur les anciens PC, la carte mère incorpore 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'ont qu'un seul timer sur la carte mère, 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. Plus rarement, certaines cartes mères possèdent un watchdog timer.

L'Oscillator start-up timer : la stabilité de l'horloge au démarrage

[modifier | modifier le wikicode]

Au démarrage de l'ordinateur, la tension d'horloge n'est pas stable. Il faut un certain temps pour que les circuits de génération d'horloge se stabilisent et fournissent un signal d'horloge stable. Et tant que le signal d'horloge n'est pas stable, le processeur n'est pas censé démarrer. L'Oscillator start-up timer est un circuit qui temporise en attendant que le signal d'horloge se stabilise. Le signal RESET, nécessaire pour démarrer le processeur, n'est mis à 1 que si l'Oscillator start-up timer lui donne le feu vert.

Dans son implémentation la plus simple, il s'agit d'un simple timer, un vulgaire compteur qui attend qu'un certain nombre de cycles d'horloge se soient écoulés. Il est supposé que la fréquence est stable après ce nombre de cycles. Par exemple, sur les microcontrôleurs PIC 8-bits, l'OST compte 1024 cycles d'horloge avant d'autoriser le RESET. D'autres processeurs attendent durant un nombre de cycles plus important, le nombre de cycles exact dépend de la fréquence. Notez que l'OST attend un certain nombre de cycles. Les premiers cycles sont irréguliers, le tout se stabilise vers la fin, ce qui ne correspond pas à un temps bien défini, une petite variabilité est toujours présente.

Un exemple de circuit générateur de fréquence : l'Intel 8284

[modifier | modifier le wikicode]

L'Oscillator start-up timer est parfois fusionné avec d'autres circuits, dont des multiplieurs/divisieurs de fréquence et des timers. Un bon exemple est celui de l'Intel 8284, un circuit qui fusionne Oscillator start-up timer, diviseur de fréquence et générateur de fréquence.

Intel 8284

Il est possible de le relier à un cristal de Quartz sur deux entrées nommée X1 et X2, et il génère alors un signal d'horloge à partir de ce qu'il reçoit du cristal. Le signal d'horloge généré est alors disponible sur une sortie dédiée, la sortie OSC. C'est la fonction générateur d'horloge. Dans le détail, pour générer une fréquence, il faut combiner un cristal de Quartz avec un circuit dit oscillateur. L'Intel 8284 incorpore l’oscillateur, mais pas le cristal de Quartz.

Il est aussi possible de l'utiliser en tant que diviseur de tension, bien que ce soit rudimentaire. La fréquence d'entrée est divisée par trois et le résultat est présenté sur la sortie PCLK. La fréquence de sortie a alors un rapport cyclique de 1/3. L'implémentation de la division par trois se fait avec un simple compteur. Un second signal d'horloge est disponible sur la sortie nommée CLK. Il a une de fréquence deux fois moindre que le premier, soit 6 fois moins que la fréquence d'entrée, et a un rappoort cyclique de 50% (signal carré).

Un point important est que la fréquence d'entrée peut provenir de deux sources différentes. En premier lieu, elle peut être présentée sur une broche séparée, appelée l'entrée EFI. En second lieu, elle peut provenir de l’oscillateur à Qaurtz intégré au 8284. Le choix entre les deux se fait avec l'entrée F/C : oscillateur si c'est un 1, entrée EFI si c'est un 0.

L'intérieur du circuit est assez simple : un timer pour l'OST, un oscilateur à Quartz, un multiplexeur pour choisir la fréquence d'entrée, deux compteurs pour les diviseurs de fréquences.

Annexe : le bouton Turbo des vieux PC

[modifier | modifier le wikicode]
Exemple de façade de PC avec un bouton Turbo.

Si vous êtes assez vieux, vous avez peut-être déjà utilisé un PC qui disposait d'un bouton Turbo à côté du bouton ON/OFF. Vous êtes vous êtes demandé ce à quoi servait ce bouton ? Et surtout : saviez-vous que contre-intuitivement, il ralentissait l'ordinateur ? La raison à cela est partiellement liée à la fréquence du processeur : le bouton Turbo réduisait la fréquence du processeur (sauf exceptions) ! La raison est une question de compatibilité logicielle avec les processeurs 8086 et 8088 d'Intel.

À l'époque, de nombreux jeux ou logiciels interactifs se basaient sur la fréquence du CPU pour mesurer le temps des actions/évènements. Mais lors du passage au processeurs 286, 396 et 486, la fréquence du CPU a augmenté. Les logiciels allaient alors plus rapidement, les jeux vidéos étaient accélérés au point d'en devenir injouables. L'effet était un peu similaire à la différence entre jeux en 50 et 60 Hz, sauf qu'ici, la fréquence était multipliée par 2, 4, voire 10 ! Le 8088 et le 8086 avait une fréquence typique de 4,77 MHz mais pouvaient utiliser une fréquence de 8 MHz sur certains modèles d'ordinateurs. Le 186 allait à 6 MHz, le 286 allait à 10 ou 12.5 MHz selon la version, le 386 allait de 12.5 MHz à 40 MHz selon la version, et le 486 allait de 16 à 100 MHz.

Pour éviter cela, les ordinateurs ont ajouté un bouton Turbo, qui était en réalité un bouton de compatibilité. Lorsqu'on l'appuyait, la vitesse de l'ordinateur était réduite de manière à faire tourner les anciens jeux/logiciels à la bonne vitesse. En clair, le bouton était mal nommé et faisait l'inverse de ce qu'on peut croire intuitivement. Du moins, c'était le principe. Quelques ordinateurs fonctionnaient à vitesse réduite à l'état normal et il fallait appuyer sur le bouton Turbo pour retrouver une vitesse normale.

Et le bouton Turbo agissait sur la fréquence du CPU. Il baissait sa fréquence pour le faire descendre aux 4,77/5 MHz adéquats. Du moins, certaines cartes mères faisaient ainsi. Des rumeurs prétendent que certains carte mères faisaient autrement, en désactivant le cache du processeur, ou en réduisant la fréquence effective du bus (en ajoutant des wait state, des cycles où le bus est inutilisé). Mais dans la majorité des cas, on peut supposer que la réduction de la fréquence était belle et bien utilisée. Et pour cela, il fallait relier le bouton Turbo à des circuits diviseurs de fréquence pour réduire la fréquence du CPU. Qu'un bouton soit relié, même indiretcement aux circuits d'horloge, est tout de même quelque chose d'assez inattendu. Imaginez si les PC actuels avaient un bouton "Power Saving" qui réduisait la fréquence du processeur.

L'horloge temps réel et la CMOS RAM

[modifier | modifier le wikicode]

Parmi tous les timers présents sur la carte mère, l'horloge temps réel se démarque des autres. Dans ce qui suit, nous la noterons RTC, ce qui est l'acronyme du terme anglais Real Time Clock. La RTC est cadencée à fréquence de 1 024 Hz, soit près d'un Kilohertz, ou du moins un circuit capable de l'émuler. La RTC est parfois connectée à l'entrée d'interruption non-masquable, car elle est utilisée pour des fonctions importantes du système d'exploitation, comme la commutation entre les processus, et bien d'autres. Mais son fonction principale est toute autre.

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 dans la section suivante comment cette RAM 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). Oui, vous avez bien lu : la CMOS RAM est utilisée pour stocker les paramètres du BIOS vu plus haut, elle sert de non-volatile BIOS memory. Il s'agit là d'une optimisation : au lieu d'utiliser une non-volatile BIOS memory et une CMOS RAM séparée, on utilise une seule mémoire non-volatile pour les deux. Mais la manière de stocker les paramètres du BIOS dans la CMOS RAM a beaucoup changé dans le temps.

Les anciens PC avaient un bus ISA, ancêtre du bus PCI. Et le BIOS devait mémoriser la configuration de bus, ainsi que l'ensemble des périphériques installé sur ce bus. Pour cela, ces données de configuration étaient stockées dans la CMOS RAM. Elles suivaient le standard dit d'Extended System Configuration Data. Il fournissait un format standard pour stocker les paramètres du bus ISA, l'adresse où placer ces données, et trois fonctions/interruptions du BIOS pour récupérer ces données. Les données de configuration ISA prenaient les 128 octets à la fin de CMOS ROM. Les bus plus récents ne mémorisent plus de données de configuration dans la CMOS RAM, ni même dans le BIOS.

La source d'alimentation de la RTC et de la CMOS RAM

[modifier | modifier le wikicode]
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 chipset de la carte mère

[modifier | modifier le wikicode]

L'organisation des cartes mères des ordinateurs personnels a évolué au cours du temps pendant que de nombreux bus apparaissaient. Les premiers ordinateurs faisaient avec un simple bus système qui connectait processeur, mémoire et entrée-sortie. Mais avec l'augmentation du nombre de périphériques et de composants systèmes, l'organisation des bus s'est complexifiée.

La première génération : les bus partagés

[modifier | modifier le wikicode]

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. 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

Bus système

L'apparition du chipset

[modifier | modifier le wikicode]

Les cartes mères récentes, après les années 1980, ne peuvent plus utiliser un bus unique, car il y a trop de composants à connecter dessus. A la place, elles utilisent une architecture à base de répartiteurs. Pour rappel, ce répartiteur est placé en avant du processeur et sert d'interface entre celui-ci, un bus pour la mémoire, et un bus pour les entrées-sorties. Mais il a aussi d'autres capacités qui dépassent de loin son rôle d'interface. Il a intégré des circuits comme le contrôleur mémoire, un contrôleur DMA, un contrôleur d'interruption, etc.

IO mappées en mémoire avec séparation des bus, usage d'un répartiteur

Un exemple est le chipset utilisé avec le processeur Intel 486, sorti en 1989. Il était connecté au processeur, à la mémoire RAM, aux différents bus, mais aussi à la mémoire cache (qui était séparé du processeur et placé sur la carte mère). Pour ce qui est des bus, il était connecté au bus PCI, au bus IDE pour le disque dur et au bus SMM. Il était indirectement connecté au bus ISA, un bus ancien conservé pour des raisons de rétrocompatibilité, mais qui avait été éclipsé par le PCI.

La séparation du chipset en deux

[modifier | modifier le wikicode]

Un autre problème est que la mémoire et la carte graphique sont devenus de plus en plus rapide avec le temps, au point que les périphériques ne pouvaient plus suivre. Les différents composants de la carte mère sont 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.

Entre les années 80 et 2000, la solution retenue par les fabricants de cartes mères utilisait deux répartiteurs séparés : le northbridge et le southbridge. Le northbridge 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 controleur mémoire. Le southbridge est le répartiteur pour les composants lents. 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.

Chipset séparé en northbridge et southbridge.
Carte mère avec circuit Super IO.

Ce qui est mis dans le southbridge est très variable selon la carte mère. Le southbridge intègre généralement tout ce qu'il faut pour gérer des périphériques, ce qui inclut des circuits aux noms barbares comme des contrôleurs de périphériques, des contrôleurs d'interruptions, des contrôleurs DMA, etc. Mais les chipsets modernes ont tellement de transistors qu'on y intègre presque tout et n'importe quoi. Par exemple, les cartes mères des processeurs Intel 5 Series intégraient la Real Time Clock. Il arrive aussi que la CMOS RAM soit intégrée au southbridge. Par contre, sur la plupart des cartes mères, le BIOS est placé en-dehors du southbridge.

Il est arrivé que le chipset incorpore des cartes sons basiques, voire carrément des cartes graphiques (GPU). Les cartes son des chipsets étaient au départ assez mauvaises, mais ont rapidement augmenté en qualité, au point où plus personne n'utilise de carte son séparée dans son ordinateur. Quand aux GPU intégrés au chipset suffisaient largement pour une utilisation bureautique de l'ordinateur. Tant que l'utilisateur ne jouait pas à des jeux vidéos peu gourmands, le GPU du chipset suffisait. De plus, il consommait peu d'énergie et produisait peu de chaleur, le chipset avait juste à être refroidit par un simple radiateur. De nos jours, ils sont remplacées par les GPU intégrés aux processeurs, qui ont globalement les mêmes défauts et qualités.

Sur certaines cartes mères, le southbridge est complémenté par un circuit de Super IO, qui s'occupe des périphériques et bus anciens, comme le bus pour le lecteur de disquette, le port parallèle, le port série, et les ports PS/2 pour le clavier et la souris. De plus, ce circuit peut contenir beaucoup d'autres sous-circuits. Par exemple, il peut contenir des capteurs de température, les circuits qui contrôlent la vitesse des ventilateurs, divers timers, et quelques autres.

L'intégration du northbridge au processeur

[modifier | modifier le wikicode]
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. le southbridge est encore là, il est toujours connecté au processeur et aux périphériques. Concrètement, le processeur a un bus mémoire séparé, non-connecté au répartiteur de type southbridge.

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 processeur incorpore pour cela des contrôleurs PCI-Express, et même d'autres circuits contrôleurs de périphériques. 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.

La gestion thermique : sondes de température et ventilateurs

[modifier | modifier le wikicode]
Section de la carte mère avec dissipateur thermique et ventilateur en aluminium au-dessus du processeur AMD. À côté, on peut voir un dissipateur thermique plus petit, sans ventilateur, au-dessus d'un autre circuit intégré de la carte mère.

Une fonctionnalité importante des cartes mères modernes est la gestion de la température, ainsi que le contrôle des ventilateurs. La carte mère surveille en permanence la température du processeur, éventuellement celle du GPU ou du chipset, et réagit en conséquence. Les réactions ne sont pas très variées, la plus simple est de simplement faire tourner les ventilateurs plus ou moins vite. Mais la carte mère peut aussi réduire la fréquence du processeur en cas de température trop importante, si les ventilateurs ne suivent pas, voire déclencher un arrêt d'urgence si le processeur chauffe trop. Tout cela est regroupé sous le terme de gestion thermique.

La carte mère incorpore un circuit pour la gestion thermique. Concrètement, le circuit varie pas mal selon la carte mère. Il peut faire partie du chipset, précisément dans le southbridge. Mais il se peut aussi qu'il soit séparé des autres, soudé sur la carte mère. Dans ce qui suit, nous partons du principe qu'il s'agit d'un microcontrôleur placé sur la carte mère ou intégré au chipset. Nous l'appellerons le microcontrôleur de surveillance hardware.

Le microcontrôleur de surveillance lit la température et décide comment commander les ventilateurs. La mesure est généralement directe, par l'intermédiaire d'un bus plus ou moins dédié. Par exemple, sur les anciens processeurs Core 2 Duo d'Intel, le bus était appelé le bus PECI. Il peut aussi lire indirectement la température via le chipset de la carte mère. Mais il existe une voie alternative, où le processeur ordonne directement d'augmenter la vitesse des ventilateurs, sans lecture des températures. Pour cela, il est connecté au microcontrôleur de surveillance, avec un fil dédié. Si le processeur met à 1 ce fil dédié, le microcontrôleur de surveillance réagit immédiatement en faisant tourner les ventilateurs plus vite.

Gestion thermique carte mère

Pour l'anecdote, le microcontrôleur de surveillance des PC était initialement un contrôleur de clavier PS/2, le "Keyboard Controller BIOS". Et ce contrôleur disposait déjà d'un fil connecté à l'entrée RESET du processeur, ce qui fait qu'il est techniquement possible de redémarrer son ordinateur par l'intermédiaire du contrôleur de clavier... Vu qu'il y avait déjà un microcontrôleur présent sur la carte mère, il a été décidé de le réutiliser pour des tâches autres, comme la gestion thermique, de la gestion des tensions d'alimentation, des régulateurs de tension, le démarrage (normal ou via réseau), la mise en veille, etc. De nos jours encore, le microcontrôleur de surveillance gère le touchpad.

La mesure de la température : les sondes thermiques

[modifier | modifier le wikicode]

Le microcontrôleur de surveillance contrôle la vitesse du ventilateur en fonction de la température mesurée. Pour cela, le microcontrôleur de surveillance est relié à une ou plusieurs sondes qui mesurent la température. De nos jours, la sonde est intégrée dans le processeur. La sonde thermique du processeur peut être complétée par des sondes sur la carte mère. Il y a aussi des sondes dans le disque dur, le GPU, parfois dans la mémoire RAM, etc. Mais le gros du travail est réalisé par une sonde intra-processeur.

Pour mesurer la température, le processeur incorpore un capteur de température, souvent appelé la diode thermique. Le terme implique qu'il s'agit d'une diode qui est utilisée pour mesurer la température. C'était effectivement le cas auparavant, au tout début de l'informatique. Mais de nos jours, on utilise des senseurs différents, généralement basés sur des transistors configurés de manière à marcher comme une diode. Il faut dire que transistors et diodes sont basés sur des jonctions PN, ce qui fait qu'un transistor peut émuler une diode, mais passons.

L'implémentation originale avec une diode est assez simple à comprendre. La tension aux bornes d'une diode est égale à ceci :

En clair, plus la température est élevée, plus la tension aux bornes de la diode chute. Dit autrement, une diode laisse d'autant mieux passer le courant. Elle chute assez rapidement, au rythme de 2 milli-volts pour chaque degré de plus. Le coefficient K vaut donc . Au final, cela donne une différence d'une centaine de millivolts aux hautes températures. Et cela peut se mesurer assez bien. La tension résultante est ensuite convertie soit en tension, soit en un nombre codé en binaire par un convertisseur analogique-numérique.

Le processeur contient au moins une diode thermique, qui est placée à l'endroit le plus susceptible de chauffer. Il y en a généralement une par cœur, afin de gérer individuellement la température de chaque cœur. Généralement, la température mesurée est convertie en digital/numérique et est mémorisée dans un registre, qui est très régulièrement mis à jour. Le registre peut même être consulté par le logiciel, ce qui explique que de nombreux logiciels de surveillance matérielles permettent de connaitre en temps réel les températures du CPU, du GPU, de la mémoire, du SSD, etc.

Le throttling du CPU et l'arrêt d'urgence en cas de surchauffe

[modifier | modifier le wikicode]

Il arrive que le processeur chauffe trop, alors que les ventilateurs tournent à leur vitesse maximale. Dans ce cas, la solution est de réduire la fréquence du processeur et sa tension, afin qu'il consomme moins et donc chauffe moins. On dit que le processeur throttle, la technique qui réduit immédiatement la fréquence en cas de surchauffe est souvent appelée le throttling. Le throttling peut s'implémenter dans le processeur ou dans le microcontrôleur de surveillance, les deux vont souvent de pair. Mais l'implémentation dans le processeur est la plus simple.

L'implémenter est assez simple. On rappelle que le processeur contient un multiplieur de fréquence configurable qui lui permet de régler en direct sa fréquence, de même qu'il contient un régulateur de tension interne lui aussi configurable. Il y a la même chose sur la carte mère. Les circuits de throttling lisent la mesure provenant de la diode thermique, puis configurent les multiplieurs de fréquence/tension.

Une température trop forte peut endommager le processeur, définitivement. Il y a un seuil de température au-delà duquel le processeur est en danger et peut fondre, prendre feu, ou tout simplement se dégrader. La température que le processeur ne doit en aucun cas dépasser est appelée la température de sureté. Au-delà de la température de sureté, la température est tellement élevée que le processeur peut être endommagé et doit immédiatement être mis en pause.

Pour éviter cela, le processeur est arrêté de force en cas de température trop forte. Dans ce cas, c'est autant le processeur que le microcontrôleur de surveillance qui sont impliqués. Si la température continue d'augmenter malgré le throttling, ou qu'elle est tout simplement trop forte, le processeur déclenche un arrêt d'urgence et le microcontrôleur de surveillance l'exécute. Le processeur a une sortie PROCHOT qui indique que la température de sureté est dépassée, qui est reliée sur une entrée dédiée du microcontrôleur de surveillance. Lorsque le microcontrôleur de surveillance détecte un 1 sur le fil PROCHOT, il déclenche immédiatement un arrêt d'urgence.

Gestion thermique CPU

Le contrôle des ventilateurs

[modifier | modifier le wikicode]

Le ventilateur principal sur une carte mère est le plus souvent du ventilateur du processeur, placé au-dessus du radiateur. Mais il y a parfois des ventilateurs placés dans le boitier ou sur le chipset, qui sont eux aussi commandés par la carte mère. Ils servent à dissiper la chaleur produite par le fonctionnement de l'ordinateur, qui est d'autant plus forte que le processeur travaille.

Autrefois, les ventilateurs tournaient en permanence, tant que l'ordinateur était allumé. Je parle là d'un temps datant d'avant les années 90, et encore. Mais depuis, les ventilateurs ne sont pas utilisés au maximum en permanence. A la place, la vitesse des ventilateurs s'adapte aux besoins. Précisément, ils s'adaptent à la consommation du processeur : plus le processeur fait de calcul, plus il émet de chaleur, plus les ventilateurs tournent rapidement.

Dans le cas le plus simple, le microcontrôleur de surveillance se contente d'allumer ou d'éteindre le ventilateur. Le résultat est un ventilateur qui tourne à plein pot ou s'arrête totalement. Le ventilateur était allumé au-dessus d'un certain seuil, éteint sous ce seuil. Pour simplifier, nous allons appeler cette technique la gestion des ventilateurs à un seuil. Elle n'est pas très efficace, mais c'était suffisant sur des processeurs assez anciens, qui pouvaient se contenter du radiateur au repos, mais devaient allumer le ventilateur quand on leur demandait plus de travail. L'inconvénient est que le confort acoustique est perfectible.

Les autres techniques qui vont suivre contrôlent plus finement la vitesse du ventilateur. Plus la température est importante, plus le ventilateur tourne vite, jusqu’à un certain point. Dans le détail, le microcontrôleur utilise une gestion des ventilateurs à deux seuils. Intuitivement, on sait que la vitesse du ventilateur augmente avec la température, jusqu'à atteindre une vitesse maximale. Mais il est aussi important d'imposer une vitesse minimale au ventilateur, même à de très basses températures. Les raisons à cela sont assez nombreuses, l'une d'entre elle est que cela permet un refroidissement minimal si la sonde de température dysfonctionne. Entre les deux seuils de température précédents, la vitesse augmente avec la température d'une manière relativement proportionnelle, linéaire.

Contrôle des ventilateurs
Exemple de connecteur de ventilateur, avec seulement trois pins (VDD, O Volt, Tension de commande).

Maintenant, on peut se demander comment commander la vitesse des ventilateurs. Les ventilateurs sont connectés à la carte mère via un connecteur, qui contient un fil pour la tension d'alimentation, un autre pour la masse, et un ou deux fils de contrôle. Le premier envoie au ventilateur une tension de commande qui indique à quelle vitesse il doit tourner. Le second, le fil de monitoring, indique à la carte mère à quel vitesse le ventilateur tourne. Sur le fil de monitoring, le ventilateur envoie un signal à une certaine fréquence, qui correspond à la fréquence de rotation du ventilateur.

Il y a plusieurs méthodes concernant la tension de commande. Avec la plus simple, la tension de commande est proportionnelle à la vitesse de rotation. Prenons par exemple un ventilateur pouvant tourner de 0 à 1000 RPM (rotations par minute). Si la tension de commande est de 5 volts, alors une tension de 0 volts donnera 0 RPM, la tension maximale de 5V donnera 1000 RPM, une tension de 2 volts donnera 400 RPM, une tension de 3 volts donnera 600 RPM, etc. Le problème est que contrôler finement une tension est quelque peu compliqué, car cela demande d'utiliser des circuits analogiques, qui sont complexes à fabriquer.

Une méthode plus utilisée actuellement est d'utiliser un signal modulé par PWM, un terme barbare dont la signification est pourtant assez simple. Le ventilateur est contrôlé par un signal d'horloge d'une fréquence de 25 KHz. Le moteur du ventilateur est allumé quand le signal d'horloge est à 1, éteint quand il est à 0. Avec l'inertie, le ventilateur continue à tourner quand le signal d'horloge est à 0 et il ralentit assez peu. Le ventilateur accélère quand le signal d'horloge est à 1, mais ralentit quand il est à 0. La vitesse instantanée du ventilateur fluctue donc lors d'une période, mais sa vitesse moyenne est bien définie.

Intuitivement, la signal d'horloge est à 1 la moitié du temps, à 0 l'autre moitié. La vitesse moyenne est donc la moitié de la vitesse maximale. Mais ce n'est pas le cas. L'idée est alors de trifouiller le signal d'horloge, en modulant la durée où il est à 1, mais sans changer la fréquence. En clair, on module le rapport cyclique (duty cycle), le pourcentage d'une période où le signal d'horloge est à 1. Plus le rapport cyclique est grand, plus le ventilateur tourne vite. Pour le dire en une phrase : la tension de commande est un signal d'une fréquence de 25 KHz, dont le rapport cyclique contrôle la vitesse du ventilateur.

Signaux d'horloge asymétriques

Typiquement, le rapport cyclique va de 30% à 100%. A 100%, le ventilateur tourne en permanence, le signal d'horloge devient un signal constant, le ventilateur va a sa vitesse maximale. En-dessous de 30%, le comportement dépend fortement de la carte mère et du mécanisme de contrôle du ventilateur. Le ventilateur est généralement éteint, ou alors il ne peut pas tourner à moins de 30% de va vitesse maximale.

Les circuits de gestion de l'alimentation

[modifier | modifier le wikicode]

Une partie importante de la carte mère est liée à l'alimentation électrique. Le sous-système d'alimentation reçoit de l'électricité de la part du connecteur d'alimentation, et génère les tensions demandées par le processeur, la mémoire, etc. Le sous-système d'alimentation est très complexe et comprend un mélange de composants analogiques, numériques, et d'autres purement électriques. Mais il est intéressant d'en parler. Ses fonctions sont très diverses : alimenter les composants avec les tensions adéquates, vérifier la stabilité de l'alimentation, détecter les problèmes de tension, gérer l'allumage et la mise en veille, etc. En cas de problème, il peut déclencher un reset hardware.

La génération des tensions pour chaque composant

[modifier | modifier le wikicode]

Le processeur, la mémoire, et les différents circuits sur la carte mère, sont alimentés à travers celle-ci. Mais ils ont chacun besoin d'une tension bien précise, qui varie d'un composant à l'autre. Par exemple, la tension d'alimentation du processeur n'est pas celle de la mémoire RAM, qui n'est pas non plus celle du chipset. Il y a donc des composants qui transforment la tension fournie par l’alimentation en tensions adéquates.

La carte mère est alimentée par une tension de base, qui provient de la batterie sur un ordinateur portable/smartphone, du bloc d'alimentation pour un PC fixe ou serveur. Les PC fixes suivent le format ATX, qui précise que le bloc d'alimentation doit fournir trois tensions de base : une de 3,3V, une autre de 5V et une autre de 12V. Les tensions de base passe alors à travers un ou plusieurs régulateurs de tension, qui fournissent chacun une tension de sortie à la bonne valeur pour soit le processeur, soit la mémoire, soit un autre circuit.

Voltage Regulator connections-en
Deux régulateurs de tension.

Un régulateur de tension est un circuit qui a au moins trois broches : une entrée pour la tension de base, une sortie pour la tension voulue, et une troisième entrée qui ne nous intéresse pas ici. Il existe des régulateurs qui fournissent une tension de 3,3V, d'autres qui fournissent du 1V, d'autres du 0.5V, d'autres du 2V, etc. Chaque régulateur est conçu pour sortir une tension bien précise. On ne peut pas, en théorie, configurer un régulateur de tension pour choisir sa tension de sortie.

Les ordinateurs modernes utilisent des régulateurs de tension améliorés, appelés des modules de régulation de la tension. Dans ce qui suit, nous utiliserons l'abréviation VRM, pour Voltage Regulator Modules. Il s'agit de composants très complexes, avec de nombreuses fonctionnalités.

Une fonctionnalité importante des VRM est que l'on peut les activer ou les désactiver. Concrètement, cela permet d'allumer ou couper une tension électrique suivant les besoins. C'est utilisé lors de l'allumage de l'ordinateur, pour l'éteindre, le redémarrer, pour mettre certains composants en veille, etc. L'activation/désactivation d'un VRM se fait via une entrée de commande nommée Enable. Mettez un 1 sur cette entrée pour activer le VRM, un 0 pour le désactiver.

Il existe des smart VRM, qui implémentent de nombreuses fonctionnalités. Par exemple, il est possible de configurer la tension de sortie voulue parmi un choix de quelques tensions prédéterminées. Ce qui peut servir pour modifier la tension d'alimentation d'un composant à la volée, chose utile pour mettre un composant en veille. De plus, le VRM surveille en permanence sa température, la tension de sortie, le courant qui le traverse, et d'autres données importantes. Le microcontrôleur de surveillance peut accéder en temps réel à ces données, afin de détecter tout problème. Il peut ainsi détecter une défaillance de la tension de sortie du VRM, ou détecter qu'un VRM surchauffe. Pour résumer, les smart VRM peuvent être configurés et/ou surveillés.

Pour cela, le smart VRM communique avec l'extérieur par l'intermédiaire d'un bus dédié. Le bus en question est le bus Power Management Bus, une variante du bus System Management Bus (SMBus), qui lui-même est une variante du bus i²c. Il s'agit d'un bus très simple, qui demande juste deux fils pour connecter le VRM à l’extérieur. Les smart VRM sont généralement connectées au microcontroleur de surveillance, parfois aux chipset, parfois les deux.

La séquence d'allumage des VRM

[modifier | modifier le wikicode]

Un point important est que chaque composant sur la carte mère utilise souvent plusieurs tensions d'alimentation. Par exemple, le processeur a souvent besoin de plusieurs tensions d'alimentation. Idem pour les cartes graphiques, qui demandent souvent d'avoir à leur disposition d'avoir plusieurs tensions distinctes.

Lors du démarrage, le composant requiert d'activer ces tensions dans un ordre bien précis. Expliquer pourquoi est compliqué, mais disons que sans cela, le composant le fonctionne tout simplement pas. Il y a donc un ordre d'allumage des tensions, décrit par une séquence d’amorçage des VRM. Et la même chose a lieu lors de la mise en veille : il faut désactiver les VRMs dans le bon ordre, souvent l'ordre inverse du démarrage. La sortie de mise en veille demande de faire comme lors de l’allumage, à quelques détails près.

L'ordre en question est géré via une fonctionnalité importante des VRM : on peut les activer ou les désactiver à la demande. Pour cela, ils disposent d'une entrée de commande qui active ou désactive le régulateur de tension. Si on envoie un 1 sur cette entrée, le VRM s'active. A l'inverse, un 0 sur cette entrée désactive le VRM. L'activation des tensions est le rôle d'un circuit spécialisé relié à chaque VRM, il a une sortie dédiée à chaque VRM, sur laquelle il envoie un 0 ou un 1 pour l'activer au besoin. Nous l’appellerons le sequencing chip. Il s'agit parfois d'un circuit dédié, mais il est souvent intégré directement dans le microcontrôleur de surveillance hardware.

Mais vous vous posez sans doute la question suivante : comment est alimenté le microcontrôleur de surveillance ? La réponse est que le microcontrôleur est relié à une tension de 3,3V qui est toujours active, qui provient du connecteur d'alimentation. Dès que vous branchez votre PC sur secteur, le 3,3 volts va alimenter le microcontrôleur de surveillance. Même si vous laissez votre ordinateur éteint, le microcontrôleur de surveillance est actif tant que le secteur est branché. Par contre, dès que vous appuyez sur le bouton d'alimentation, le microcontrôleur de surveillance démarre la séquence d’amorçage des VRM. D'ailleurs, le microcontrôleur de surveillance est directement connecté au bouton d'alimentation, il lui réserve une entrée dédiée.

Alimentation du microcontroleur de surveillance et liaison aux VRM

Il faut noter que le microcontrôleur de surveillance n'est pas le seul alimenté ainsi. En réalité, une partie du chipset de la carte mère et quelques circuits annexes le sont aussi. Mais nous verrons cela plus tard.

La stabilité de l'alimentation au démarrage

[modifier | modifier le wikicode]

Lors d'un démarrage ou d'un redémarrage, le processeur est allumé par un signal RESET. Il y a une petite différence entre l'allumage et le redémarrage, qui tient dans la manière dont la tension d'alimentation et l'horloge sont gérées. Lors d'un reset hardware, le processeur reste alimenté, il n'est pas coupé. Il réinitialise ses registres, remet à zéro pas mal de circuits, mais l'alimentation reste maintenue. Lors du démarrage, ce n'est pas le cas : l'alimentation est allumée, la tension d'alimentation passe de 0 à 12 ou 5 volts. Au passage, si vous débranchez votre ordinateur avant de le rebrancher, ce n'est pas un reset hardware, car vous éteignez l'ordinateur avant de le relancer. L'alimentation est coupée entre-temps.

Lors du démarrage, avant de générer un signal RESET, il faut attendre que la tension d'alimentation se stabilise, de même que le signal d'horloge. Si les tensions d'alimentation ne marchent pas comme prévu, le démarrage n'a pas lieu pour éviter d'endommager le processeur, idem pour le signal d'horloge. Pour cela, la carte mère contient deux circuits séparés. Le Power-on reset, temporise en attendant que la tension d'alimentation se stabilise.

Le Power-on reset vérifie les tensions 12 V, 5 V et 3,3 V, et n'autorise le RESET du processeur que si celles-ci sont stables et à la bonne valeur. Pour cela, il est relié au microcontrôleur de surveillance, sur une entrée de ce dernier. Le microcontrôleur de surveillance ne démarre la séquence d'allumage des VRM que s'il reçoit le signal POR. Ainsi que d'autres signaux, comme celui de l'oscillator startup timer.

Les défaillances de l'alimentation

[modifier | modifier le wikicode]

Une fois l'ordinateur allumé, le processeur fonctionne. On s'attend à ce que la tension et la fréquence soient stables, mais il est parfaitement possible que la tension soit soudainement déstabilisée, par exemple lorsqu'on débranche la prise, une coupure de courant, un problème matériel avec les régulateurs de tension, des condensateurs de la carte mère qui fondent, etc.

Un circuit appelé le Low-voltage detect surveille en permanence la tension d'alimentation, pour détecter de telles défaillances et générer un arrêt d'urgence. Il est souvent fusionné avec le Power-on reset en un seul circuit. Rien d'étonnant à cela : les deux sont reliés à la tension d'alimentation et vérifient sa stabilité. Typiquement, le Low-voltage detect permet de détecter si la tension d'alimentation descend sous un seuil pendant un certain temps, typiquement sous 2-3 volts pendant quelques centaines de millisecondes.

Le Low-voltage detect est implémenté différemment entre un PC moderne et les cartes mères hors-PC. Sur PC, c'est rarement un circuit à part, il est intégré dans le microcontrôleur de surveillance. Et c'est encore plus vrai si la carte mère utilise des smart VRM. Il suffit alors de connecter des smart VRM au microcontrôleur de surveillance, via un bus PMBUS, pour que celui-ci puisse vérifier les tensions de chaque VRM en temps réel. Mais sur les cartes mères hors PC, le Low-voltage detect est souvent un circuit à part. Il faut dire qu'elles n'incorporent pas forcément de microcontrôleur de surveillance, ni de smart VRM, car leur système d'alimentation est beaucoup plus simple. Aussi, un circuit spécialisé est utilisé à la place.

La gestion d'une défaillance de l'alimentation est aussi gérée différemment entre un PC et un microcontrôleur. La raison est qu'un PC peut fonctionner durant quelques millisecondes, grâce à la présence de condensateurs sur la carte mère. Ils maintiennent la tension d'alimentation pendant quelques millisecondes, ce qui lui laisse le temps de faire quelques sauvegardes mineures et d'éteindre l'ordinateur proprement. Un arrêt d'urgence avec l'entrée NMI est donc acceptable, si les condensateurs sont dimensionnés pour.

Par contre, les microcontrôleurs ne sont pas dans ce cas-là, surtout dans les systèmes alimentés sur batterie. Là, le problème n'est pas tellement une coupure soudaine de l'alimentation, mais une baisse progressive de la tension, au fur et à mesure que la batterie se vide. Typiquement, si la batterie se vide progressivement, il arrive un moment où la tension chute trop bas, et reste basse tant que la batterie n'est pas rechargée. Dans ce cas, le processeur n'est pas éteint, mais maintenu en état de RESET tant que la tension est trop faible. Dès que la tension remonte, le processeur redémarre. On parle alors de brown-out reset.

Les domaines de puissance et le standby domain

[modifier | modifier le wikicode]

Le microcontrôleur de surveillance est alimenté par une tension d'alimentation dite de standby, qui est présente dès que l'ordinateur est branché au secteur, même éteint. Et c'est aussi le cas d'une partie du chipset, du contrôleur USB et du contrôleur Ethernet. Ils sont tous partiellement alimentés même ordinateur éteint. C'est grâce à ça qu'un ordinateur peut être allumé via le réseau. L'ensemble des composants alimentés même ordinateur éteint est appelé le Stand By Domain.

Standby domain

Le Standby Domain s'oppose aux circuits qui ne sont allumés qu'une fois qu'on a démarré l'ordinateur. Il est parfois appelé le Wakeup Domain. Mais ce dernier n'est pas uniforme. En réalité, il est lui-même séparé en plusieurs domaines de puissance, chacun relié à un VRM et alimenté par une tension d'alimentation précise. Par exemple, la mémoire RAM n'est pas alimentée par le même VRM que le processeur. Il est ainsi possible d'éteindre le processeur tout en maintenant la RAM allumée. C'est d'ailleurs ce qui est fait lors de la mise en veille normale de l'ordinateur : le CPU est éteint, mais la RAM continue à être alimentée pour ne pas perdre ses données.

Les différents power domains d'une carte mère : CPU, RAM, standby et autres

Outre le chipset et le microcontroleur de surveillance, le Standby Domain contient des tas de bus spécialisés dans la gestion thermique ou la gestion de l'alimentation. Ils sont reliés au microcontroleur de surveillance, à la glue logic et au chipset, ainsi qu'aux VRM, aux ventilateurs, aux sondes de température, etc. Nous avons mentionné le bus PECI d'Intel qui permet au microcontrôleur de surveillance et au chipset de lire la température du processeur. Mais ce n'est pas le seul.

L'un des tout premier bus utilisé dans cet optique était le bus SMBUS, pour System Management Bus, dont l'objectif initial était d'interconnecter un grand nombre de composants peu rapides, soudés à la carte mère. Il est assez spécialisé dans la gestion thermique et de l’alimentation, mais est aussi utilisé pour gérer les LEDs RGB et quelques autres fonctionnalités dans le genre. Le PMBUS est utilisé pour commander les smart VRM est une version améliorée de ce SMBUS.

Les Power States

[modifier | modifier le wikicode]

Pour rappel, un ordinateur peut être dans plusieurs états de fonctionnement : en veille, allumé, éteint. Dans chaque état, certains composants sont allumés, d'autres éteints. Dans le détail, ces états de fonctionnement sont appelés des Power State. Sur les processeurs x86, ils sont normalisés par la norme ACPI. Elle définit plusieurs Power State distincts.

  • S0 : ordinateur allumé, en fonctionnement ;
  • S0 amélioré, facultatif : forme spécifique de veille ;
  • S1, S2, S3 : veille normale ;
  • S4 : veille prolongée ;
  • S5 Standby : secteur branché, ordinateur éteint ;
  • G3 Mechanical OFF : sécteur débranché.

En mode G3, le secteur est débranché, rien n'est alimenté. En mode S5, le standby domain est alimenté, pas le reste. En mode S0, tout est alimenté, ou presque.

Les modes S1, S2 et S3 sont aussi appelés la veille suspend to RAM, le nom est assez transparent. La RAM est alimentée, mais pas le processeur. Le standby domain est alimenté et c'est pour ça qu'un ordinateur en veille se réveille quand on appuie sur une touche du clavier. Les trois ne sont pas équivalents, mais les différences sont mineures, au point que seul le S3 est réellement utilisé de nos jours.

Le mode S4 veille prolongée est aussi appelé le mode suspend to disk. Avec lui, l'ordinateur est quasiment éteint, même la RAM est désactivée. La RAM de l'ordinateur est recopiée dans un fichier sur le disque dur, pour être restauré en RAM lors de la sortie de mise en veille. Évidemment, la sauvegarde de la RAM sur le disque dur est assez lente, ce qui fait que la mise en veille n'est pas immédiate. Si les premières implémentations utilisaient le BIOS, ce n'est plus le cas. Il peut en théorie être implémenté en logiciel, mais il est géré en partie par le système d'exploitation et le matériel, que ce soit sur Linux et Windows. Tant que le matériel est intégralement compatible avec la norme ACPI, la mise en hibernation est possible.

Les transitions entre ces différents états/power state, sont illustrées ci-dessous. Elles sont gérées en coopération par le processeur, le chipset et le microcontroleur de périphérique. Lors de la mise en veille, le processeur sauvegarde son état de manière à reprendre là où il en était. Puis, il prévient le microcontrôleur de surveillance et le chipset pour prévenir que l'ordinateur peut être mis en veille. Là, les tensions d'alimentation du CPU sont coupées. Lors de la sortie de veille, les tensions sont rallumées par le microcontrôleur de surveillance, puis un signal RESET est émis.

Power state x86

Il faut noter que chaque périphérique peut aussi avoir des power state similaires. Par exemple, un SSD peut avoir un mode basse consommation, qui est utilisé si le SSD n'est pas utilisé mais que l'ordinateur est allumé. Typiquement, les composants/périphériques disposent de quatre états : un mode éteint, un mode veille, un mode basse consommation et un mode fonctionnement normal. Le composant peut être mis en veille, où il consomme un petit peu mais est plus rapide à réactiver qu'un SSD éteint. Il peut aussi fonctionner en mode basse consommation, où les performances sont un peu diminuées et certaines fonctionnalités ne sont pas disponibles.

La gestion logicielle des Power States et de l'économie d'énergie

[modifier | modifier le wikicode]

Autrefois, la transition entre ces états était gérée par le microcontrôleur de surveillance, qui répondait aux boutons ON/OFF et celui de mise en veille s'il existait. Mais le logiciel ne pouvait pas éteindre l'ordinateur de lui-même, encore moins le mettre en veille. A vrai dire, la mise en veille normale n'existait pas forcément, seule la mise en veille prolongée existait et était gérée purement en logiciel. Il existait des mises en veille basiques, qui se résumaient à éteindre l'écran et le disque dur, mais le CPU fonctionnait normalement, sa fréquence n'était pas stoppée ni réduite. LA gestion des power state des périphériques n'était pas gérée du tout.

Windows 95 ne pouvait pas éteindre l’ordinateur, il fallait appuyer sur le bouton ON/OFF.

L'apparition du standard APM (Advanced Power Management) a permis au logiciel de gérer les power state, mais aussi d'autres fonctionnalités d'économie d'énergie. Le BIOS gérait tout ce qui avait trait à la consommation d'énergie, il était au centre du standard. Il pouvait désactiver des périphériques, les mettre en veille de manière sélective, les mettre en état basse consommation, etc. Le logiciel pouvait envoyer des ordres au BIOS pour cela, en passant par l'intermédiaire d'in pilote de périphérique dédié, appelé driver APM. Le standard définissait comment le BIOS et le logiciel devaient communiquer entre eux.

APM

De nos jours, le standard APM a été remplacé par le standard ACPI (Advanced Configuration and Power Interface). Ce standard ne gère pas que l'économie d'énergie, mais aussi des fonctionnalités comme la découverte du matériel (détecter le matériel installé dans le PC au démarrage). Il ne se base plus du tout sur le BIOS, contrairement au format APM, mais laisse le système d'exploitation gérer la consommation énergétique du PC.

Au passage, les temps de mise en veille sont gérés par des timers dédiés, appelés timers d'inactivité. Vous savez sans doute qu'il est possible de gérer combien de temps doit d'écouler avant d'éteindre l'écran, de mettre l'ordinateur en veille. Hé bien ces temps sont gérés par des timers dédiés, faisant partie de la norme ACPI. Dès que Windows détecte un ordinateur inactif, il initialise le timer avec la durée configurée et laisse le timer faire son travail. Le timer est réinitialisé en cas d'activité. Si le timer déborde, il génère un signal dit d'interruption à destination du processeur, qui réagit immédiatement et met en veille l'ordinateur.

La glue logic et les circuits annexes

[modifier | modifier le wikicode]

Pour résumer, une carte mère intègre de nombreux circuits très différents. Mais avec la loi de Moore, de nombreux circuits distincts ont pu être fusionnés en un seul circuit. Par exemple, le chipset regroupe de nombreux circuits autrefois distincts, mais nous détaillerons cela dans le chapitre sur les contrôleurs de périphériques. De même, le microcontrôleur de surveillance a beaucoup de fonctions différentes : la gestion de l'alimentation au sens large, la gestion thermique, et bien d'autres que nous n'avons pas encore abordées. Et toutes ces fonctions étaient autrefois le fait de plusieurs circuits séparés.

Une carte mère contient donc deux composants principaux : le microcontroleur de surveillance et le chipset. Et à cela il faut ajouter tous les bus électroniques, qui relient le chipset, le microcontroleur de surveillance, les connecteurs, le processeur, la RAM, le disque dur, les ports PCI-Express, et tout ce qui est sur la carte mère. Mais à tout cela, il faut ajouter un dernier composant : la glue logic.

La glue logic regroupe tout le reste, à l'exception de composants analogiques/électriques comme des condensateurs de découplage et autres. Il s'agit d'un ensemble de circuits qui n'a pas de fonction précise et dépend fortement de la carte mère. Formellement, son rôle est de servir à coller ensemble des circuits séparés. La carte mère contient un chipset, le µcontroleur de surveillance, le CPU, la RAM, et d'autres composants qui sont fabriqués séparément, et qui doivent être "collés ensemble" pour obtenir la carte mère finale. La glue logic s'occupe de ce collage, de cet interfaçage.

Elle s'occupe majoritairement de la gestion thermique et des tensions, mais pour des fonctions qui ne sont pas prises en charge par le chipset ou le µcontroleur. Mais ce n'est pas sa seule fonction. Il est difficile de faire des généralités sur la glue logic, car elle varie énormément d'une carte mère à l'autre. Elle est souvent implémentée avec des FPGA, sur les carte mères actuelles. Mais les anciennes cartes mères utilisaient des composants séparés. La glue logic prenait donc beaucoup de place, ce qui n'est plus trop le cas maintenant.

Les chips de sécurité

[modifier | modifier le wikicode]

Depuis la décennie 2010, les cartes mères incorporent des chips spécialisés dans la sécurité informatique. Vous avez peut-être entendu parler des puces TPM, rendues nécessaires pour passer à Windows 11, de l'Intel Management Engine, ou de son équivalent chez AMD. De telles puces ont une utilité mal compris par le grand public, et souvent décriée pour, paradoxalement, des raisons de sécurité. Quelques-uns les ont accusées de contenir des backdoors.

Le Trusted Platform Module

[modifier | modifier le wikicode]

Le Trusted Platform Module, ou TPM, est ce qui s'appelle un processeur cryptographique sécurisé. Concrètement, il s'agit d'un circuit spécialisé dans tout ce qui est chiffrement. Il a deux rôles principaux : générer et stocker des clés de chiffrement.

Il peut par exemple générer des nombres aléatoires, des clés RSA, etc. En théorie, un processeur cryptographique peut aussi servir d'accélérateur cryptographique, à savoir que le processeur CPU lui délègue les calculs cryptographiques. Il peut alors exécuter des fonctions de hachage cryptographiques comme SHA-1, etc. Disons qu'il peut être utilisé dès qu'il faut crypter ou décrypter des données

Il peut aussi mémoriser certaines clés cryptographiques du système, de manière persistante, dans une mémoire EEPROM/FLASH dédiée, intégrée au TPM. Elles ne sont alors accessibles ni par le système d'exploitation, ni par l'utilisateur, ni par qui ou quoi que ce soit d'autres que le TPM.

Outre le stockage des clés à l'intérieur du TPM, le TPM peut protéger une portion de la mémoire RAM afin d'y stocker des clés de cryptage/décryptage importantes. La portion de mémoire est appelée une enclave mémoire sécurisée. Elle est accessible uniquement par le TPM, mais est inaccessible par l'OS ou l'utilisateur, ou alors seulement partiellement accessible. Cela permet de sécuriser certaines clés cryptographiques pendant qu'elles sont utilisées.

TPM 1.2, diagramme.

Les utilisations principales sont les trois suivantes le chiffrement des disques durs, la sécurité lors du démarrage et les DRMs. Le TPM sert en premier lieu pour encrypter les disques durs avec Bitlocker sous Windows ou son équivalent sous Linux/mac. Les clés utilisées pour crypter et décrypter un disque dur encrypté sont stockées dans le TPM et sont aussi utilisées à travers une enclave matérielle. Une utilisation plus controversée est celle des techniques de Secure Boot, qui vise à protéger l’ordinateur dès le démarrage, pour stopper net les malwares qui infectent le BIOS ou les secteurs de boot.

Mais le TPM peut aussi être utilisé pour implémenter le trusted computing, un ensemble de techniques qui visent à identifier un ordinateur. Par identifier, on veut dire que le trusted computing attribue une identité à chaque ordinateur, afin de l'identifier parmi tous les autres. L'intention est d'implémenter des techniques permettant de ne décrypter des données que sur des ordinateurs bien précis et pas ailleurs. Le trusted computing fournit aussi des procédés afin d'éviter toute usurpation d'identité entre ordinateurs. Le tout se fait par des procédés matériels auxquels qu'aucun logiciel ne peut altérer, pas même le système d'exploitation, et pas même l'utilisateur lui-même (d'où certaines controverses). Elles servent surtout à gérer les DRM, des protections logicielles qui visent à empêcher le piratage de musique, jeux vidéos, films et autres.

Les techniques en question attribuent au matériel une clé de sécurité inconnue de l'OS et de l'utilisateur. A sa création, chaque puce TPM se voit attribuer une clé de sécurité RSA de 2048 bits, qu'il est impossible de changer, et qui est différente d'une puce à l'autre. Avant de poursuivre, rappelons la différence entre clé privée et publique. L'algorithme RSA permet à deux entités de communiquer entre elles en s'échangeant des messages cryptés. Les messages sont encryptés avec une clé publique et décryptés avec une clé privée. La clé dont il est question pour le TPM est une clé privée. Elle permet d'identifier l'ordinateur de manière unique, ce qui sert pour les DRM ou pour authentifier l'ordinateur pour un quelconque login.

Parmi les fonctionnalités du trusted computing, voici quelques exemples. La technique de Sealed storage permet de lire des données encryptées uniquement sur une machine particulière. Les données sont encryptées par le TPM, qui ajoute des informations sur la configuration dans la clé de cryptage, dont sa clé TPM. La technique est utilisée pour les DRM. La technique de Remote attestation permet à un autre ordinateur d'être au courant des modifications effectuées sur l'ordinateur considéré.

Le TPM est parfois une puce placée sur la carte mère, mais ce n'est pas le cas sur les PC commerciaux. A la place, le TPM est soit intégré au chipset, soit intégré au processeur. Il en sera de même pour les circuits de sécurité que nous allons voir par la suite, qui font partie du CPU ou du chipset.

Le Management Engine d'Intel et l'AMD Platform Security Processor

[modifier | modifier le wikicode]

L'Intel Management Engine (ME) et l'AMD Platform Security Processor (PSP) sont des microcontrôleurs intégrés au chipset de la carte mère. Ils ont accès à tout le matériel de l'ordinateur, d'où sa place dans le chipset. Ils sont actifs en permanence, même si l'ordinateur est éteint, ce qui fait qu'ils sont dans le standby domain. Ils ont des fonctions très diverses, mais ils s'occupent de l'initialisation du chipset et du démarrage de l'ordinateur, de l'initialisation des périphériques, mais aussi de fonctionnalités de sécurité.

En premier lieu, il agit comme un processeur cryptographique sécurisé, à savoir qu'il incorpore des accélérateurs cryptographiques (des circuits de calcul capables de crypter/décrypter des données avec des algorithmes comme AES, RSA, autres). Chez Intel, le tout est appelé l'OCS (Offload and Cryptography Subsystem), et est couplé à un système de stockage sécurisé de clés, ainsi qu'un contrôleur DMA (un circuit qui sert pour les échanges entre OCS et mémoire RAM). Le TPM vu précédemment est souvent intégré dans le ME/PSP. Il gère aussi d'autres fonctionnalités, comme des protections lors du démarrage de l’ordinateur, comme le secure boot, ou l'Intel® Boot Guard. Des DRM gérés en hardware sont aussi disponibles.

L'Intel ME était initialement basé sur un processeur ARC core et utilisait le système d'exploitation temps réel ThreadX. De nos jours, depuis la version 11 du ME, il utilise un processeur Intel Quark 32 bits et fait tourner le système d'exploitation MINIX 3. Il est possible pour le processeur principal (CPU) de communiquer avec le ME, via la technologie Host Embedded Controller Interface (HECI). Mais la communication est surtout utilisée pour la gestion thermique et d'alimentation.


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 : 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Bus

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 | modifier le wikicode]

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 | modifier le wikicode]

La grande majorité des bus actuellement utilisés 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 | modifier le wikicode]

Une minorité de bus se passent complètement de signal d'horloge, et ont un protocole conçu pour : ce sont les bus asynchrones. Les bus asynchrones utilisent des protocoles de communication spécialisés. Les bus asynchrones permettent à 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, généralement grâce à un bit dédié sur le bus, souvent appelé le bit REQ. Le récepteur réceptionne alors la donnée et indique qu'il a pris en compte les données envoyées en envoyant un bit ACK. Cette synchronisation se fait grâce à des fils spécialisés du bus de commande, qui transmettent des bits particuliers.

Exemple d'écriture sur un bus asynchrone

De tels bus sont donc très adaptés pour transmettre des informations sur de longues distances (plusieurs centimètres ou plus). La raison est qu'à 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 est dominée par le temps de propagation de l'horloge dans le fil. Les bus asynchrones n'ont pas ce genre de problèmes.

La largeur du bus

[modifier | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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.


Avant de passer aux liaisons point à point et aux bus, nous allons voir comment un bit est codé sur un bus. Vous pensez certainement que l'encodage des bits est le même sur un bus que dans un processeur ou une mémoire. Mais ce n'est pas le cas. Il existe des encodages spécifiques pour les bus.

Les codes en ligne : le codage des bits sur une ligne de transmission

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

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.

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. Nous détaillerons cela à la fin de ce chapitre.

L'ordre d'envoi des bits sur une liaison série

[modifier | modifier le wikicode]

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).

Les encodages de bus

[modifier | modifier le wikicode]

Les encodages de bus encodent des données pour les envoyer sur un bus. Ils s'appliquent aux bus parallèles, à savoir ceux qui transmettent plusieurs bits en même temps sur des fils séparés. Un de leurs nombreux objectifs est de réduire la consommation d'énergie des bus parallèles. La consommation d'énergie dépend en effet de deux choses : de l'énergie utilisée pour faire passer un fil de 0 à 1 (ou de 1 à 0), et du nombre de fois qu'il faut inverser un bit par secondes. Le premier paramètre est fixé une fois pour toute, mais le second ne l'est pas et peut être modifié par des encodages spécifiques.

Le retour du code Gray

[modifier | modifier le wikicode]

Une première idée serait d'utiliser des encodages qui minimisent le passage d'un 0 à un 1 et inversement. Dans les premiers chapitres du cours, nous avions parlé du code Gray, qui est spécifiquement conçu dans cet optique. Rappelons qu'avec des entiers codés en code de Gray, deux entiers consécutifs ne différent que d'un seul bit. Ainsi, quand on passe d'un entier au suivant, seul un bit change de valeur. Une telle propriété est très utile pour les compteurs, mais pas vraiment pour les bus : on envoie rarement des données consécutives sur un bus.

Il y a cependant un contre-exemple assez flagrant : le bus mémoire ! En effet, les processeurs ont tendance à accéder à des données consécutives quand ils font des lectures/écritures successives. Nous verrons pourquoi dans les chapitres sur le processeurs, mais sachez que c'est lié au fait que les programmeurs utilisent beaucoup les tableaux, une structure de données qui regroupe des données dans des adresses consécutives. Les programmeurs parcourent ces tableaux dans le sens croissant/décroissant des adresses, c'est une opération très fréquente. Une autre raison est que les transferts entre RAM et mémoire cache se font par paquets de grande taille, environ 64 octets, voire plus, qui sont composés d'adresses consécutives. De ce fait, utiliser le code Gray pour encoder les adresses sur un bus mémoire est l'idéal.

Maintenant, cela demande d'ajouter des circuits pour convertir un nombre du binaire vers le code Gray et inversement. Et il faut que l'économie d'énergie liée au code Gray ne soit pas supprimée par l'énergie consommée par ces circuits. Heureusement, de tels circuits sont basés sur des portes XOR. Jugez plutôt. On voit que pour convertir un nombre du binaire vers le code Gray, il suffit de faire un XOR entre chaque bit et le suivant. Pour faire la conversion inverse, c'est la même chose, sauf qu'on fait un XOR entre le bit à convertir et le précédent.

Circuit de conversion du binaire vers le code Gray.
Circuit de conversion du code Gray vers le binaire.

L'encodage à adressage séquentiel

[modifier | modifier le wikicode]

Plus haut, on a dit que l'on peut profiter du fait que des accès mémoire consécutifs se font à des adresses consécutives. L'utilisation du code Gray est une première solution, mais il y a une autre solution équivalente. L'idée est d'ajouter un fil sur le bus mémoire, qui indique si l'adresse envoyée est la suivante. Le fil, nommé INC, indique qu'il faut incrémenter l'adresse envoyée juste avant sur le bus. Si on envoie l'adresse suivante, alors on met ce fil à 1, mais on n'envoie pas l'adresse suivante sur le bus mémoire. Si l'adresse envoyée est totalement différente, ce fil reste à 0 et l'adresse est effectivemen envoyée sur le bus mémoire. On parle de Sequential addressing ou encore de T0 codes.

La mémoire de l'autre côté du bus doit incorporer quelques circuits pour gérer un tel encodage. Premièrement, il faut stocker l'adresse reçue dans un registre à chaque accès mémoire. De plus, il faut aussi ajouter un circuit incrémenteur qui incrémente l'adresse si le fil INC est mis à 1. Le registre et l'incrémenteur sont placés juste entre le bus et le reste de la mémoire, il ne faut rien de plus, si ce n'est quelques multiplexeurs.

Le codage à inversion de bus

[modifier | modifier le wikicode]

Le codage à inversion de bus (bus inversion encoding, BIE) est un codage généraliste qui s'applique à tous les bus, pas seulement au bus mémoire. L'idée est que pour transmettre une donnée, on peut transmettre soit la donnée telle quelle, soit son complément obtenu en inversant tous les bits. Pour minimiser le nombre de bits qui changent entre deux données consécutives envoyées sur le bus, l'idée est de comparer quelle est le cas qui change le plus de bits.

Par exemple, imaginons que j'envoie la donnée 0000 1111 suivie de la donnée 1110 1000. Les deux données n'ont que deux bits de commun, 6 de différents. Par contre, si je prends le complément de 1110 1000, j'obtiens 0001 0111. Il y a maintenant 6 bits de commun et 2 de différents. Envoyer la donnée inversée est donc plus efficace : moins de bits sont à changer. Mais cela ne marche pas toujours : parfois, ne pas inverser la donnée est une meilleure idée. Par exemple, si j'envoie la donnée 0000 1111 suivie de la donnée 0001 1110, inverse les bits donnera un plus mauvais résultat.

Pour implémenter cette technique, le récepteur doit savoir si la donnée a été inversée avant d'être envoyée. Pour cela, on rajoute un fil/bit sur le bus qui indique si la donnée a été inversée ou non. Le bit associé, appelé le bit INV, vaut 1 pour une donnée inversée, 0 sinon.

Bus inversion encoding.

L'implémentation de ce code est assez simple, du côté du récepteur : il suffit d'ajouter un inverseur commandable dans le récepteur, qui est commandé par le bit INV du bus. Et pour rappel, un inverseur commandable est un circuit qui fait un XOR avec le bit reçu.

Decodeur de BIE du côté récepteur.

Du côté de l'émetteur, il faut ajouter là aussi un inverseur commandable, mais sa commande est plus compliquée. Le circuit doit détecter s'il est rentable ou non d'inverser la donnée. Pour cela, il faut ajouter un registre pour contenir la donnée envoyé juste avant. La donnée à envoyer est comparé à ce registre, pour déterminer quels sont les bits différents, en faisant un XOR entre les deux. Le résultat passe ensuite dans un circuit qui détermine s'il vaut mieux inverser ou non, qui commande un inverseur commandable.

Encoder InversionEncoding

Déterminer s'il faut inverser ou non la donnée est assez simple. On détermine les bits différents avec une porte XOR. Puis, on les compte avec un circuit de population count. Si le résultat est supérieur à la moitié des bits, alors il est bénéfique d'inverser. En effet prenons deux entiers de taille N. S'il y a X bits de différence entre eux, alors si l'on inverse l'un des deux, on aura N-X bits de différents. Il est possible de remplacer le circuit de population count par un circuit de vote à majorité, qui détermine quel est le bit majoritaire. S'il y a plus de 1 dans le résultat du XOR, cela signifie que l'on a plus de bits différents que de bits identiques, on dépasse la moitié (et inversement si on a plus de 0).

Le cout en circuits de cette technique est relativement faible, mais elle est assez efficace. Elle permet des économies qui sont au maximum de 50%, soit une division par deux de la consommation électrique du bus. Elle est plus proche de 10% dans des cas réalistes, pour un cout en circuit proche de plusieurs centaines de portes logiques pour des entiers 32/64 bits. De plus, la technique réduit la consommation dynamique du bus, celle liée aux changements d'état du bus, il reste une consommation statique qui a lieu en permanence, même si les bits du bus restent identiques.

Des calculs théoriques, couplés à des observations empiriques, montrent que la technique marche assez bien pour les petits bus, de 8/16 bits. Mis pour des bus de 32/64 bits, la technique n'offre que peu d'avantages. Une solution est alors d'appliquer la méthode sur chaque octet du bus indépendamment des autres. Par exemple, pour un bus de 64 bits, on peut utiliser 8 signaux INV, un par octet. Ou encore, on peut l'utiliser pour des blocs de 16 bits, ce qui donne 4 signaux INV pour 64 bits, un par bloc de 16 bits. On parle alors de Partitioned inversion encoding. La technique marche bien pour les bus mémoire, car c'est surtout les octets de poids faible qui changent souvent.

Les encodages sur les liaisons point à point asynchrones

[modifier | modifier le wikicode]

Avant de passer à la suite, nous allons voir comment sont encodées les données sur les bus asynchrones. Les liaisons point à point asynchrones permettent à deux circuits/composants de se communiquer sans utiliser de signal d'horloge. L'absence de signal d'horloge fait qu'il faut trouver d'autres méthodes de synchronisation entre composants. Et les méthodes en question se basent sur l'ajout de plusieurs bits ACK et REQ sur le bus.

Dans ce qui suit, on se concentrera sur les liaisons point à point asynchrones unidirectionnelles, on avec un composant émetteur qui envoie des données/commandes à 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

Les liaisons asynchrones doivent résoudre deux problèmes : comment synchroniser émetteur et récepteur, comment transmettre les données. Les deux problèmes sont résolus de manière différentes. Le premier problème implique d'ajouter des fils au bus de commande, qui remplacent le signal d'horloge. Le second problème est résolu en jouant sur l'encodage des données. Voyons les deux problèmes séparément.

La synchronisation des composants sur une liaison asynchrone

[modifier | modifier le wikicode]

La synchronisation entre deux composants asynchrones 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.

La transmission des données sur une liaison asynchrone

[modifier | modifier le wikicode]

La transmission des données/requêtes peut se faire de deux manières différentes, qui 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.

Bundled Encoding

Le Bundled Encoding utilise un fil par bit de données à transmettre. De telles liaisons sont souvent utilisées dans les composants asynchrones, à savoir les processeurs, mémoires et autres circuits asynchrones. Les circuits asynchrones sont composés de sous-circuits séparés, qui communiquent avec des liaisons asynchrones, qui utilisent le Bundled Encoding. Les circuits construits ainsi sont souvent appelés des micro-pipelines.

Le Bundled Encoding 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


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.

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 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. Le cas le plus classique est celui des bus série où la trame est envoyée bit par bits. Sur un bus parallèle, il peut y avoir des trames, si les données à transmettre sont plus grandes que la largeur du bus. Tout ce qui sera dit dans ce chapitre vaut pour les trames au sens : paquet de données envoyé en plusieurs fois.

La taille d'une trame

[modifier | modifier le wikicode]

La taille d'une trame est soit fixe, soit variable. Par fixe, on veut dire que toutes les trames ont la même taille, le même nombre de bits. Une taille de trame fixe rend l'interprétation du contenu des trames très simple. On sait où sont les données, elles sont tout le temps au même endroit, le format est identique dans toutes les trames. Mais il arrive que le bus gère des trames de taille variable.

Par exemple, prenons l'exemple d'un bus mémoire série, à savoir que la mémoire est reliée à l'extérieur via un bus série (certaines mémoires FLASH sont comme ça). Un accès mémoire doit préciser deux choses : s'il s'agit d'une lecture ou d'une écriture, quelle est l'adresse à lire/écrire, et éventuellement la donnée à écrire. Une trame pour le bus mémoire contient donc : un bit R/W, une adresse, et éventuellement une donnée. La trame pour la lecture n'a pas besoin de préciser de données à écrire, ce qui fait qu'elle contient moins de données, elle est plus courte. Pour une mémoire à accès série, reliée à un bus série, cela fait que la transmission de la trame est plus rapide pour une lecture que pour une écriture.

Sur un bus parallèle, la taille de la trame pose quelques problèmes. Dans le cas idéal, la taille de la trame est un multiple de la taille d'un mot, un multiple de la largeur du bus. Une trame contient N mots, avec N entier. Mais si ce n'est pas le cas, alors on fait face à un problème. Par exemple, imaginons que l'on doive envoyer une trame 2,5 fois plus grande qu'un mot. Dans ce cas, on envoie la trame dans trois mot, le dernier sera juste à moitié remplit. Il faut alors rajouter des bits/octets de bourrage pour remplir un mot.

Le codage des trames : début et de la fin de transmission

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Le codage des trames : les bits d'ECC

[modifier | modifier le wikicode]

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.

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 de correction/détection d'erreur aux données de base, aussi appelés des bits d'ECC. Ils 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.

Les bits d'ECC sont générés lors de l'envoi de la donnée sur la liaison point à point. Dans ce qui suit, on part du principe que l'on utilise une liaison série, la donnée est envoyée sur le bus bit par bit. La conversion parallèle-série est faite en utilisant un registre à décalage. La sortie du registre à décalage donne le bit envoyé sur le bus.

Le générateur/checker de parité

[modifier | modifier le wikicode]

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. Lors de l'envoi d'une donnée, le bit de parité est généré par un circuit appelé le générateur de parité sériel. Comme son nom l'indique, il calcule le bit de parité bit par bit, avec une bascule et une porte XOR. Rappelons que le bit de parité se calcule en faisant un XOR entre tous les bits du nombre à envoyer.

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. Le bit de parité final est disponible quand tous les bits ont été envoyés sur le bus, et la sortie du générateur de parité est alors connectée au bus pendant un cycle.

Générateur de parité sériel

Le générateur/checker de CRC

[modifier | modifier le wikicode]

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.

Pour les transmissions réseaux, le code utilisé est un code de redondance cyclique, un CRC. 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.

Les méthodes de retransmission

[modifier | modifier le wikicode]

Les codes de détection d'erreurs permettent parfois de corriger une erreur de transmission. 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 | modifier le wikicode]

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 | modifier le wikicode]

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.

La transmission des trames sur un bus série

[modifier | modifier le wikicode]

L'envoi d'une trame sur une liaison série demande d'envoyer celle-ci bit par bit. Et il existe des composants spécialisés, qui traduisent une trame en un flux de bit, qui envoient les bits un par un à la fréquence adéquate. De tels composants sont appelés des Universal asynchronous receiver-transmitter (UART ) ou encore des Universal synchronous and asynchronous receiver-transmitter (USART). La différence entre les deux tient dans le fait que l'un gère des communications asynchrones, l'autre gére des communications synchrones et asynchrones. Pour rappel, la communication asynchrone se passe de signal d'horloge et utilise surtout des bits de START/STOP.

Port série PC.

Les UART étaient beaucoup utilisés avec les anciens connecteurs RS-232 et RS-485, autrefois appelés ports séries sur les anciens PC. Le port série était utilisé pour brancher des imprimantes, des modems, et d'autres périphériques du genre. Le port série était opposé au port parallèle, qui lui était relié à un bus parallèle capable de transférer un octet à la fois. Les ports séries étaient tous précédés par un UART ou un USART. Ils sont aujourd'hui tombés en désuétude et l'UART est aujourd'hui intégré au chipset de la carte mère.

Notons que ce qui va suivre sera surtout valide pour les anciennes liaisons série, qui transmettaient des informations par petits paquets. Généralement, la transmission se faisait octet pat octet, parfois par trames de 5, 6, 7 bits, plus rarement 9. Les liaisons série modernes comme le PCI-Express ont des trames nettement différentes. Mais les principes de base restent les mêmes.

L'interface d'un UART/USART

[modifier | modifier le wikicode]

L'interface d'un USART est assez simple à comprendre quand on sait qu'il n'est qu'un intermédiaire. Il est un intermédiaire entre la liaison série, et un autre composant. Historiquement, les tout premiers USART étaient reliés directement sur le processeur. Par la suite, ils ont été connectés à d'autres circuits de la carte mère, en l’occurrence le chipset.

Toujours est-il que leur statut d'intermédiaire fait que certaines broches sont reliées à la liaison série, alors que d'autres sont reliées au processeur/chipset ou à un autre composant. Les broches en question sont donc séparées en deux ports : un port série pour la liaison série, un port externe pour la liaison avec le reste. D'autres broches ne font pas partie d'un port, comme la broche pour la tension d'alimentation, celle pour la masse, celle pour l'horloge, etc.

L'UART est un circuit séquentiel qui a une fréquence plus élevée que la liaison série, généralement 8 à 16 fois plus élevée. Et c'est là une des différences entre le port série et le port externe. Le port série a la même fréquence la liaison série, si celle-ci est une liaison synchrone. Dans le cas d'une liaison asynchrone, la fréquence de l'UART est conçue pour être compatible avec une transmission asynchrone sur la liaison. Par contre, le port externe a une fréquence bien plus élevée que le port série, identique à la fréquence de l'UART proprement dit. Cette différence de fréquence s'explique par le fait que la liaison série est plus lente que le processeur/chipset. Et cela aura quelques conséquences qu'on verra dans ce qui suit.

Le port externe a une taille qui généralement de l'ordre de l'octet, à savoir qu'il est connecté à un bus de un octet, ou une liaison point à point de un octet. Et historiquement, la majorité des bus série utilisait des trames d'un octet. Notons que les trames ne sont pas forcément d'un octet, il arrive que les trames fassent facilement 4 à 10 octets, selon le bus série utilisé. Les liaisons Ethernet utilisent par exemple des trames de 1500 octets. Dans ce cas, les trames sont envoyées octet par octet à l'UART, qui doit mémoriser ces octets pour qu'ils fassent une trame complète. Il contient pour cela des registres, comme nous le verrons plus haut.

L'intérieur d'un UART/USART

[modifier | modifier le wikicode]

Le fonctionnement interne d'un USART est assez simple, sur le principe. Pour envoyer ou recevoir les données sur une liaison série, il utilise des registres à décalage. Pour simplifier les explications, nous allons prendre une liaison série bidirectionnelle, et plus précisément de type full duplex, à savoir qu'il y a un fil pour l'envoi et un autre pour la réception. Sur le principe, elles restent valables pour une liaison unidirectionnelle, ou half-duplex, mais avec quelques changements mineurs. Même chose pour l'adaptation à un bus série.

USART relié à une liaison série bidirectionnelle de type full duplex, à savoir avec un fil pour l'envoi et un autre pour la réception.

Un UART contient un registre à décalage PISO dont la sortie est reliée à la liaison série, ce qui fait que les bits sont envoyés un par un. Il y a la même chose en réception, où un second registre à décalage SIPO voit le fil de réception connecté sur son entrée, ce qui fait que les bits reçus sont accumulés un par un dans ce registre à décalage. Le récepteur accumule les bits dans ce registre à décalage SIPO, jusqu'à avoir une trame complète.

Il faut noter que l'USART ne fait pas qu'envoyer les bits de données. En envoi, il ajoute aussi des bits de START/STOP pour délimiter les trames, ainsi que des bits de parité. En réception, il retire les bits START/STOP de la trame. Pour être précis, il utilise les bits START/STOP pour délimiter les trames. Il contient des circuits pour détecter les bits de START, ainsi que les bits de STOP. En clair, il ne fait pas de la conversion série-parallèle, il code et encode des trames complètes. Pour cela, les registres à décalage sont couplés à des circuits de contrôle internes à l'USART.

Parmi les circuits de contrôle, il faut mentionner ceux qui calculent les bits de parité ou d'ECC. Le premier génère les bits de parité à ajouter à la trame, le second vérifie les bits de parité reçu. Ils peuvent corriger les données reçues si l'ECC est utilisé et qu'une seule erreur de transmission a eu lieu. En cas d'échec, ils peuvent lancer la procédure adéquate pour gérer l'erreur, mais certains UART simples n'en sont pas capables.

Gestion des trames par l'USART.

Les deux registres à décalage sont cadencés par un signal d'horloge généré à l'intérieur de la puce. En effet, une liaison série peut fonctionner à plusieurs fréquences différentes. Par exemple, le RS-232 pouvait fonctionner à une vitesse allant de 50 à 19 200 bits par secondes. Les fréquences en question sont souvent multiples l'une de l'autre. En clair, elles peuvent s'obtenir à partir d'une fréquence de base, multipliée par un coefficient. Quelques UART implémentaient cependant ces fréquences à l'envers : elles prenaient la fréquence maximale, et la divisaient par un coefficient pour obtenir la fréquence voulue. Les UART contenaient pour cela des multiplieurs/diviseurs de fréquences.

L'UART contient aussi des registres de configuration, qui permettent de configurer la vitesse de transmission, la taille d'une trame, etc. Par exemple, il est possible de configurer la taille d'une trame pour choisir entre des tailles prédéfinies, comme des trames de 5, 7, 8, 9 bits. Il est aussi possible de désactiver les bits de parité ou d'ECC si le périphérique n'en a pas besoin. On a alors un gain en vitesse, vu que les bits d'ECC ne sont pas transmis. Enfin, il est aussi possible de configurer la vitesse de transmission, selon que le composant périphérique relié à la liaison série est un composant rapide ou non.

Les registres d’interfaçage

[modifier | modifier le wikicode]

Un UART contient aussi deux autres registres, appelés le registre d'émission et de réception. Ils servent d'interface avec le port externe, qui ne fonctionne pas à la même fréquence que la liaison série. Lors d'une émission, le processeur/chipset envoie une trame à l'UART. Elle est alors copiée dans le registre d'émission, où elle attend que la liaison soit libre. La trame peut être envoyée à l'UART octet par octet, les octets sont alors accumulés dans le registre d'émission jusqu'à obtenir une trame complète. En réception, le registre de réception sert à la même chose : lorsqu'une trame complète a été reçue, elle est copiée dans le registre de réception, puis envoyée octet par octet sur le port externe.

La présence de ces deux registres permet aussi de simplifier les échanges avec le processeur. Par exemple, lors d'un envoi, le processeur écrit dans le registre d'émission, mais la liaison série n'est pas forcément libre. Aussi, la trame envoyée doit attendre que la liaison soit libre, dans le registre d'émission. Par exemple, une trame peut être en cours d'envoi dans le registre PISO, pendant que le processeur écrit dans le registre d'émission. Idem avec le registre de réception : le processeur peut récupérer une donnée dans ce registre, pendant qu'une autre transmission est en cours.

Nous verrons dans le chapitre sur le contrôleur de périphérique que c'est là un principe général, qui s'applique à tous les périphériques, et que les registres équivalents pour les autres périphériques sont appelés des registres d’interfaçage. Pour le moment, nous allons les appeler registres d'interface processeur, car on suppose que l'UART est relié directement au processeur. S'il n'est pas connecté directement au processeur, il est connecté à un bus auquel le processeur a accès et où seul le processeur est censé commander l'UART. Notons que ce bus est souvent half-duplex, ce qui n'est pas forcément le cas de la liaison série, les deux registres d'émission/réception sont alors utiles pour faire l'interface entre les deux.

UART, schéma de principe.

Une autre utilité est que l'ajout des bits de START et de STOP est plus simple avec ces registres. Lorsqu'un paquet de données est copié du registre d'émission vers le registre à décalage, les bits de START/STOP sont ajoutés. Ils sont physiquement présents dans le registre à décalage PISO. Même chose en réception, où les bits de START/STOP sont présents, mais sont retirés lors de la copie dans le registre de réception. Idem avec les bits de parité ou d'ECC, qui sont ajoutés ou retirés.

Les UART/USART historiques : le 8250 et le 16 550

[modifier | modifier le wikicode]

De nos jours, plus personne n'utilise de UART standalone, ils sont intégrés dans les composants eux-mêmes et ne sont que des portions minuscules de circuits intégrés plus grands. Par exemple, une carte mère a de nos jours un chipset, un circuit intégré qui fait tout, dont l'UART n'occupe que 1% du circuit intégré. Mais étudier les anciens circuits UART est intéressant pour comprendre comment fonctionnent les USART.

8250 and 16450 UART

Dans cette section, nous allons voir le circuit intégré 8250 de la société National Semiconductor, et son successeur le 16 550. C'étaient des UART assez simples. Ils étaient présents dans les tout premiers PC 8 bits, mais ils étaient aussi très utilisés dans des modems, des imprimantes, et bien d'autres composants. Il est illustré ci-contre. Comme vous le voyez, son interface est assez complexe, avec beaucoup de broches, 40 au total. Nous n'allons pas détailler toutes les broches, seulement les principales.

En premier lieu, les broches D0 à D7 permettent de lire/écrire un octet sur la liaison série. Elles servent d'entrées et de sorties. En entrée, c'est là que le processeur écrit la donnée à envoyer. En sortie, c'est là qu'est récupéré l'octet réceptionné. Elles sont connectées à un bus parallèle, sur lequel le processeur ou un contrôleur de périphérique envoie/récupère l'octet voulu. Les broches RD, WR, /RD et /WR indiquent si l'USART doit être configuré en envoi ou réception, les signaux /CS2, CS1, CS0 ils activent ou désactivent l'USART (c'est des signaux de Chip Select).

La broche INTRPT est connectée directement au processeur. Pour simplifier, elle est impliquée dans la communication entre UART et processeur. Elle indique au CPU qu'un octet a été réceptionné et est disponible. Il s'agit formellement d'une broche d'interruption, dont nous ne pouvons pas parler ici, vu que nous n'avons pas encore vu le concept d'interruption.

Le 8250 avait un générateur de fréquence interne, ce qui lui permettait de gérer plusieurs bus différents. par exemple, il pouvait gérer le bus RS-232 ou le bus PS/2, alors qu'ils ont tout deux des fréquences différentes. Pour cela, le 8259 recevait une horloge de base sur les entrées dédiées XIN, XOUT, /BAUDOUT, RCLK. Puis, il multipliait cette fréquence par un coefficient, afin de générer la fréquence idéale pour le bus voulu.

Le remplacement des registres d'émission/réception par une FIFO

[modifier | modifier le wikicode]

Il 8250 et ses successeurs n'étaient pas les seuls UARTs disponibles sur le marché. Entre le Zilog SCC, le MOS Technology 6551, le Zilog Z8440 et bien d'autres, il y avait le choix. Et parmi tout ce zoo d'UART, il faut séparer deux classes. La première est celle qui regroupe le 8250 vu précédemment, le 8251 d'Intel, le Motorola 6850, le 6551 de MOS Technology et quelques autres. Ils disposaient d'un registre d'émission et d'un registre de réception, là où les autres les remplaçaient par une mémoire FIFO.

Le Zilog SCC a remplacé le registre de réception par une mémoire FIFO. L'idée était d'accumuler plusieurs octets avant de prévenir le processeur. Plusieurs trames étaient réceptionnées avant qu'on fasse intervenir le processeur, qui n'était pas interrompu à tout bout de champ. Nous verrons cela plus en détail dans le chapitre sur la synchronisation entre CPU et périphériques. Toujours est-il que le Zilog SCC avait un registre d'émission de un octet et une FIFO de réception de 3 octets. Seule la réception était améliorée.

Le successeur du 8250, le 16 550, a appliqué la même optimisation pour le registre d'émission, qui était remplacé par une mémoire FIFO. Pour rappel, l'UART a une fréquence plus élevée que la liaison série et il communique avec un processeur qui a aussi une fréquence très élevée. Il était alors possible d'envoyer plusieurs trames à l'UART en même temps, ou du moins dans un intervalle de temps très court, largement plus petit que celui nécessaire pour transmettre une trame. Sur le 16 550, le processeur pouvait envoyer 16 trames à la suite, qui sont ensuite transmises une par une par l'UART. Les UARTs suivants implémentaient des FIFOs de 128 octets, comme le 16 850 ou le 16C850.

De nombreux UART implémentaient cette optimisation, que ce soit en réception ou en émission. Il faut dire que sans ça, les transmissions étaient limitées à environ 1000 octets par secondes, pour des raisons liées au système d'exploitation. Pour simplifier, l'OS limite le nombre d'accès à l'UART à 1000 fois par seconde (une interruption toutes les millisecondes). Mais avec l'ajout de mémoires FIFO, le débit s'améliore. Une FIFO peut être intégralement remplie ou vidée en un seul passage du processeur. Par exemple, avec une FIFO de 128, on peut transmettre 128 × 1000 = 125 kibioctets par secondes.


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 | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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. À 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Le PCI Express

[modifier | modifier le wikicode]

Le PCI Express est le remplaçant du vieux bus PCI. Il est officiellement abrégé PCI-E ou PCIe et nous utiliserons cette abréviation dans ce qui suit. Malgré son nom, il y a de grandes différences entre le bus PCI et le PCI-Express. Notez que je n'ai pas parlé de bus PCI-Express, et ce pour une bonne raison : le PCI-E n'utilise pas un bus, mais des liaisons point-à-point ! De plus, ces liaisons de type série, là où le PCI est un bus parallèle ! Enfin, le bus PCI est un bus half-duplex, alors que le PCI-E est full duplex. Pour résumer ces différences fondamentales :

  • bus partagé de 32 bits pour le PCI, liaisons point à point pour le PCI-E ;
  • liaison parallèle pour le PCI, série pour le PCI-E ;
  • liaison half-duplex pour le PCI, full-duplex pour le PCI-E.
Différences entre PCIe et PCI.

La topologie des liaisons PCI-E

[modifier | modifier le wikicode]

Les périphériques PCI-E sont tous connectés à un répartiteur central, appelé le Root Complex. Les liaisons entre périphérique et Root Complex sont des liaisons point à point série. Le Root Complex est intégré soit à la carte mère, soit au processeur. Auparavant, il était placé sur la carte mère, soit dans le northbridge de la carte mère, soit dans le southbridge, les deux étaient possibles. S'il est intégré à la carte mère, il y a généralement un bus dédié pour le relier au processeur s'il est dans le northbridge, ou au northbridge s'il est dans le southbridge.

Liaison entre Root Complex et périphériques PCI-E.

Évidemment, il y a une limite au nombre de périphériques qu'on peut brancher directement sur le Root Complex. Cependant, on peut dépasser cette limite en ajoutant des intermédiaires appelés des switchs, qui servent de répartiteur secondaire. Pour signaler que les périphériques se situent à la fin du réseau d'interconnexion, une fois tous les intermédiaires parcourus, les périphériques sont appelés des End Points.

Le bus PCI-E peut servir à émuler l'ancien bus PCI. Pour cela, il faut connecter un circuit qui fait l'intermédiaire entre le bus PCI et le Root Complex, et qui traduit les trames PCI en commandes compréhensibles par le Root Complex. L'intermédiaire est souvent appelé un Bridge PCIe.

PCI Express, topologie.

Les liaisons PCIe : lanes, ports et signaux

[modifier | modifier le wikicode]

Une liaison série PCIe contient deux paires différentielles, chacune permettant un transfert série. La première paire différentielle transmet les données/commandes du périphérique vers le root complex, l'autre liaison s'occupe des transferts dans l'autre sens. C'est la raison pour laquelle la liaison est full-duplex : il y a deux liaisons série en une, une par sens de transfert.

Intuitivement vous vous dites que pour chaque périphérique PCIe , il y a une seule liaison série connectée au Root Complex. Sauf qu'en réalité, un périphérique PCIe peut utiliser plusieurs liaisons série, plusieurs lanes. Pour faire la distinction, il faut faire la distinction entre une liaison série et une connexion entre périphérique PCIe et root complex. Une liaison série est appelée une lane, alors qu'une connexion entre root complex et périphérique PCIe est appelée un port PCIe , un link PCIe . Un port PCIe contient donc une ou plusieurs lanes : 1, 2, 4, 8 ou 16 lanes.

Terminologie des liaisons PCI Express.

Le débit binaire d'un port PCIe augmente avec le nombre de lanes. Un port à 8 lanes sera 8 fois plus rapide qu'un port à une seule lane. Le débit binaire d'une lane dépend de la version du PCIe : elle était de 250 Mb/s pour le PCIe 1.0, 500 Mb/s pour le PCIe 2.0, elle est de 7,563 Gb/s pour la version 6.0. Le débit binaire a presque doublé d'une version a une autre.

Les périphériques ayant besoin de peu de débit binaire utilisent une seule lane, les périphériques ayant des besoins plus importants en utilisent 8 ou 16. Il faut noter qu'il existe différents connecteurs PCIe , qui se différencient par le nombre de lanes. Il y a des connecteurs avec une seule lane', un autre avec deux lanes, un autre avec 4, etc. Plus il y a de lanes, plus le connecteur est long.

Various PCIe Slots

L'envoi des données sur les différentes lanes se fait octet par octet. On envoie le premier octet sur la première lane, le second octet sur la seconde lane, le troisième octet sur la troisième lane, et ainsi de suite. La spécification du PCIe appelle cela du data striping.En faisant ainsi, il peut y avoir un petit décalage entre les lanes, à savoir que les octets ne sont pas envoyés exactement en même temps. Le décalage est cependant limité, la norme PCIe impose une limite de l'ordre de quelques nanosecondes entre lanes d'un même port PCIe.

Les commandes PCIe

[modifier | modifier le wikicode]

Les commandes envoyées sur les liaisons PCIe sont standardisées. Une commande PCIe peut demander de faire une lecture ou une écriture, configurer un périphérique, envoyer des messages entre périphérique ou au processeur (les interruptions). Le protocole fait la distinction entre lecture/écriture mémoire et lecture/écriture dans un périphérique. Les commandes de configuration d'un périphérique sont émises par le root complex vers un périphérique, l’inverse est impossible. Les autres commandes peuvent être émises dans n'importe quel sens.

Le PCIe autorise deux types de transactions : les transferts DMA entre un périphérique et la RAM, un transfert entre deux périphériques. Les transferts entre périphériques peuvent passer par l'intermédiaire du root complex, sans faire intervenir le processeur ou la mémoire RAM. Pour déterminer qui est le destinataire d'une commande, celle-ci intègre l'adresse du destinataire. Le root complex reçoit la commande, extrait cette adresse, et détermine qui est le destinataire de la commande.

Il existe des commandes de gestion de l'alimentation, afin de configurer le récepteur pour le mettre en veille, activer certaines fonctionnalités d'économie d'énergie, etc. Il existe aussi des commandes de contrôle de flux. Elles permettent de gérer l'occupation des liaisons PCIe. Un périphérique PCIe reçoit des commandes et les accumule dans une mémoire tampon, en attendant de les traiter. L'émetteur peut envoyer des commandes pour savoir si cette mémoire tampon est pleine, en passe de l'être, vide, globalement vide, etc. Il adapte alors ses transferts en fonction.

Les trames PCIe

[modifier | modifier le wikicode]

Les trames PCIe sont beaucoup plus complexes que les trames du bus PCI. Une trame PCIe est composé d'un header de 12-16 octets, suivi par les données à transmettre. Les données peuvent prendre de 0 à 4096 octets. Le tout peut être complété par 4 à 8 octets de détection/correction d'erreur. En tout, la trame contient donc de 12 à 4116 octets. À cela, il faut ajouter les octets de START/STOP de la trame et 2 octets supplémentaires.

Une trame contient des données essentielles : l'adresse à destination du récepteur, l'identifiant de l'émetteur, un champ type qui indique de quelle commande il s'agit, etc. La taille de la trame est aussi encodée dans le header à un endroit bien précis. Le champ Type est précédé par un nombre qui indique quelle est la taille du header et comment il doit être interprété.

Comparaison entre une trame PCI à gauche, et une trame PCI Express à droite.

Détailler le contenu des trames est assez complexe. Il faut dire que le PCIe définit un protocole de communication entre root complex et périphérique PCIe qui est assez complexe. Il incorpore de nombreuses fonctionnalités qu'on attendrait plutôt d'un protocole réseau ! Les informaticiens seraient étonnés de voir que la spécification du PCIe reprendre des termes courants dans le domaine des réseaux, comme la séparation entre une couche transport, une couche de liaison et une couche physique.

La raison est que le PCIe gère des transferts de taille arbitraire, qui sont découpés en trames envoyées sur un port PCIe. Les trames sont numérotées, afin de se souvenir de leur ordre d'envoi. Cela permet de garantir que les données sont bien envoyées trame par trame, dans l'ordre. Le numéro de trame est intégré dans le header de la trame, dans les 12-16 octets au début de la trame. Si jamais une trame n'est pas transmise, à cause d'une erreur de transmission détectée par l'ECC, les numéros de trame permettent de détecter qu'un problème a lieu.

Par exemple, imaginez qu'une trame soit perdue. Dans ce cas, deux trames consécutives n'auront pas de numéro de trame consécutifs. Par exemple, une trame aura le numéro 6 et la suivante le numéro 8, ce qui indique que la trame numéro 7 a été perdue en chemin. Un autre exemple est le cas où une trame est transmise, mais où l'ECC détecte une erreur de transmission. Dans ce cas, le receveur prévient l'émetteur qu'une erreur a eu lieu.

Pour gérer cela, le PCIe gère pour cela des commandes d'achèvement pour prévenir qu'une donnée a bien été reçue, qu'une transaction s'est bien déroulée. Le protocole PCIe incorpore deux commandes ACK et NACK : ACK indique que la trame a été transmise sans problème, un NACK indique qu'un problème a eu lieu. Si l'émetteur envoie une trame et recoit un ACK, il envoie la trame suivante. Mais s'il reçoit un NACK, il renvoie la trame fautive.

De plus, les trames suivant la trame fautive sont annulées et sont renvoyées dans l'ordre adéquat. Pour cela, la commande NACK précise le numéro de la trame fautive, pour que l'émetteur sache quelle est la trame fautive et quelles sont les trames à renvoyer. Pour renvoyer les trames, l'émetteur n'efface pas les trames une fois qu'elles sont transmises, mais les conserver en mémoire. Les trames ne sont effacées qu'une fois l'ACK associé reçu. L'émetteur doit aussi savoir quelle est la dernière trame valide envoyée, à savoir la dernière trame à avoir reçu un ACK. Il contient un registre pour mémoriser le numéro de cette trame, qui est incrémenté à chaque réception d'un ACK valide.


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 | modifier le wikicode]

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.

Les mémoires à semi-conducteurs

[modifier | modifier le wikicode]

Le support de mémorisation peut être un support électronique, comme sur les registres ou les mémoires ROM/RAM/SSD et autres. Les mémoires en question sont appelées des mémoires à semi-conducteurs. Le codage des données n'est pas différent de celui observé dans les registres, à savoir que les bits sont codées par une tension électrique. Elles sont presque toutes fabriquées avec des transistors MOS/CMOS, peu importe qu'il s'agisse des mémoires RAM ou ROM. La seule exception est celle des mémoires EEPROM et des mémoires FLASH, qui sont fabriquées avec des transistors à grille flottante, qui sont des transistors MOS modifiés. L'essentiel est que les mémoires à semi-conducteurs sont fabriquées à partir de transistors MOS et de portes logiques, comme les circuits vus dans les premiers chapitres du cours.

La loi de Moore influence directement la capacité des mémoires à semi-conducteurs. Une mémoire à semi-conducteurs est composée de cellules mémoires qui mémorisent chacune 1 bit (parfois plusieurs bits, comme sur les mémoires FLASH). Chaque cellule est elle-même composées d'un ou de plusieurs transistors MOS reliés entre eux. Plus la finesse de gravure est petite, plus des transistors l'est aussi et plus la taille d'une cellule mémoire l'est aussi. Quand le nombre de transistors d'une mémoire double, le nombre de cellules mémoire double, 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 SRAM, ROM, EEPROM et bien d'autres. Les performances de ces mémoires ont aussi suivi, encore que les mémoires DRAM stagnent pour des raisons qu'on expliquera dans quelques chapitres.

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.

Les anciennes technologies de mémoire

[modifier | modifier le wikicode]
Ordinateur Digital PDP-8, avec ses bandes magnétiques visibles.

Les mémoires magnétiques, assez anciennes, utilisaient un support de mémorisation magnétique, dont l'aimantation permet de coder un 0 ou un 1. Sur les disques durs et disquettes, le support magnétique est un plateau dont la surface est aimantée. Mais, d'autres mémoires utilisaient une bande magnétique similaire à celle des vielles cassettes audio. Les bandes magnétiques étaient enroulées dans un cylindre plastique, et étaient déroulées par un système de déroulement.

Les bandes magnétiques ont servi de support de stockage en-dehors des ordinateurs : les cassettes audio utilisaient une bande magnétique unique, les cassettes VHS étaient elles aussi basées sur des bandes magnétiques, et j'en passe. D'ailleurs, quelques vieux ordinateurs utilisaient des cassettes à bande magnétique, les mêmes qui étaient utilisées comme cassettes audio il y a de cela quelques décennies. C'était le cas de vieux ordinateurs personnels, comme les Amstrad CPC, les ZX Spectrum, ou les ordinateurs Commodore.

Mémoire à bande magnétique.
Ordinateur personnel Amstrad CPC 464.

Les mémoires magnétiques 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.

Les mémoires optiques, dont les plus connues sont les CD et les DVD, utilisent une surface réfléchissante comme support de mémorisation. 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 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 | modifier le wikicode]
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 | modifier le wikicode]

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 octet 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).

Les mémoires mask ROM sont intégralement construites en utilisant des transistors MOS normaux, ce qui fait que leurs performances est censée suivre la loi de Moore. Mais dans les faits, ce n'est pas vraiment le cas pour une raison simple : on n'a pas besoin de mémoires ROM ultra-rapides, ni de ROM à grosse capacité. Les ROM sont aujourd'hui presque exclusivement utilisées pour les firmware des systèmes embarqués à faible performance, ce qui contraint les besoins. Pas besoin d'avoir des ROM ultra-rapides pour stocker ce firmware.

Les mémoires PROM, RPROM, EPROM et EEPROM sont elles fabriqués autrement, généralement en utilisant des transistors MOS modifiés appelés transistors à grille flottante. Nous verrons ce que sont ces transistors dans quelques chapitres, mais nous pouvons d'or et déjà dire que leur fabrication n'est pas si différente des transistors MOS normaux. En conséquence, la loi de Moore s'applique, ce qui fait que la capacité de ces mémoires doubles environ tous les deux ans. Les performances s'améliorent aussi avec le temps, mais à un rythme moindre.

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 | modifier le wikicode]

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 adresse, 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 | modifier le wikicode]

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 | modifier le wikicode]

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

[modifier | modifier le wikicode]

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. La différence entre les deux est la suivante. 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. 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.

Les RAM statiques sont appelées des SRAM (Static RAM), alors que les RAM dynamiques sont appelées des DRAM (Dynamic RAM). Les SRAM et DRAM ne sont pas fabriquées de la même manière : transistors uniquement pour la première, transistors et condensateurs (des réservoirs à électrons) pour l'autre. Les SRAM ont des performances excellentes, mais leur capacité laisse à désirer, alors que c'est l'inverse pour la DRAM. Aussi, leur usage ne sont pas les mêmes.

Leur usage dépend de si l'on parle de systèmes embarqués/industriels ou des PC pour utilisateur particulier/professionnel. La SRAM est surtout utilisée dans les microcontrôleurs ou les systèmes assez simples, pour lesquels le rafraichissement mémoire poserait plus de problèmes que nécessaires. Les microcontrôleurs ou systèmes embarqués n'utilisent généralement pas de mémoire DRAM. Dans les PC actuels, la SRAM est intégrée dans le processeur et se trouve dans le cache du processeur, éventuellement pour les local store. À l'opposé, la DRAM est utilisée pour fabriquer des barrettes de mémoire, pour la RAM principale.

La différence entre les deux est que la SRAM est utilisée comme mémoire à l'intérieur d'un circuit imprimé, qui regroupe mémoire, processeur, et éventuellement d'autres circuits. Par contre, la DRAM est placée dans un circuit à part, séparé du processeur, dans son propre boitier rien qu'à elle, voire dans des barrettes de mémoire. Et la raison à cela est assez simple : la SRAM utilise les mêmes technologies de fabrication CMOS que les autres circuits imprimés, alors que la DRAM requiert des condensateurs et donc des techniques de fabrication distinctes.

Il existe des mémoires qui sont des intermédiaires entre les mémoires SRAM et DRAM. Il s'agit des mémoires pseudo-statiques, qui sont techniquement des mémoires DRAM, utilisant des transistors et des condensateurs, mais qui gèrent leur rafraichissement mémoire toutes seules. Sur les DRAM normales, le rafraichissement mémoire est effectué par le processeur ou par le contrôleur mémoire sur la carte mère. Il envoie régulièrement des commandes de rafraichissement à la mémoire, qui rafraichissent une ou plusieurs adresses à la fois. Mais sur les mémoires pseudo-statiques, le rafraichissement se fait automatiquement sans intervention extérieure. Des circuits intégrés à la mémoire pseudo-statiques font le rafraichissement automatiquement.

Les mémoires non-volatiles

[modifier | modifier le wikicode]

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 BBSRAM ont é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).

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.

RAM drive.

Les RAM drive matériels sont une version sous stéroïdes des mémoires précédentes, qui sont construites à partir de barrettes de mémoire DRAM. Concrétement, ce sont des pseudo-disques durs fabriqués en combinant des barrettes de RAM et une batterie. Un circuit intégré simule un disque dur à partir des barrettes de RAM, il sert d'interface entre le connecteur du disque dur et les barrettes de mémoire. Les barrettes sont alimentées par une batterie, afin qu'elles ne s'effacent pas, ce qui permet de conserver des données sur une période de quelques heures maximum. Nous en reparlerons en détail dans un chapitre ultérieur, dans la section sur les mémoires de masse de ce cours.

Le lien avec la technologie de fabrication et les autres critères

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

Les mémoires électroniques RAM et ROM utilisent l'adressage mémoire, aux côtés de quelques mémoires de masse. Pour rappel, la mémoire est découpée en cases mémoire de N bits, qui sont numérotées. Une case mémoire mémorise un ou plusieurs octets, selon la mémoire. Toutes les cases mémoire d'une RAM/ROM ont la même taille, le nombre d'octet est identique. Par exemple, les RAM modernes ont des cases mémoire de 64 bits, soit 8 octets.

Avec 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 l'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 | modifier le wikicode]

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 | modifier le wikicode]

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 !

Les mémoires de ce type sont appelées des mémoires séquentielles. Les bandes magnétiques, cassettes audio inclues, étaient des mémoires de ce type. 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. 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.

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).

Les mémoires synchrones et asynchrones

[modifier | modifier le wikicode]

Les toutes premières mémoires électroniques étaient 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.

Il existe plusieurs types de mémoires synchrones. Les premières sont tout simplement des mémoires naturellement synchrones. Elles sont construites avec des bascules D synchrones, ce qui fait que le support de mémorisation est lui-même synchrone. Mais cela ne concerne que quelques mémoires SRAM bien spécifiques, d'une utilisation très limitée. De nos jours, cela ne concerne que les registres du processeurs ou quelques mémoires tampons bien spécifiques utilisées dans les processeurs modernes.

Le second type de mémoire synchrone prend une mémoire asynchrone et ajoute des registres sur ses entrées/sorties. Instinctivement, on se dit qu'il suffit de mettre des registres sur les entrées associées au bus d'adresse/commande, et sur les entrées-sorties du bus de données. Mais faire ainsi a des conséquences pas évidentes, au niveau du nombre de cycles utilisés pour les lectures et écritures. Nous détaillerons tout cela dans le chapitre sur les mémoires SRAM synchrones, ainsi que dans le chapitre sur les DRAM.

Le lien entre les différents types de mémoires

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]
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 généralement 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.

Les bits Chip Select et Output Enable

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 (facultatif)

[modifier | modifier le wikicode]

Toutes les mémoires adressables sont naturellement connectées au bus. La transmission de l'adresse à la mémoire peut se faire de plusieurs manières. La plus simple utilise un bus dédié pour envoyer les adresses à la mémoire, séparé du bus de données et du bus de commande. Le bus en question est appelé le bus d'adresse.

Entrées et sorties d'un bus normal.

Mais d'autres mémoires font autrement et fusionnent le bus d'adresse et de données. Le bus de commande existe toujours, il est secondé par un autre bus qui sert à transmettre données et adresses, mais pas en même temps. De tels bus sont appelés soit des bus multiplexés, soit des bus à transmission par paquet. Les deux méthodes sont légèrement différentes, comme on le verra dans ce qui suit.

Les bus d'adresse multiplexés

[modifier | modifier le wikicode]

Avec un bus d'adresse dédié, 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 | modifier le wikicode]

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.

Les bus à commutation de paquet

[modifier | modifier le wikicode]

Des mémoires DRAM assez rares ont exploré un bus mémoire particulier : avoir un bus peu large mais de haute fréquence, sur lequel on envoie les commandes/données en plusieurs fois. Elles sont regroupées sous le nom de mémoires à commutation par paquets. Elles utilisent des bus spéciaux, où les commandes/adresses/données sont transmises par paquets, par trames, en plusieurs fois. Le processeur envoie des paquets de commandes, les mémoires répondent avec des paquets de données ou des accusés de réception. Toutes les barrettes de mémoire doivent vérifier toutes les transmissions et déterminer si elles sont concernées en analysant l'adresse transmise dans la trame. En théorie, ce qu'on a dit sur le codage des trames dans le chapitre sur le bus devrait s'appliquer à de telles mémoires. En pratique, les protocoles de transmission sur le bus mémoire sont simplifiés, pour gérer le fonctionnement à haute fréquence.

Les mémoires à commutation par paquets sont peu nombreuses. Les plus connues sont les mémoires conçues par la société Rambus, à savoir la RDRAM (Rambus DRAM) et ses deux successeurs XDR RAM et XDR RAM 2. La Synchronous-link DRAM (SLDRAM) est un format concurrent conçu par un consortium de plusieurs concepteurs de mémoire.

Un premier exemple est celui des mémoires RDRAM, où le bus permettait de transmettre soit des commandes (adresse inclue), soit des données, avec un multiplexage total. Le processeur envoie un paquet contenant commandes et adresse à la mémoire, qui répond avec un paquet d'acquittement. Lors d'une lecture, le paquet d'acquittement contient la donnée lue. Lors d'une écriture, le paquet d'acquittement est réduit au strict minimum. Le bus de commandes est réduit au strict minimum, à savoir l'horloge et quelques bits absolument essentiels, les bits RW est transmis dans un paquet et n'ont pas de ligne dédiée, pareil pour le bit OE.

Pour donner un autre exemple, parlons rapidement des mémoires SLDRAM. Elles utilisaient un bus de commande de 11 bits, qui était utilisé pour transmettre des commandes de 40 bits, transmises en quatre cycles d'horloge consécutifs. Le bus de données était de 18 bits, mais les transferts de donnée se faisaient par paquets de 4 à 8 octets (32-65 bits). Pour résumer, données et commandes sont chacunes transmises en plusieurs cycles consécutifs, sur un bus de commande/données plus court que les données/commandes elle-mêmes.

Le bus de données et les mémoires multiports

[modifier | modifier le wikicode]

Le bus de données transmet un nombre fixe de bits. Dans la plupart des cas, le bus de données peut transmettre plusieurs bits à chaque transmission (à chaque cycle d'horloge). Un bus qui permet cela est appelé un bus parallèle. Les bus mémoire modernes sont assez larges : 16, 32 ou 64 bits, parfois plus ! Les PC modernes ont des bus mémoire de 128 ou 256 bits avec les technologies dual/quad channel !

Un bus mémoire parallèle transmet un mot mémoire, à savoir un paquet de bits transmis en même temps sur le bus de données. Un mot mémoire est généralement composé de plusieurs octets consécutifs dans la RAM. Il correspond souvent à une case mémoire, mais ce n'est pas toujours le cas. Il arrive qu'une case mémoire soit transmise en plusieurs fois sur le bus mémoire. Par exemple, il est possible d'avoir un mot mémoire de 64 bits, pour une case mémoire de 128 bits : la case mémoire est envoyée en deux cycles d'horloge sur le bus mémoire. C'est ce principe qui est utilisé sur les mémoires DDR, qu'on abordera dans quelques chapitres. Pour le moment, nous allons considérer qu'un mot mémoire a la même taille qu'une case mémoire.

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 octets complets, mais ceux-ci sont transmis bits par bits sur le bus de données. La conversion entre octet 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 | modifier le wikicode]

Le bus de données est un bus bidirectionnel, sauf pour les mask ROM qui ne gèrent que la lecture, qui peuvent se contenter d'un bus unidirectionnel. Sur la plupart des mémoires, le bus de données est bidirectionnel et sert aussi bien pour les lectures que pour les écritures. Les mémoires de ce type sont appelées des mémoire simple port.

Mémoire simple-port

Sur la majorité des mémoires SRAM, 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. Les mémoires de ce type sont appelées des mémoire double port lecture-écriture.

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)

En général, les mémoires ROM et DRAM sont de des mémoires simple port, alors que les mémoires SRAM sont double port, les exceptions étant rares. C'est assez intuitif pour les mémoires ROM : elles sont utilisées en lecture uniquement, avec des reprogrammations éventuelles assez rares. Pas besoin d'ajouter un second bus pour des reprogrammation rares, ce qui aurait un cout en termes de broches, de packaging, etc. Pour les DRAM, la raison est tout autre, et tient au fait que le plan mémoire des DRAM est naturellement siomple port, mais cela deviendra plus clair dans le chapitre sur les cellules mémoire. Les SRAM sont généralement double port pour les mêmes raisons : le plan mémoire est naturellement double port.

Les mémoires multiport

[modifier | modifier le wikicode]

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.

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.

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.

Les circuits de l'interface mémoire

[modifier | modifier le wikicode]

Une mémoire RAM a donc une interface assez précise, notamment pour le bus de commande. L'interface la plus simple regroupe un bus de données, un bus d'adresse, un bit R/W, deux bits Chip Select et Output Enable, une entrée d'horloge. A l'intérieur de la mémoire, il existe un cœur qui contient les cases mémoire et un décodeur d'adresse connecté au bus d'adresse. Entre ce cœur et le bus mémoire, se trouve un circuit d’interfaçage mémoire, qui est commandé par une logique de contrôle, elle-même commandée par les bits RW, Chip Select et Output Enable. Et nous allons voir ce circuit d’interfaçage dans ce qui suit.

Il faut préciser que ce circuit d’interfaçage est situé dans la mémoire RAM/ROM, pas en-dehors. Il faut en effet distinguer l'interface mémoire, interne aux puces de RAM/ROM, de la glue logic qui connecte la mémoire RAM/ROM au bus mémoire proprement dit. La glue logic du bus mémoire est située en-dehors des puces de RAM/ROM, elle est composée de circuits distincts, qu'on verra au chapitre suivant, qui porte justement sur le bus mémoire. Ici, nous allons voir tout ce qui est relié directement aux broches de la puce mémoire, pas autre chose.

Organisation interne d'une mémoire adressable.

L’interfaçage avec le bus de données : rappels

[modifier | modifier le wikicode]
Tampons 3 états.

Expliquer Le circuit d’interfaçage n'est pas compliqué, si on se rappelle ce qu'on a vu dans le chapitre sur les bus électroniques. Pour interfacer un composant avec un bus, la solution la plus utilisée est d'utiliser des tampons trois états. Pour rappel, un tampon trois-états peut être vu comme une porte OUI modifiée, qui peut déconnecter sa sortie de son entrée. Suivant ce qui est mis sur l'entrée de commande, la sortie est soit déconnectée de l'entrée, 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.

L'intérêt d'utiliser des tampons trois-états est que cela permet de déconnecter la mémoire du bus mémoire si besoin, ce qui gère nativement le bit Output Enable. Prenons par exemple une mémoire mask ROM, pour laquelle seule la lecture est possible, pas l'écriture ni la re-programmation. Sur une ROM, le bus de commande se résume à une entrée d'horloge et le bit Output Enable, il n'y a pas de bit R/W. Une mémoire ROM utilise donc un tampon trois-état par bit du bus de données , qui est commandé par le bit Output Enable. Si Output Enable est à 0, le tampon trois-état est ouvert et la ROM est déconnectée du bus mémoire, sa sortie est désactivée. Si Output Enable est à 1, le tampon trois-état se ferme et la mémoire ROM est connectée sur le bus mémoire, sa sortie est activée.

Bus en écriture seule.

L'interface d'une mémoire SRAM ou DRAM utilise quant à elle deux tampons trois-état : un pour les lectures, un autre pour les écritures. La raison est que les tampons-trois état sont des composants unidirectionnels. Ce ne sont pas des interrupteurs qu'on ouvre et ferme, qui laissent passer le courant dans les deux sens. Les tampons trois états sont commandés par le bit Output Enable, 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. Je laisse la conception de ce petit circuit en exercice au lecteur, le circuti est vraiment très simple.

Bus en lecture et écriture.

Les interfaces Dual et Quad data rate

[modifier | modifier le wikicode]

L'interface avec le bus mémoire est une source d'optimisations assez importantes. En effet, le bus mémoire doit idéalement être très rapide, ce qui demande de jouer sur deux caractéristiques : sa fréquence et sa largeur. Pour rappel, la performance d'un bus peut se résumer à son débit binaire, à savoir la quantité d'octets qu'il peut transmettre par seconde. Et celle-ci est le produit entre sa fréquence et la largeur du bus (le nombre de bits transmis en une fois sur le bus de données). Il se trouve que la largeur du bus est relativement contrainte, le nombre de fils qu'on peut câbler sur la carte mère est limité, de même que le nombre de broches sur les puces mémoire. A l'inverse, un bus mémoire peut fonctionner à faute fréquence sans trop de problèmes.

Par contre, les puces mémoires ont le compromis inverse. Les mémoires modernes ont un temps d'accès assez élevé, qui stagne depuis des décennies. En conséquence, la fréquence réelle des mémoires stagne relativement dans le temps, ou tout du moins elle augmente très lentement au cours des décennies. A l'opposé, Les mémoires modernes sont capables d'avoir un débit binaire important : il suffit d'utiliser des cases mémoire très larges, ce qui permet de lire/écrire plein d'octets à la fois. Rein de compliqué à implémenter.

Toujours est-il que le bus mémoire et les mémoires RAM ont des caractéristiques opposées en termes de performances. Pour profiter du débit binaire élevé des RAM, il faudrait augmenter la largeur du bus, mais cela a trop de désavantages : il faudrait câbler beaucoup trop de fils. De même, impossible d'augmenter la fréquence du bus, car la fréquence de la mémoire ne suivrait pas. Mais 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.

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 la mémoire et réciproquement. La conséquence est que la fréquence et la largeur du bus mémoire sont réglés sur le plus petit dénominateur commun avec la RAM : fréquence basse pour rester compatible avec la RAM, largeur restreinte pour respecter les contraintes du bus.

Mémoire SDR.

Le préchargement 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 qui compense exactement la différence de fréquence. 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 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 à un registre qui accumule les 128 bits lus ou à écrire, couplé à des multiplexeurs pour envoyer les 64 bits adéquats sur le bus.

Mémoire DDR.

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.

Le préchargement augmente donc le débit théorique maximal, en optimisant l'interface mémoire. L'interface mémoire fonctionne à double ou quadruple fréquence, voire plus, pour se synchroniser avec le bus mémoire. Elle doit interfacer un cœur mémoire très large avec un bus mémoire moins large, et doit aussi envoyer une donnée très large en plusieurs morceaux sur le bus. L'implémentation demande un registre et des multiplexeurs. A noter que 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.

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 canal 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.


Le bus mémoire est en soi très simple : c'est juste un ensemble de fils, avec quelques circuits annexes qui servent à interfacer ce bus avec la processeur et la mémoire. En somme, des fils et de la glue logic. Néanmoins, il y a quand même des choses à dire dessus, surtout qu'il ne prend pas la même forme sur les ordinateurs PC et sur les autres architectures.

Barrette de mémoire RAM.

La plupart des PC commerciaux utilisent des barrettes de RAM, qu'on peut retirer de la carte mère si besoin. Le bus mémoire est donc relié à un connecteur standardisé, appelé slot mémoire, dans lequel on insère une barrette de RAM. La barrette de RAM est en soi un morceau de plastique sur lequel on place les puces mémoires, avec des broches dorées qui font contact avec le connecteur, et des interconnexions pour relier les puces aux broches dorées.

Slots mémoires.

Par contre, les autres systèmes n'utilisent pas de barrettes de RAM. Ils sont fournit avec une quantité de RAM bien précise, qu'on ne peut pas upgrader. La RAM est alors soudée sur la carte mère, et le bus mémoire est une connexion directe entre processeur et mémoire RAM. Il n'y a pas de connecteur dédié, juste des puces mémoire. De nombreux ordinateurs portables font ça, mais aussi les smartphones, les microcontrôleurs ou d'autres systèmes du même genre.

Les systèmes anciens avaient des bus mémoires assez réduits, peu larges, en raison de contraintes techniques. Il était intéressant de limiter la taille du bus mémoire pour économiser des broches, que ce soit sur le processeur ou la mémoire RAM. Les systèmes modernes n'ont pas ce problème, l'évolution de la technologie permet au contraire d'avoir des bus mémoire assez large. Il s'agit de deux contraintes différentes : soit on économise des broches au détriment de la performance, soit on sacrifie beaucoup de fils/broches pour avoir des performances excellentes. Les deux cas donnent des contraintes très différentes, voyons comment les deux contraintes façonnent le bus mémoire.

Les bus mémoire réduits : l'économie de broches

[modifier | modifier le wikicode]

Faire des économies sur le bus mémoire peut viser plusieurs objectifs. Il est possible de réduire le nombre de fils du bus mémoire, de réduire le nombre de broches du processeur ou d'économiser les broches de la mémoire RAM/ROM. Les trois objectifs sont assez différents, et certains sont plus utiles que d'autres. Par exemple, un processeur a besoin de beaucoup plus de broches qu'une mémoire pour faire son travail, vu que l'interface d'un processeur est assez complexe. 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. À l'inverse, les mémoires RAM/ROM peuvent parfaitement s'en passer, vu que leur interface est assez simple. Les contraintes entre processeur et mémoires RAM/ROM sont donc opposées.

Une méthode intéressante pour économiser des broches sur le processeur est d'utiliser un bus multiplexé. En théorie, cela demande d'avoir une mémoire compatible, qui tendent à être rares et plus chères. 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. Heureusement, il est possible d'implémenter un bus multiplexé avec une mémoire qui ne l'est pas, ce qui permet d'avoir le meilleur des deux mondes : cela permet d'utiliser un processeur et une mémoire à bas prix, tout en ayant des processeurs avec peu de broches. Mais cela demande de faire quelques modifications sur le bus mémoire pour que cela fonctionne.

Un exemple est donné dans le schéma ci-dessous. Le processeur possède un bus multiplexé, alors que la mémoire EPROM 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 aussi dans le cas où la mémoire est une RAM. L'implémentation demande juste l'ajout d'un registre sur le bus d'adresse et une commande adéquate de l'entrée OE (Output Enable). 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

Les bus mémoire larges : multiples canaux et arrangement horizontal

[modifier | modifier le wikicode]

Le bus mémoire des PC modernes est très important pour les performances. 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. La solution la plus évidente est d'augmenter la fréquence des mémoires et/ou de diminuer leur temps d'accès. Mais c'est que c'est plus facile à dire qu'à faire ! Les mémoires actuelles ne peut pas vraiment être rendu plus rapides, compte tenu des contraintes techniques actuelles. La solution actuellement retenue est d'augmenter le débit de la mémoire. Et pour cela, la performance du bus mémoire est primordiale.

Le débit binaire des mémoires actuelles dépend beaucoup de la performance du bus mémoire. La performance d'un bus dépend de son débit binaire, qui lui-même est le produit de sa fréquence et de sa largeur. Diverses technologies tentent d'augmenter le débit binaire du bus mémoire, que ce soit en augmentant sa largeur ou sa fréquence. La largeur du bus mémoire est quelque peu limitée par le fait qu'il faut câbler des fils sur la carte mère et ajouter des broches sur les barrettes de mémoire. Les deux possibilités sont déjà utilisées à fond, les bus actuels ayant plusieurs centaines de fils/broches.

Pour commencer, mettons de côté la fréquence, et intéressons-nous à la largeur du bus mémoire. Les PC actuels ont des bus d’une largeur de 64 bits minimum, avec cependant possibilité de passer à 128, 192, voire 256 bits ! C'est ce qui se cache derrière les technologies dual-channel, triple-channel ou quad-channel.

Le bus mémoire a une taille de 64 bits par barrette de mémoire, avec quelques contraintes de configuration. Le dual-channel permet de connecter deux barrettes de 64 bits, à un bus de 128 bits. Ainsi, on lit/écrit 64 bits de poids faible depuis la première barrette, puis les 64 bits de poids fort depuis la seconde barrette. Le triple-channel fait de même avec trois barrettes de mémoire, le quad-channel avec quatre barrettes de mémoire. Ces techniques augmentent la largeur du bus, donc influencent le débit binaire, mais n'ont pas d'effet sur le temps de latence de la mémoire. Et ce ne sont pas les seules techniques dans ce genre.

Pour en profiter, il faut placer les barrettes mémoire d'une certaine manière sur la carte mère. Typiquement, une carte mère dual channel a deux slots mémoires, voire quatre. Quand il y en a deux, tout va bien, il suffit de placer une barrette dans chaque slot. Mais dans le cas où la carte mère en a quatre, les slots sont d'une couleur différent pour indiquer comment les placer. Il faut placer les barrettes dans les slots de la même couleur pour profiter du dual channel.

Les bus mémoire à base de liaisons point à point : les barrettes FB-DIMM

[modifier | modifier le wikicode]

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. Le bus mémoire est un bus parallèle, avec tous les défauts que ca implique quand on travaille à haute fréquence. Diverses contraintes électriques assez compliquées à expliquer font que les bus parallèles ont du mal à fonctionner à haute fréquence, la stabilité de transmission du signal est altérée.

Bus mémoire
FB-DIMM - principe

Les barrettes mémoire FB-DIMM contournent le problème en utilisant plusieurs liaisons point à point. Il y a deux choses à comprendre. La première est que chaque barrette est connectée à la suivante par une liaison point à point, comme indiqué ci-dessous. Il n'y a pas de bus sur lequel on connecte toutes les barrettes, mais une série de plusieurs liaisons point à point. Les commandes/données passent d'une barrette à l'autre jusqu'à destination. Par exemple, une commande SDRAM part du contrôleur mémoire, passe d'une barrette à l'autre, avant d'arriver à la barrette de destination. Même chose pour les données lues depuis les DRAM, qui partent de la barrette, passent d'une barrette à la suivante, jusqu’à arriver au contrôleur mémoire.

Ensuite, les liaisons point à point sont au nombre de deux par barrette : une pour la lecture (northbound channel), l'autre pour l'écriture (southbound channel). Chaque barrette est reliée aux liaisons point à point par un circuit de contrôle qui fait l'interface. Le circuit de contrôle s'appelle l'Advanced Memory Buffer, il vérifie si chaque transmission est destinée à la barrette, et envoie la commande/donnée à la barrette suivante si ce n'est pas le cas.

Bus mémoire pour les barrettes FB-DIMM, schéma détaillé.

L'avantage de cette organisation est que l'on peut facilement brancher beaucoup de barrettes mémoire sur la carte mère. Avec un bus parallèle, il est difficile de mettre plus de 4 barrettes mémoire. Plus on insère de barrettes de mémoire, plus la stabilité du signal transmis avec un bus parallèle se dégrade. Cela ne pose pas de problème quand on rajoute des barrettes sur la carte mère, car elles sont conçues pour que le signal reste exploitable même si tous les slots mémoire sont remplis. Mais cela fait qu'on a rarement plus de 4 slots mémoire par carte mère. Avec des barrettes FB-DIMM, on peut monter facilement à 8 ou 16 barrettes.


La micro-architecture d'une mémoire adressable

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

Avant toute chose, faisons un petit point terminologique. Techniquement, les cellules mémoire d'une SRAM peuvent se fabriquer de deux manières différentes. La toute première utilise tout simplement une bascule D, la seconde utilise utilise un montage de plusieurs transistors. Le terme cellule de SRAM (SRAM cell) désigne uniquement la seconde possibilité. Il faut ainsi faire la distinction entre une bascule D fabriquée avec des portes logiques, et une cellule de SRAM fabriquée directement avec des transistors. La distinction est assez subtile et pas intuitive : le terme cellule de SRAM fait référence à une méthode de fabrication de la cellule mémoire, pas à la mémoire qui l'utilise. Il est possible de créer une mémoire SRAM en utilisant des bascules D ou des cellules de SRAM, les deux sont possibles. Il s'agit là d'une distinction qui ne sera pas faite pour les autres cellules mémoire.

Nous ne reviendrons pas sur les bascules D, pour ne pas faire de redite des chapitres précédents. Par contre, nous allons étudier les cellules de SRAM. Les cellules de SRAM ont bien évoluées depuis les toutes premières versions jusqu’au SRAM actuelles. 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. En comparaison, les bascules D sont composées de 10 à 20 transistors.

La différence de nombre de transistors fait que les mémoires SRAM ont une meilleure densité, à savoir que l'on peut mettre plus de cellules SRAM sur une même surface. En général, moins la cellule contient de transistors, moins elle prend de place et plus la RAM aura une grande capacité mémoire. Par contre, cela se fait au détriment des performances. Ainsi, une SRAM fabriquée avec des bascules D sera très rapide, mais aura une faible capacité. Une SRAM fabriquée avec des cellules de SRAM aura une meilleure capacité, mais des performances moindres. En conséquence, les bascules D sont surtout utilisées pour les registres du processeur, alors que les cellules de SRAM sont utilisées pour les mémoires caches.

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]
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.

À côté du condensateur, on ajoute un transistor qui va autoriser l'écriture ou la lecture dans notre condensateur. Tant que le transistor se comporte comme un interrupteur ouvert, le condensateur est isolé du reste du circuit : pas d'écriture ou de lecture possible. Il faut l'ouvrir pour lire ou écrire dans le condensateur.

La cellule de DRAM a donc deux broches : une broche de données connectée indirectement au condensateur à travers le transistor MOS, une broche de commande connectée à la grille du transistor MOS. La broche de commande est connectée à un fil appelé la word line ou ligne de mot, la broche de données est connectée à un fil appelé la bitline ou ligne de bit. Retenez bien ces termes, nous les utiliserons beaucoup dans ce qui suit.

1T-DRAM.

Le condensateur est connecté au transistor MOS d'un côté, à la masse (le 0 volts) de l'autre. Du moins, c'était le cas avant, les DRAM actuelles ne connectent pas le condensateur à la masse, mais à une tension égale à la moitié de la tension d'alimentation, pour des raisons qu'on expliquera plus tard. Le condensateur stocke alors un 1 quand la tension est positive, un 0 quand elle est négative.

Le rafraichissement mémoire

[modifier | modifier le wikicode]

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.

La lecture et l'écriture d'une cellule de 1T-DRAM

[modifier | modifier le wikicode]

Le circuit précédent a deux broches : une broches de données et une broche de contrôle. La broche de données est celle sur laquelle on récupère un bit lors d'une lecture, mais aussi sur laquelle on envoie le bit lors d'une écriture. Elle est connectée à la ligne de bit. La broche de commande sert à connecter le condensateur au bus de données lors d'une lecture ou écriture, elle est connectée à la ligne de mot.

Lors d'une écriture, la broche de données est mise à 0 ou 1. Si elle est mise à 0, le condensateur va se vider intégralement dans le fil et restera à zéro une fois vidé. Si l'entrée-sortie est mise à 1, le condensateur se remplit, s'il n'est pas déjà remplit. Au final, un 1 sera stocké dedans.

Lors d'une lecture, le condensateur est connecté sur la broche de données et se vide entièrement dedans. Il faut le récrire après chaque lecture sous peine de perdre le contenu de la cellule de DRAM. Il s'agit d'une forme de rafraichissement mémoire, dans le sens où on doit rafraichir le contenu de la cellule mémoire 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é. Les circuits de rafraichissement sur lecture et celui d'amplification sont souvent fusionnés en un seul circuit qui fait les deux, comme on le verra plus tard.

La conception du condensateur

[modifier | modifier le wikicode]

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. Un autre avantage est que les deux peuvent s'empiler l'un au-dessus de l'autre ce qu'on ne peut pas faire avec des transistors CMOS, ce qui améliore encore la densité des DRAM par rapport aux autres mémoires.

Au tout début, le transistor et le condensateur étaient placés l'un à côté de l'autre, sur le même plan. Le condensateur était alors appelé un condensateur planaire. Mais depuis les années 80, le condensateur et le transistor sont placés l'un au-dessus de l'autre, ou l'un en-dessous de l'autre. Si le condensateur est placé au-dessus du transistor, on parle de condensateur empilés (stacked capacitor). Mais si le condensateur est placé en-dessous du transistor, on parle de condensateur enterré (trench capacitor).

La taille d'une cellule de DRAM est souvent mesurée en utilisant une mesure appelée le . F est approximativement égal à la finesse de gravure, mais prenez garde à ne pas faire de comparaison avec la finesse de gravure des transistors CMOS. L'aire est l'unité de base pour comparer la taille des cellules DRAM, qui est un multiple de , et vaut donc : . Il existe trois types de DRAM en fonction de la place prise par la cellule de DRAM : les DRAM où la taille de la cellule mémoire est de 8 fois celle d'une unité de base, les DRAM où c'est seulement 6 fois, et les DRAM où c'est seulement 4 fois.

L'arrangement le plus simple est l'arrangement 4F2. Avec lui, les cellules de DRAM sont carrées. Il ne s'agit pas de l'arrangement le plus utilisé, car les cellules mémoires ne sont pas assez denses pour le moment. Le tout est illustré ci-dessous. La cellule de DRAM est le carré orange, les lignes de bits sont en vert et les lignes de mot en jaune. On voit que le transistor est situé au-dessus, que le transistor est composé des structures 6, 7 et 8 (source, grille et drain), avec une connexion directe à la ligne de mot sur la grille en 7. La ligne de bit est située tout en-dessous, elle est connectée au drain (le numéro 8).

1. Ligne de mots 2. Ligne de bits 3. Condensateur 4. Taille d'une cellule 5. Condensateur 6. Source 7. Canal 8. Drain 9. Film isolant de grille

L'arrangement actuel est cependant différent, car les cellules de DRAM ne sont pas carrées, mais rectangulaires. Dans larrangement 8F2, les cellules de DRAM sont deux fois plus longues que larges. Le tout donne ce qui est illustré dans le schéma suivant, où la cellule de DRAM est le rectangle orange, les lignes de bits sont en vert et les lignes de mot en jaune. La forme rectangulaire de la cellule de DRAM impose de placer les lignes de bit et de mot d'une manière bien précise. On s'attendrait que à ce que les lignes de mot soient deux fois plus éloignées que les lignes de bits (ou l'inverse), mais ce n'est pas le cas ! La distance est la même entre toutes les cellules. Pour cela, les cellules de DRAM ne sont pas alignées, mais décalées d'une ligne à l'autre.

1. Ligne de mots 2. Ligne de bits 3. Condensateur 4. Taille d'une cellule

Larrangement 6F2 utilise des cellules de mémoire 1.5 fois plus longues que large. Il est utilisé depuis les années 2010.

Les DRAM à base de transistors : 3T-DRAM et 2D-DRAM

[modifier | modifier le wikicode]

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.

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

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 | modifier le wikicode]

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 | modifier le wikicode]

Les mémoires EPROM, EEPROM et Flash sont fabriquées avec des transistors à grille flottante (un par cellule mémoire). On peut les voir comme une sorte de mélange entre transistor et condensateur. Un transistor MOS normal est composé d'une grille métallique et d'une tranche de semi-conducteur, séparés par un isolant, ce qui en fait un mini-condensateur. Un transistor à grille flottante, quant à lui, possède deux armatures et deux couches d'isolant. La seconde armature est celle qui sert de condensateur, celle qui stocke un bit : il suffit de la remplir d’électrons pour stocker un 1, et la vider pour stocker un 0. La première armature sert de grille de contrôle, de signal qui autorise ou interdit le remplissage de la seconde armature.

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

Afin de simplifier l'exposé, nous allons étudier une mémoire série où chaque case mémoire fait 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 | modifier le wikicode]

Une mémoire bit-adressable a un plan mémoire rudimentaire. Quand on sélectionne un bit, avec son adresse, le bit est recopié sur le bus de données. Dit autrement, la cellule mémoire associée est connecté sur le bus de données 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 case mémoire > 1)

[modifier | modifier le wikicode]

Après avoir vu le cas des mémoires bit-adressables, il est temps d'étudier les mémoires quelconques. Sur les mémoires les plus simples, chaque adresse mémoire est associée à un octet. On parle alors de mémoire adressable à l'octet. Il existe d'autres mémoires RAM/ROM où une adresse mémoire identifie un groupe de 2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 32, 64 bits. De telles mémoires sont découpées en cases mémoire, chacune contenant N bits. Les mémoires modernes ont des cases mémoire de 64, 128, voire 256 bits.

Surprenamment, ces mémoires peuvent être conçues en utilisant plusieurs mémoires bit-adressables. Par exemple, prenons une mémoire adressable à l'octet. Elle peut être construite en prenant 8 mémoires bit-adressable : une par bit de l'octet. Et le raisonnement se généralise pour des cases mémoire de N bits : il faut alors N mémoires bit-adressable. Prenons l'exemple d'une mémoire où chaque case mémoire fait deux bits, à savoir qu'une adresse mémoire sélectionne un groupe de 2 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 case mémoire, alors que l'autre stock le bit de poids fort.

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 une case mémoire 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 à une case mémoire.

Plan mémoire, avec les bitlines.

Là encore, chaque colonne peut utiliser des lignes de bits différentielles ou croisées.

La connexion des broches de commande : le transistor et le signal de sélection

[modifier | modifier le wikicode]

É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 de la case mémoire 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.

Pour simplifier, partons du principe qu'il y a un octet par adresse. 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 de l'octet. Vu que tous les bits de l'octet 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 de l'octet.

Signal de sélection et case mémoire.

Le même principe est utilisé si une adresse mémoire correspond à un groupe de plusieurs octets, ou un groupe de N bits avec N quelconque.

Le cas des lignes de bit simples et repliées

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

Les lignes de bit ont une capacité parasite qui pose de nombreux problèmes

[modifier | modifier le wikicode]
La ligne de bit a une capacité parasite, ce qui pose problèmes lors de la lecture d'une cellule de DRAM.

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  : le circuit obtenu est un circuit RC. Le condensateur est appelé la capacité parasite de la ligne de bit.

Sur les mémoires DRAM, la capacité parasite est plus grande que la capacité de la cellule de DRAM. Une cellule de DRAM a une capacité de l'ordre de la dizaine de picoFarads, le condensateur stocke quelques millions d'électrons. La ligne de bit a une capacité parasite qui est au moins d'un ordre de grandeur plus grand. Pour les anciennes DRAMA de 16 Kibioctets, la capacité parasite était 3 à 6 fois plus grande. Pour les premières mémoires de 64 Kibioctets, elle est passée à 8-10 fois plus grande. Les mémoires modernes ont un ratio différent, mais qui est fortement influencé par un paquet d'optimisations, qu'on verra dans la suite.

La capacité parasite atténue le signal fournit par la cellule de DRAM/SRAM

[modifier | modifier le wikicode]

Imaginons maintenant que l'on lise une cellule de DRAM. Lors d'une lecture, la cellule est connectée sur la ligne de bit et se vide dedans. Les électrons stockés dans la cellule se dispersent dans la ligne de bit, ce qui donne une certaine tension dans la ligne de bit. Et c'est cette tension qui code pour le 1/0 lu. Le truc, c'est que la tension de sortie dépend non seulement de la cellule de DRAM, mais aussi de la capacité parasite de la ligne de bit.

Pour rappel, la cellule DRAM stocke une certaine quantité d'électron, une certaine quantité de charges électriques, qui vaut : . Lors d'une lecture, la charge est répartie dans la la ligne de bit et la cellule DRAM. La tension de sortie se calcule approximativement en prenant la charge Q et en divisant par la capacité totale, ligne de bit inclue. La capacité totale de l'ensemble est la somme de la capacité de la cellule et de la capacité parasite : , ce qui donne :

Ce que l'équation dit, c'est que la tension stockée dans la cellule et la tension en sortie de la ligne de bit ne sont pas égales. La tension de la ligne de bit est bien plus faible, elle est grandement atténuée. Le rapport est généralement proche de 10, voire 50, ce qui fait qu'une tension de 5 volts dans la cellule DRAM se transforme en une tension de moins d'un Volt en sortie. Le seul moyen pour corriger ce problème est d'amplifier la tension de sortie. Le même problème a lieu pour les mémoires SRAM, bien que d'une manière un peu différente.

La solution pour cela est d'amplifier le signal présent sur la ligne de bit, afin d'obtenir un 0/1 valide en sortie. Divers optimisations visent aussi à réduire la capacité parasite de la ligne de bit.

La capacité parasite ralentit les lectures

[modifier | modifier le wikicode]

Plus haut, on a dit qu'une ligne de bit se modélise bien en mettant une résistance R en série avec une capacité parasite  : le circuit obtenu est un circuit RC. 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.

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.

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.

Pour résumer, selon la longueur des lignes de bits, 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 vers la fin du chapitre.

L'amplificateur de tension

[modifier | modifier le wikicode]

Plus haut on a vu que la lecture d'une cellule de DRAM génère une tension très faible dans la ligne de bit, insuffisante pour coder 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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'organisation du plan mémoire

[modifier | modifier le wikicode]

Il est important de réduire la capacité parasite et la résistance de la ligne de bit. Il se trouve que les deux sont proportionnelles à sa longueur : plus la ligne de bit est longue, plus sa résistance R sera élevée, plus sa capacité parasite le sera aussi. Réduire la taille des lignes de bit est donc une bonne solution. 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 case mémoire larges, pour réduire la taille des colonnes. Mais d'autres optimisations du plan mémoire permettent d'obtenir des lignes de bit plus petites, à capacité et largeur de case mémoire inchangée.

La technique du wire partitionning

[modifier | modifier le wikicode]

L première technique visant à réduire la longueur des lignes de bit s'appelle le wire partitionning. Elle consiste à couper une longue ligne de bits en plusieurs lignes de bits, reliées entre elles par des répéteurs, des circuits qui régénèrent la tension. La ligne de bits est donc découpée en segments, et la tension est recopiée d'un segment à l'autre.

Ainsi, si une cellule mémoire est reliée à un segment, la tension augmente dans ce fil assez rapidement, vu que le segment est très court. Et pendant ce temps, la tension se propage au segment suivant par l'intermédiaire du répéteur, la tension augmente rapidement vu que ce segment aussi est court. Et ainsi de suite, la tension se propage d'un segment à un autre.

L'avantage principal est que si l'on coupe une ligne de bit en N segments, le temps mis pour qu'un bit se propage sur une ligne de bit est divisé par N², le carré de N. Mais il y a un désavantage : la consommation d’électricité est plus importante. Les répéteurs consomment beaucoup de courant, sans compter que le fait que la tension monte plus vite a aussi un impact significatif. Il est possible de démontrer que la consommation d'énergie augmente exponentiellement avec N.

Pour éviter cela, l'idée est de désactiver les parties de la ligne de bit qui ne sont pas nécessaires pour effectuer une lecture. Pour cela, les répéteurs sont remplacés par des tampons trois-états, ce qui permet de déconnecter un segment. La mémoire a juste à désactiver les segments de la ligne de bit qui sont situés au-delà de la cellule mémoire à lire/écrire, et de n'activer que ceux qui sont entre la cellule mémoire et le bus de données.

Wire partitionning

Nous verrons que cette technique n'est pas utilisée que sur les mémoires RAM, mais est aussi utilisée sur les mémoires associatives, ou sur certaines mémoires caches. Nous en reparlerons aussi dans le chapitre sur les processeurs à exécution dans le désordre, qui incorporent des pseudo-mémoires spécialisées qui font grand usage de cette technique.

L'agencement en colonne de donnée ouvertes

[modifier | modifier le wikicode]

La première optimisation consiste à 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é. Mais cette organisation complexifie l'amplificateur au milieu de la mémoire. Et 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.

L'amplificateur de lecture est un amplificateur différentiel. Lors de l'accès à une cellule mémoire, la cellule sélectionnée se trouve sur une moitié de la ligne de bit. La ligne de bit en question est comparée à l'autre moitié de la ligne de bit, sur laquelle aucune cellule ne sera connectée. Cette organisation est dite en ligne de bits ouvertes.

Optimisations du plan mémoire pour réduire la taille des bitlines.

Pour donner un exemple, voici l'intérieur d'une mémoire DRAM de 16 kibioctets, d'origine soviétique. La ligne du milieu regroupe l'amplificateur de lecture et le multiplexeur des colonnes.

K565RU3 die photo

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.

Pour donner un exemple, voici l'intérieur d'une mémoire DRAM de 1 mébioctet, de marque Sanyo. Les deux lignes sont les amplificateur de lecture. On voit qu'ils sont situés de manière à réduire au maximum la longueur des lignes de bit.

Sanyo Fast 1M DRAM

La pré-charge des lignes de bit

[modifier | modifier le wikicode]
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.

Pour réduire le temps de lecture/écriture, 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 de la case mémoire adressée à 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 cases mémoire : le 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 | modifier le wikicode]

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 | modifier le wikicode]

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 de trois composants : une mémoire qui mémorise les lignes, et un multiplexeur qui sélectionne la colonne, avec un tampon de ligne entre les deux. 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 cases mémoire à partir d'une mémoire contenant B fois moins de cases mémoire, mais dont chacun des cases mémoire seront B fois plus grosses.

Mémoire à row buffer - principe.

Le tampon de ligne s'intercale entre la mémoire à adressage linéaire et la sélection des colonnes. Chaque accès lit ou écrit une « super-case mémoire » de la mémoire interne, et le copie dans le tampon de ligne. Puis, un multiplexeur sélectionner la case mémoire dans celui-ci.

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 case mémoire par case mémoire. 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'une case mémoire 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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éussi à 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 cases mémoire 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 à une case mémoire attribué à un programme peuvent déborder sur les cases mémoire d'un autre programme, avec des conséquences assez variables. Par exemple, un virus présent en mémoire pourrait interagir avec la case mémoirequi 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.

L'accès en rafale

[modifier | modifier le wikicode]

L'accès en rafale est un accès mémoire qui permet de lire ou écrire plusieurs adresses consécutives 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/écrire 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, mais une seule, ce qui 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.

Le nombre d'adresses consécutives lues lors d'une rafale 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. C'est le cas sur les mémoires SDRAM, où on peut choisir s'il faut lire 1, 2, 4, ou 8 octets en rafale.

L'accès en rafale séquentiel, linéaire et entrelacé

[modifier | modifier le wikicode]

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. Sur certaines mémoires, la rafale peut commencer n'importe où. Mais sur d'autres, le mode séquentiel est parfois restreint et ne peut démarrer qu'à certaines adresses bien précises. Par exemple, pour une mémoire dont le mot mémoire fait 4 octets bits, avec une rafale de 8 mots, on ne peut démarrer les rafales qu'à des adresses multiples de 8 * 4 = 64 octets. Il s'agit d'une contrainte dite d'alignement de rafale. Pour le dire autrement, la mémoire est découpées en blocs qui font la même taille qu'une rafale, et une rafale ne peut transmettre qu'on bloc complet en partant du début.

Le mode linéaire est un petit peu plus compliqué. Il lit un bloc de taille fixe, qui est aligné en mémoire, comme expliqué dans le paragraphe précédent. Mais il peut commencer l'accès en rafale n'importe où dans le bloc, tout en lisant/écrivant la totalité du bloc. Par exemple, prenons une rafale de 8 octets, dont les octets ont les adresses 0, 1, 2, 3, 4, 5, 6, et 7. Un accès séquentiel aligné doit commencer à l'adresse 0. Mais une rafale en mode linéaire peut très bien commencer par lire ou écrire l'octet numéro 3, par exemple. Dans ce cas, on commence par lire l'octet numéroté 3, puis le 4, le 5, le 6 et le 7. Puis, l'accès reprend au bloc 0, avant d’accéder aux blocs 1, 2 et 3. En clair, la mémoire est découpée en blocs de 8 octets 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 à cette adresse, puis reprend au début du bloc une fois arrivé au bout.

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 | modifier le wikicode]

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. 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.

Les banques et rangées

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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.

L'arrangement vertical

[modifier | modifier le wikicode]

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.

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.

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.

L'entrelacement (interleaving)

[modifier | modifier le wikicode]

La technique de l'entrelacement utilise un arrangement vertical assez spécifique, afin de gagner en performance. Avec une mémoire sans entrelacement, on doit attendre qu'un accès mémoire soit fini avant d'en démarrer un autre. Avec l'entrelacement, on peut réaliser un accès mémoire sans attendre que les précédents soient finis. L'idée est d’accéder à plusieurs banques en parallèles. Pendant qu'une banque est occupée par un accès mémoire, on en démarre un nouveau dans une autre banque, et ainsi de suite jusqu’à avoir épuisé toutes les banques libres. L'organisation en question se marie bien avec l'accès en rafale, si des adresses consécutives sont placés dans des banques séparées.

Pipemining mémoire

Précisons que le temps d'accès mémoire ne change pas beaucoup avec l'entrelacement. Par contre, on peut faire plus d'accès mémoire simultanés. Cela implique que la fréquence de la mémoire augmente avec l'entrelacement. Au lieu d'avoir un cycle d'horloge assez long, capable de couvrir un accès mémoire entier, le cycle d'horloge est plus court. On peut démarrer un accès mémoire par cycle d'horloge, mais l'accès en lui-même prend plusieurs cycles. Le nombre de cycles d'un accès mémoire augmente, non pas car l'accès mémoire est plus lent, mais car la fréquence est plus élevée. D'un seul cycle par accès mémoire, on passe à autant de cycles qu'il y a de banques.

Les mémoires à entrelacement ont un débit supérieur aux mémoires qui ne l'utilisent pas, essentiellement car la fréquence a augmentée. Rappelons que le débit binaire d'une mémoire est le produit de sa fréquence par la largeur du bus. L'entrelacement est une technique qui augmente le débit en augmentant la fréquence du bus mémoire, sans pour autant changer les temps d'accès de chaque banque. Tout se passe comment si la fréquence de chaque banque restait la même, mais que l'entrelacement trichait en augmentant la fréquence du bus mémoire et en compensant la différence par des accès parallèles à des banques distinctes.

L'entrelacement basique

[modifier | modifier le wikicode]

Sans entrelacement, 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 adresses consécutives 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 adresses consécutives 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 la case mémoire.

Adresse mémoire d'une mémoire entrelacée

En faisant cela, on peut accéder à un plusieurs cases mémoire consécutives 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 case mémoire l'un après l'autre. Avec l’entrelacement parallèle, on lit plusieurs cases mémoire consécutives en même temps, en accédant à toutes les banques en même temps, avant d'envoyer chaque mot mémoire l'une 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 adresses consécutives à partir de cette adresse initiale.
  • Une autre méthode démarre un nouvel accès mémoire à chaque cycle d'horloge, pour lire des mot mémoire 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 mots mémoire consécutifs est rendu plus rapide.

Les mémoires à entrelacement par décalage

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 mémoires multiports

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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. La méthode utilisée dépend de si la cellule mémoire est fabriquée avec une bascule, ou avec une cellule SRAM. Elle dépend aussi de l'interface de la bascule.

Les mémoires multiport les plus simples sont les mémoires double port, avec un port de lecture et un d'écriture. Il suffit de prendre des cellules à double port, avec un port de lecture et un d'écriture. Il suffit de connecter la sortie de lecture à un multiplexeur, et l'entrée d'écriture à un démultiplexeur.

Intérieur d'une RAM fabriquée avec des registres et des multiplexeurs.

On peut améliorer la méthode précédente pour augmenter le nombre de ports de lecture assez facilement : il suffit de connecter plusieurs multiplexeurs.

Mémoire multiport faite avec des MUX-DEMUX

Les choses sont plus compliquées avec les cellules mémoires à une seule broche d'entrée-sortie, ou à celles connectées à une ligne de bit. Dans les mémoires vues précédemment, chaque cellule mémoire est reliée à 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 | modifier le wikicode]

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 | modifier le wikicode]

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, il y a un conflit d'accès aux banques. Un choix devra être fait et un des deux ports devra être mis en attente.

Mémoire à multiports par banques.

Les mémoires à détection et correction d'erreur

[modifier | modifier le wikicode]

La performance et la capacité ne sont pas les deux seules caractéristiques importantes des mémoires. On attend d'elles qu'elles soient fiables, qu'elles stockent des données sans erreur. Si on stocke un 0 dans une cellule mémoire, on ne souhaite pas qu'une lecture ultérieure renvoie un 1 ou une valeur illisible. Malheureusement, ce n'est pas toujours le cas et quelques erreurs mineures peuvent survenir. Les erreurs en question se traduisent le plus souvent par l'inversion d'un bit : un bit censé être à 0 passe à 1, ou inversement. 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.

La majorité de ces inversions de bits proviennent de l'interaction de particules à haute énergie avec le circuit. Les plus importantes sont les rayons cosmiques, des particules à haute énergie produites dans la haute atmosphère et qui traversent celle-ci à haute vitesse. Les secondes plus importantes sont les rayons alpha, provenant de la radioactivité naturelle qu'on trouve un peu partout. Et, ironie du sort, ces rayons alpha proviennent souvent du métal présent dans la puce elle-même ou de son packaging !

Les techniques pour détecter et corriger ces erreurs sont nombreuses, comme nous l'avions vu dans le chapitre dédié sur les circuits de correction d'erreur. Mais elles ne sont pas appliquées de manière systématique, seulement quand ça en vaut la peine. Pour ce qui est du processeur, les techniques sont très rarement utilisées et sont réservées à l'automobile, l'aviation, le spatial, etc. Pour les mémoires les techniques sont déjà plus fréquentes sur les ordinateurs personnels, bien que vous n'en ayez pas vraiment conscience.

La première raison à cela est que les mémoires sont plus sujettes aux erreurs. Historiquement, du fait de leur conception, les mémoires sont plus sensibles à l'action des rayons cosmiques ou des particules alpha. Leur plus grande densité, le fait qu'elles stockent des bits sur de longues périodes de temps, leur processus de fabrication différent, tout cela les rend plus fragiles. La seconde raison est qu'il existe des techniques assez simples et pratiques pour rendre les mémoires tolérantes aux erreurs, qui ne s'appliquent pas pour le processeur ou les autres circuits. Il s'agit ni plus ni moins que l'usage de codes ECC, que nous avions abordé au début du cours dans un chapitre dédié, mais que nous allons rapidement réexpliquer dans ce qui suit.

Les mémoires ECC

[modifier | modifier le wikicode]

Les codes de détection et de correction d'erreur ajoutent des bits de correction/détection d'erreur aux données mémorisées. A chaque octet, on rajoute quelques bits calculés à partir des données de l'octet, qui servent à détecter et éventuellement corriger une erreur. Plus le nombre de bits ajoutés est important, plus la fiabilité des données sera importante. Ils sont généralement assez simples à mettre en œuvre, pour un cout modéré en circuit et en performance.

Il existe différents codes de ce type. Le plus simple est le bit de parité mémoire, qui ajoute un bit à l'octet mémorisé, de manière à ce que le nombre de bits à 1 soit pair. En clair, si on compte les bits à 1 dans l'octet, bit de parité inclus, alors le résultat est pair. Cela permet de détecter qu'une erreur a eu lieu, qu'un bit a été inversé, mais on ne peut pas corriger l'erreur. Un bit de parité indique qu'un bit a été modifié, mais on ne sait pas lequel.

Lorsqu'on lisait un octet dans la mémoire, le contrôleur mémoire calculait le bit de parité de l'octet lu. Le résultat était alors comparé au bit de parité stocké dans l'octet. Si les deux concordent, on suppose qu'il n'y a pas eu d'erreurs. C'est possible qu'il y en ait eu, comme une double erreur qui inverse deux bits à la fois, mais de telles erreurs ne se voient pas avec un bit de parité. Par contre, si les deux bits de parité sont différents, alors on sait qu'il y a eu une erreur. Par contre, vu qu'on ne sait pas quel bit a été inversé, on sait que la donnée est corrompu, sans pouvoir récupérer la donnée originale. Aussi, quand l'ordinateur détectait une erreur, il n'avait pas d'autre choix que de stopper l'ordinateur et d'afficher un écran bleu dans le pire des cas.

Les mémoires DRAM d'avant les années 1990 utilisaient systématiquement un bit de parité par octet. Les mémoires de l'époque étaient assez peu fiables, du fait de processus de fabrication pas encore perfectionnés, et l'usage d'un bit de parité permettait de compenser cela. Les tous premiers ordinateurs mémorisaient les bits de parité dans une mémoire séparée, adressée en parallèle de la mémoire principale. Mais depuis l'arrivée des barrettes de mémoire, les bits de parité sont stockés dans les cases mémoire elle-mêmes, sur la barrette de mémoire. Depuis les années 1990, l'usage d'un bit de parité est tombé en désuétude avec l'amélioration de la fiabilité intrinsèque des DRAM.

Une barrette mémoire contenant 9 puces mémoires (les boitiers noirs). Il y en a un par bit et vous remarquerez qu'il y a 9 puces mémoires : 8 pour les données des octets, le 9ème pour les bits de parité.

Les mémoires ECC utilisent un code plus puissant qu'un simple bit de parité. Le code en question permet non seulement de détecter qu'un bit a été inversé, mais permettent aussi de déterminer lequel. Le code en question ajoute au minimum deux bits par octet/adresse. Nous avions vu quelques codes de ce genre dans le chapitre sur les circuits de correction d'erreur, nous ne ferons pas de rappels, qui seraient de toute façon inutiles dans ce chapitre. La majorité des codes utilisés sur les mémoires ECC permettent de corriger l'inversion d'un bit. De plus, ils permettent de détecter les situations où deux bits ont été inversés (deux erreurs simultanés) mais sans les corriger. Mais le cout en circuits est plus conséquent : il y a environ 4 bits d'ECC par octet.

Là encore, la détection/correction d'erreur est le fait de circuits spécialisés qui calculent les bits d'ECC à partir de l'octet lu, et comparent le tout aux bits d'ECC mémorisés dans la RAM. Les circuits d'ECC se situent généralement dans le contrôleur mémoire, mais se peut qu'ils soient intégrés dans la barrette mémoire. La différence entre les deux est une question de compatibilité. S'ils sont intégrés dans la barrette mémoire, la gestion de l'ECC est complétement transparente et est compatible avec n'importe quelle carte mère, peu importe le contrôleur mémoire utilisé. Par contre, si elle est le fait du contrôleur mémoire, alors il peut y avoir des problèmes de compatibilité. Une barrette non-ECC fonctionnera toujours, mais ce n'est pas le cas des barrettes ECC. Le contrôleur mémoire doit gérer l'ECC et être couplé à des barrettes ECC pour que le tout fonctionne. Si on branche une mémoire ECC sur un contrôleur mémoire qui ne gère pas l'ECC, l'ordinateur ne démarre même pas. Notons que de nos jours, le contrôleur mémoire est intégré dans le processeur : c'est ce dernier qui gère l'ECC.

L'usage de l'ECC sur les ordinateurs personnels est assez complexe à expliquer. Précisons d'abord qu'il est rare de trouver des mémoires ECC dans les ordinateurs personnels, alors qu'elles sont systématiquement présentes sur les serveurs. Par contre, les mémoires cache d'un processeur de PC utilisent systématiquement l'ECC. En effet, si les DRAM sont sensibles aux erreurs, mais que les SRAM le sont tout aussi ! Les caches aussi peuvent subir des erreurs, et ce d'autant plus que le processeur est miniaturisé. Et pour cela, les caches des CPU actuels incorporent soit des bits de parité, soit de la SRAM ECC. Tout dépend du niveau de cache, comme on le verra dans le chapitre sur le cache.

Le memory scrubbing

[modifier | modifier le wikicode]

La plupart des erreurs ne changent qu'un seul bit dans un octet, mais le problème est que ces erreurs s'accumulent. Entre deux accès à un octet, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le memory scrubbing, qui permet de résoudre le problème au prix d'un certain cout en performance.

L'idée est de vérifier chaque octet régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque octet toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Et évidemment, le memory scrubbing a un cout en performance, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats.

Précisons qu'il ne s'agit pas d'un rafraichissement mémoire, même si ça a un effet similaire. Disons que lors de chaque "pseudo-rafraichissement", l'octet est purgé de ses erreurs, pas rafraichit. D'ailleurs, les mémoires SRAM peuvent incorporer du memory scrubbing, et de nombreuses mémoires cache ne s'en privent pas, comme on le verra dans le chapitre sur le cache. Cependant, sur les mémoires DRAM, le memory scrubbing peut se faire en même temps que le rafraichissement mémoire, afin de fortement limiter son cout en performance.

Le memory scrubbing peut compléter soit l'ECC, soit un bit de parité. Imaginons par exemple qu'on le combine avec un bit de parité. Le bit de parité permet de détecter qu'une erreur a eu lieu. Mais si deux erreurs ont lieu, le bit de parité ne pourra pas détecter la double erreur. Le bit de parité indiquera que la donnée est valide. Pour éviter cela, on utilise le memory scrubbing pour éviter que deux erreurs consécutives s'accumulent, permettant de détecter un problème dès la première erreur. On n'attend pas de lire la donnée invalide pour vérifier le bit de parité.

Le même raisonnement a lieu avec l'ECC, avec quelques différences. Au lieu d'attendre que deux erreurs aient lieu, ce que l'ECC peut détecter, mais pas corriger, on effectue des vérifications régulières. Si une vérification tombe entre deux erreurs, elle corrigera la première erreur avant que la seconde survienne. Au final, on a une mémoire non-corrompue : l'ECC corrige la première erreur, puis la suivante, au lieu de laisser deux erreurs s'accumuler et d'avoir un résultat détectable mais pas corrigeable.

Les mémoires à tampon de ligne optimisées

[modifier | modifier le wikicode]

Dans cette section, nous allons voir les optimisations rendues possibles sur les mémoires à tampon de ligne. Ce sont techniquement des mémoires à tampon de ligne. Pour rappel, elles sont organisées en lignes et colonnes. Elles sont composées d'une mémoire dont les cases mémoire sont des lignes, d'un tampon de ligne pour mémoriser la ligne en cours de traitement, et d'un multiplexeur/démultiplexeur pour lire/écrire les mots mémoires adressés dans la ligne.

Mémoire à tampon de ligne à registre.

L'implémentation du mode rafale

[modifier | modifier le wikicode]

Diverses optimisations se basent sur la présence du tampon de ligne. L'implémentation du mode rafale est par exemple grandement facilitée sur ces mémoires. Une rafale permet de lire le contenu d'une ligne d'un seul bloc, idem pour les écritures. Pour une lecture, la ligne est copiée dans le tampon de ligne, puis la rafale démarre. Les mot mémoires à lire sont alors lus dans le tampon de ligne directement, un par un. Il suffit de configurer le multiplexeur pour passer d'une adresse à la suivante. Le compteur de rafale est relié au multiplexeur, sur son entrée, et est incrémenté à chaque cycle d'horloge du bus mémoire.

Il en est de même pour l'écriture, sauf qu'il y a une étape en plus. La ligne à écrire est copiée dans le tampon de ligne, puis l'écriture en rafale a lieu dans le tampon de ligne, mot mémoire par mot mémoire, et la ligne est ensuite recopiée du tampon de ligne vers la mémoire. Vous vous demandez sans doute pourquoi copier la ligne dans le tampon de ligne avant d'écrire dedans. La réponse est que la rafale ne fait pas forcément la taille d'une ligne. Par exemple, si une ligne fait 126 octets et que la rafale en seulement 8, il faut tenir compte des octets non-modifiés dans la ligne. Sachant qu'il n'y a pas de copie partielle du tampon de ligne dans la mémoire RAM, recopier la ligne pour la modifier est la meilleure solution.

Un défaut de cette implémentation est qu'une rafale ne put pas être à cheval sur deux lignes, sauf si la RAM incorpore des optimisations complémentaires. Les rafales doivent être alignées de manière à rentrer dans une ligne complète. Pour rendre l'alignement plus facile, la taille des lignes doit être un multiple de la longueur de la rafale. De plus, les rafales doivent être alignées, que ce soit en mode séquentiel ou linéaire. Par exemple, si une rafale lit/écrit 4 octets, alors les lignes doivent faire 8 * N octets. De plus, les rafales doivent commencer à une adresse multiple de 8 octets * 4 adresses consécutives = 32 octets. Pour le dire autrement, la rafale voit la mémoire comme des blocs qui peuvent être transmis en rafale. Mais impossible de lancer une rafale au beau milieu d'un bloc, sauf à utiliser le mode rafale linéaire pour revenir au début du bloc quand on atteint la fin.

Les mémoires à cache de ligne intégré

[modifier | modifier le wikicode]

Quelques modèles de RAM à tampon de ligne ont ajouté un cache qui mémorise les dernières lignes ouvertes, ce qui permet d'améliorer les performances. Les RAM en question sont les EDRAM (enhanced DRAM), ESDRAM (enhanced synchronous DRAM), Virtual Channel Memory RAM, et CDRAM (Cached DRAM). Elles demandaient pour certaines une modification de l'interface, avec des commandes pour copier le tampon de ligne dans le cache, en plus des traditionnelles commandes de lecture/écriture. L'idée était d'avoir plusieurs lignes ouvertes en même temps, ce qui améliorait les performances dans certains scénarios.

Mémoire à cache de ligne intégré

Les optimisations des copies en mémoire

[modifier | modifier le wikicode]

Une telle organisation en tampon de ligne permet d'implémenter facilement les accès en rafale, mais aussi d'autres opérations. L'une d'entre elle est la copie de données en mémoire. Il n'est pas rare que le processeur copie des blocs de données d'une adresse vers une autre. Par exemple, pour copier 12 kibioctets qui commencent à l'adresse X, vers un autre bloc de même taille, mais qui commence à l'adresse M. En théorie, la copie se fait mot mémoire par mot mémoire, mais la technologie row clone permet de faire la copie ligne par ligne.

L'idée est de lire une ligne, de la stocker dans le tampon de ligne, puis de l'écrire à la destination voulue. Pas de passage par le bus de données, les données ne sortent pas de la mémoire. L'avantage est que la copie des données est beaucoup plus rapide. De plus, elle consomme nettement moins d'énergie, car il n'y a pas de transmission sur le bus mémoire, sans compter qu'on n'a pas d'utilisation des multiplexeurs/démultiplexeurs.

L'implémentation demande d'ajouter des registres dans la mémoire pour mémoriser les adresses de départ/destination, mais surtout d'ajouter des commandes sur le bus mémoire pour déclencher ce genre de copie. Il faut ajouter une commande de copie, qui désigne la ligne originelle et la ligne de destination, des numéros de lignes doivent être transmis dans la commande et mémorisés par la mémoire, etc.

L'implémentation est plus compliquée sur les mémoires multi-banques, car il faut prévoir de quoi copier des données d'une banque à l'autre. L'optimisation précédente ne fonctionne alors pas du tout, mais on gagne quand même un peu en performance et en consommation d'énergie, vu qu'il n'y a pas de transmission sur le bus mémoire avec toutes les lenteurs que cela implique.


Les mémoires primaires

[modifier | modifier le wikicode]
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 | modifier le wikicode]

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 | modifier le wikicode]

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 l'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.

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.
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 en reliant les sorties à un même fil. Mais elle demande que le décodeur utilise des sorties à drain/collecteur ouvert, c'est-à-dire des sorties qui peuvent soit sortir un 1, soit être déconnectées, mais ne peuvent pas fournir un 0 (ou inversement). Le OU câblé connecte au même fil les sorties dont on souhaite faire un OU. 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 portes logiques TTL ont des sorties à collecteur ouvert de ce type et on peut fabriquer un décodeur avec, mais elles sont rares. Aussi, il vaut mieux utiliser un décodeur normal et transformer ses sorties en sorties à drain/collecteur ouvert. Pour cela, il suffit d'ajouter un petit circuit en aval d'une sortie normale. Il existe deux manières pour cela : la première utilise une diode, la seconde utilise un transistor.

Les ROM à diodes

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 et autres circuits logiques programmables

[modifier | modifier le wikicode]

Les mémoires ROM et circuits combinatoires sont tous créés de la même manière : une couche de portes NON, suivi d'une couche de portes ET, suivie par une couche de portes OU. La seule différence tient dans la manière dont ces couches sont interconnectées, ainsi que dans le nombre de portes logiques. Et cela nous donne un indice sur la manière de créer des circuits programmables, dont les mémoires PROM ne sont qu'un sous-ensemble.

Les PROM sont regroupées avec d'autres circuits similaires, mais qui ne sont techniquement pas des mémoires. Ces derniers portent les doux noms de Programmable Logic Device (PLD). Il s'agit de circuits qui comprennent un grand nombre de portes logiques, dont un peu programmer les interconnexions. La programmation peut être définitive ou non, ce qui donne différents types de PLD :

  • les PLD simples, dont la programmation est définitive, comme sur une PROM ;
  • les EPLD dont la programmation est réversible après exposition aux UV comme sur les EPROM ;
  • les EEPLD dont la programmation est effaçable électriquement, comme sur les EEPROM.

Les PLD programmables une seule fois

[modifier | modifier le wikicode]

Pour le moment, nous allons uniquement parler des PLD dont la configuration est permanente. Une fois qu'on a configuré ce genre de PLD, le circuit ne peut pas être reprogrammé. Ils servent à créer n’importe quel circuit combinatoire, voire séquentiel pour les plus complexes. Leur intérêt est assez similaire à celui des PROM, comparé aux mask ROM : ils sont plus simples à utiliser. Au lieu de créer un circuit combinatoire sur mesure en interconnectant des portes logiques, autant prendre un PLD et le configurer comme on le souhaite.

Tous sont fabriqués sur le même modèle, qui est décrit dans ce qui suit. En premier lieu, on trouve une couche de portes NON, reliée aux entrées. En sortie de celle-ci, on trouve les entrées ou leur inverse. On trouve aussi une couche de portes ET et une couche de portes OU. Mais surtout : on trouve des circuits d'interconnexion qui permettent soit de relier les entrées aux portes ET, soit les sorties des portes ET aux entrées des portes OU. Les deux sont appelés respectivement la matrice ET, et la matrice OU.

Microarchitecture d'un Programmable Logic Device.

Les interconnexions sont programmables une fois, on verra comment elles sont faites plus bas. Pour l'expliquer rapidement, les deux matrices contiennent des circuits permettent de connecter/déconnecter une entrée à une sortie, qui sont représentés par des petits ronds dans le schéma ci-dessous. Pour les PLD simples, ces connecteurs sont des fusibles qui sont grillés si on met une tension trop importantes.

Par défaut, toutes les interconnexions possibles sont présentes. Pour la matrice ET, cela signifie que toutes les entrées sont reliées à chaque porte ET, que tous les minterms sont présents. Pour la matrice OU, cela veut dire que chaque porte ET sont reliées à toutes les portes OU. Lors de la programmation, on va conserver seulement une partie des interconnexions, le reste étant éliminé.

Exemple de circuit obtenu par programmation d'un PLD. Le circuit de gauche est le circuit voulu, le circuit de droite est celui sur le PLD après configuration de celui-ci.

La plupart des PLD récents ajoutent un registre sur leur sortie. Pour cela, en aval de chaque porte OU, on trouve une bascule D qui mémorise la sortie du circuit PLD. Le PLD est un circuit imprimé, qui a une sortie pour chaque porte OU, mais cette sortie n'est pas toujours connectée directement à la sortie de la bascule. La raison est qu'on trouve un MUX qui permet de choisir si on veut récupérer soit le contenu de la bascule, soit la sortie directe de la porte OU. La plupart des PLD ajoutent aussi la possibilité de prendre l'inverse de chaque sortie. Le tout est configuré par quelques bits d'entrée.

Sortie d'un circuit PLD.

La classification des PLD est assez complexe, mais on peut en distinguer trois sous-types principaux :

  • Les PLA (Programmable logic array) : les deux matrices ET et OU sont programmables.
  • Les PAL (Programmable Array Logic) : la matrice ET est programmable, mais pas la matrice OU.
  • Les PROM : la matrice OU est programmable, mais pas la matrice ET.
Matrice ET non-programmable Matrice ET programmable
Matrice OU non-programmable Circuit combinatoire PAL
Matrice OU programmable PROM PLA

Les mémoires PROM

[modifier | modifier le wikicode]
Mémoire PROM fabriquée avec des transistors bipolaires.

Avec une mémoire PROM, la matrice ET est fixée une fois pour toute. L'ensemble matrice ET + portes ET + portes NON forme un simple décodeur, aux sorties à collecteur ouvert. La couche de portes OU peut faite de portes OU câblées, mais ce n'est pas systématique.

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, octet par octet. 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 | modifier le wikicode]

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 | modifier le wikicode]
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 | modifier le wikicode]

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 octet par octet, alors que les Flash les font par paquets de plusieurs octets. Là où on peut effacer/programmer un octet 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 octets. 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 octets, pas plus), alors que les Flash ont des unités beaucoup plus larges.

Les différences entre EEPROM, Flash NOR et Flash NAND

[modifier | modifier le wikicode]

Dans ce cours, nous ferons la distinction entre EEPROM et Flash sur le critère suivant : l'effacement peut se faire octet par octet 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 octet par octet, comme l'effacement. Mais sur les mémoires Flash, elle peut se faire soit octet par octet, soit par paquets de plusieurs centaines d'octets. 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 octets 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 d'octets. 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 octet par octet Reprogrammation par blocs entiers
Effacement octet par octet 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 | modifier le wikicode]

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 | modifier le wikicode]

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.


Les toutes premières mémoires SRAM étaient des mémoires asynchrones, non-cadencées par 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.

Fabriquer une mémoire synchrone demande de rajouter des registres sur les entrées/sorties d'une mémoire asynchrone. Instinctivement, on se dit qu'il suffit de mettre des registres sur les entrées associées au bus d'adresse/commande, et sur les entrées-sorties du bus de données. Mais faire ainsi a des conséquences pas évidentes, au niveau du nombre de cycles utilisés pour les lectures et écritures. Aussi, nous allons procéder par étapes, en ajoutant des registres d'abord pour mémoriser l'adresse, puis les données à écrire, puis sur toutes les entrées-sorties. Ces trois cas correspondent à des mémoires qui existent vraiment, les trois modèles ont été commercialisé et utilisés.

Pour simplifier les explications, nous allons prendre le cas d'une mémoire avec un port de lecture séparé du port d'écriture. On peut alors ajouter des registres sur le bus d'adresse/commande, sur le port de lecture et sur le port d'écriture. Il est possible de faire la même chose sur une mémoire avec un port unique de lecture/écriture, mais laissons cela de côté pour le moment.

Les SRAM synchrones à tampon d'adresse

[modifier | modifier le wikicode]

Le premier type de SRAM synchrone que nous allons étudier est celui des SRAM synchrones à tampon d’adresse. Leur nom est assez clair et dit bien comment ces SRAM sont rendues synchrones. Elles partent d'un modèle asynchrone, sur lequel on ajoute des registres. L'adresse et les signaux de commande sont mémorisés dans des registres, mais pas les données.

Mémoire synchrone première génération.

Grâce à l'ajout du registre d'adresse, le processeur n'a pas à maintenir l'adresse en entrée de la SRAM durant toute la durée d'un accès mémoire : le registre s'en charge. Le diagramme suivant montre ce qu'il se passe pendant une lecture, avec l'ajout d'un registre sur l'adresse uniquement. On voit que sur la SRAM asynchrone, l'adresse doit être maintenue durant toute la durée du cycle d'horloge mémoire, c'est à dire durant une dizaine de cycles d'horloge du processeur. Mais sur la SRAM synchrone, l'adresse est envoyée en début du cycle seulement, l'adresse est écrite dans le registre lors d'un cycle processeur, puis maintenue par le registre pour les autres cycles processeur.

Différence entre mémoire asynchrone et synchrone avec mémorisation d'adresse uniquement.

L'écriture se passe comme sur les SRAM asynchrones, si ce n'est que l'adresse n'a pas à être maintenue. La donnée à écrire doit être maintenue pendant toute la durée de l'écriture, même si elle dure plusieurs cycles d'horloge processeur. Le processeur envoie la donnée à écrire en même temps que l'adresse, mais peut se déconnecter du bus d'adresse précocement. Les SRAM de ce style ont des lectures rapides, mais des écritures plus lentes.

Différence entre mémoire asynchrone et mémoire synchrone sans mémorisation des écritures

Avec cette organisation, les lectures et écritures ont le même nombre de cycles. La présentation de l'adresse se fait au premier cycle, la lecture/écriture proprement dite est effectuée au cycle suivant. Pour une lecture, on a le premier cycle pour la présentation de l'adresse, et le second cycle où la donnée est disponible sur le bus de données. Pour l'écriture, c'est pareil, sauf que la donnée à écrire peut être présentée dès le premier cycle, mais elle n'est terminée qu'à la fin du second cycle. De telles mémoires synchrones sont de loin les plus simples. Les toutes premières SRAM synchrones étaient de ce type, la première d'entre elle étant la HM-6508.

Les SRAM synchrones à tapon d'écriture

[modifier | modifier le wikicode]

Les SRAM synchrones à tampon d'écriture reprennent la structure précédente, et y ajoutent d'un registre sur le port d'écriture. Pour les SRAM avec un seul port, le registre sert de tampon pour les données à écrire, mais il est contourné pour les lectures, qui ne passent pas par ce registre. Un exemple de SRAM de ce type est la HM-6504, qui disposait d'un port de lecture séparé du port d'écriture. Un autre exemple est celui de la HM-6561, qui elle n'a qu'un seul port utilisé à la fois pour la lecture et l'écriture, mais qui dispose bien d'un registre utilisé uniquement pour les écritures. Lors des lectures, ce registre est contourné et n'est pas utilisé.

Mémoire à flot direct.

Les écritures anticipées

[modifier | modifier le wikicode]

L'ajout d'un registre pour les écritures permet de faire la même chose que pour les adresses, mais pour les données à écrire. 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. Les lectures comme les écritures sont plus rapides que sur les SRAM à tampon d'adresse, du moins en apparence.

Différence entre mémoire asynchrone et synchrone avec mémorisation des écritures

Du point de vue du processeur, les écritures ne prennent plus qu'un seul cycle : celui où on présente l'adresse et la donnée à écrire. Mais en réalité, la durée d'une écriture n'a pas changée, c'est juste que la SRAM effectue l'écriture au second cycle, comme dans les SRAM précédentes. Simplement, la SRAM effectue l'écriture elle-même, sans que le processeur ait besoin de maintenir la donnée à écrire sur le bus de données. Pour le processeur, les écritures prennent un cycle et les lectures deux, alors que la SRAM considère que les deux se font en deux cycles. Et cela a des conséquences.

Les écritures tardives

[modifier | modifier le wikicode]

Les écritures anticipées ne posent pas de problèmes quand on effectue des écritures consécutives, mais elle pose problème quand on enchaine les lectures et écritures. Pour comprendre pourquoi, prenons le cas simple où une lecture est suivie d'une écriture, séparées par un seul cycle d'horloge. Au premier cycle d'horloge, le processeur présente l'adresse à lire à la SRAM. Au second cycle, la donnée lue sera disponible, mais le processeur va aussi présenter l'adresse et la donnée à écrire. Autant cela ne pose pas de problème si la SRAM a un port de lecture séparé de celui d'écriture, autant lire une donnée et écrire la suivante en même temps n'est pas possible avec un seul bus de données.

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. L'intérêt des écritures tardives est qu'elles garantissent que l'écriture se fasse en deux cycles, tout comme les lectures. Ce faisant, dans une succession de lectures/écritures, il n'y a pas de différences de durée entre lectures et écritures, donc les problèmes disparaissent.

Écriture tardive.

Évidemment, il y a des situations où il est possible d'effectuer des écritures anticipées sans problèmes, notamment quand la SRAM a des cycles inutilisées. Les SRAM synchrones gèrent à la fois les écritures anticipées et les écritures tardives, sauf pour quelques exceptions. Quelques bits de commande permettent de choisir s'il faut faire une écriture tardive ou anticipée, ce qui permet au processeur et/ou au contrôleur de mémoire externe de faire le choix le plus adéquat. 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.

Les SRAM synchrones pipelinées

[modifier | modifier le wikicode]

Les SRAM précédentes, à tampon d'adresse et à tampon d'écriture, sont regroupées dans la catégorie des SRAM à flot direct, pour lesquelles la donnée lue ne subit pas de mémorisation dans un registre de sortie. L'avantage est que les lectures sont plus rapides, elles ne prennent qu'un seul cycle. On écrit l'adresse à lire lors d'un cycle, la donnée est disponible au cycle suivant.

Elles sont opposées aux SRAM synchrones pipelinées, qui ont un registre tampon pour les lectures, pour les données lues depuis la RAM. Avec elles, 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 SRAM et son débit.

Mémoire synchrone registre à registre.

Avec une SRAM à pipeline, il faut ajouter un cycle supplémentaire pour lire la donnée. Sur une SRAM à flot direct (non-pipelinée), l'adresse de lecture est envoyée lors du premier cycle, la donnée lue est présentée au cycle suivant. Avec un registre sur le port de lecture, le processeur écrit l'adresse dans le registre lors du premier cycle, la mémoire récupère la donnée lue et l'enregistre dans le registre de sortie lors du second cycle, la donnée est disponible pour le processeur lors du troisième cycle. Au final, la donnée lue est disponible deux cycles après la présentation de l'adresse.

Mais quel est l'avantage des SRAM à pipeline, dans ce cas ? Et bien il vient du fait qu'elles peuvent fonctionner à une fréquence supérieure et effectuer plusieurs accès mémoire en même temps.

Le pipeline de base à trois étages

[modifier | modifier le wikicode]

Pour comprendre pourquoi les SRAM à pipeline fonctionnent à une fréquence plus élevée, il faut étudier comment s'effectue une lecture.

Sur une SRAM à flot direct, on doit attendre que l'accès mémoire précédent soit terminé avant d'en lancer un autre. Si on effectue plusieurs lectures successives, voici ce que cela donne :

Accès mémoires sans pipeline.

Sur une SRAM à pipeline, l'accès en lecture se fait en trois é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. Les trois étapes sont complétement séparées, temporellement et surtout : physiquement. La première implique les registres et bus d'adresse et de commande, la seconde la SRAM asynchrone, la troisième le registre de sortie et le bus de données.

La conséquence est qu'on peut lancer une nouvelle lecture à chaque cycle d'horloge. Et ce n'est pas incompatible avec le fait qu'une lecture prenne 3 cycles d'horloge. Les différentes lectures successives seront à des étapes différentes et s’exécuteront en même temps. Pendant que l'on lit le registre de sortie pour la première lecture, la seconde lecture accède à la SRAM asynchrone, et la troisième lecture prépare l'adresse à écrire dans le registre d'adresse/commande. Le résultat est que l'on peut effectuer plusieurs lectures en même temps. On lance un nouvel accès mémoire à chaque cycle d'horloge, même si une lecture prend trois cycles. Le résultat est que l'on peut effectuer trois lectures en même temps, comme montré dans le schéma plus haut.

Accès mémoires avec pipeline.

Un point sur lequel il faut insister est qu'avec un pipeline, une lecture prend globalement le même temps en secondes, comparé à une SRAM à flot direct. Il y a une petite différence liée au fait que les registres ont un temps de propagation non-nul, mais laissons cela de côté pour le moment. Si la lecture prend trois cycles, ce n'est pas parce que les lectures deviennent sensiblement plus lentes, mais parce que la fréquence augmente. Au lieu d'avoir un cycle horloge long, capable de couvrir une lecture complète, un cycle d'horloge avec pipeline correspond à une seule étape de la lecture, environ un tiers. Les SRAM à pipeline fonctionnent à plus haute fréquence, ce qui donne un débit binaire plus élevé, pour des temps d'accès identiques.

Notons que ce pipeline ne vaut que pour les lectures. Les écritures sont dans un cas un peu différent, avec une différence entre les écritures anticipées et tardives. les écritures anticipées sont toujours possibles et une succession d'écriture n'a pas d'organisation en pipeline nette. La données à écrire et l'adresse sont envoyées en même temps, à raison d’une par cycle. Mais l'écriture effective a lieu au cycle suivant. En clair, une écriture se fait en deux étapes, pas trois ! Il y a bien un pipeline, mais il est plus court : deux étapes au lieu de deux, ce qui fait que l'on ne peut faire que deux écritures simultanées. Pendant que l'une écrit dans les registres, l'autre effectue l'écriture dans la SRAM asynchrone. Et ce pose évidemment quelques problèmes, comme on va le voir dans la section suivante.

Notons que le principe a des conséquences assez similaires à celles de l'entrelacement. On peut effectuer plusieurs accès en parallèle, la fréquence augmente, le débit binaire est améliorée, mais les temps d'accès restent les mêmes. Les deux techniques peuvent d'ailleurs se combiner, bien que ce soit assez rare.

Les conflits d'accès liés au pipeline

[modifier | modifier le wikicode]

Plus haut, pour les SRAM sans pipeline, 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 SRAM sans pipeline était de retarder la présentation de la donnée à écrire, ce qui donne des écritures tardives.

Sur les SRAM à pipeline, un problème similaire à lieu. Il est causé par le fait que les écritures prennent entre un et deux cycles et les lectures trois. Et cela se marie mal avec l'organisation en pipeline qui permet des accès mémoires simultanés. Par exemple, imaginons qu'une écriture ait lieu deux cycles après une lecture. Dans ce cas, la lecture utilise le bus de données à son troisième cycle, l'écriture utilise le bus de données à son premier cycle, le décalage de deux cycles entre les deux fait qu'il y a conflit : l’écriture et la lecture veulent utiliser le bus en même temps. Le même problème peut survenir quand on utilise des écritures tardives, même si les timings ne sont pas les mêmes.

Cas de conflit entre lecture et écriture sur une SRAM pipélinée

Une solution est d'utiliser un port de lecture séparé du port d'écriture. Les conflits d'alternance entre lectures et écritures disparaissent. La seule exception est le cas où une lecture tente de lire une donnée en même temps qu'elle est écrite. La solution est de lire la donnée directement depuis le registre d'écriture. Pour cela, il faut ajouter un comparateur qui vérifie si les deux adresses consécutives sont identiques, et qui commande le bus de données pour le connecter au registre d'écriture.

Sur les mémoires simple port, il existe plusieurs solutions pour gérer ce cas. La plus simple consiste à retarder l'écriture d'un cycle en cas de conflit potentiel. Le contrôleur mémoire externe à la SRAM doit détecter de potentiels conflits et retarder les écritures problématiques d'un cycle quand c'est nécessaire. Une autre solution utilise les écritures tardives, à savoir des écritures où la donnée à écrire est envoyée un cycle après l'adresse. Mais il faut adapter le retard pour le faire correspondre au temps des lectures. Vu que la lecture prend trois cycles sur une SRAM à pipeline, 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.

Les SRAM synchrones registre-à-verrou

[modifier | modifier le wikicode]

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 par 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 peut maintenir la donnée lue durant plusieurs cycles sur le bus mémoire. De plus, 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 RAM dynamiques sont opposées aux mémoires RAM statiques. Les RAM statiques sont les plus intuitives à comprendre : elles conservent leurs données tant qu'on ne les modifient pas, ou tant que l’alimentation électrique est maintenue. Les RAM dynamiques ont pour défaut que les données s'effacent après un certain temps, en quelques millièmes ou centièmes de secondes si l'on n'y touche pas. En conséquence, il faut réécrire chaque bit de la mémoire régulièrement 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.

L'interface des DRAM et le contrôleur mémoire

[modifier | modifier le wikicode]

Un point important est que les DRAM modernes ne sont pas connectées directement au processeur, mais le sont par l'intermédiaire d'un contrôleur mémoire externe. Le contrôleur mémoire sert d'intermédiaire, d'interface entre la DRAM et le processeur. Il ne faut pas le confondre avec le contrôleur mémoire interne, placé dans la mémoire RAM, et qui contient notamment le décodeur. Les deux sont totalement différents, bien que leur nom soit similaire.

Le contrôleur mémoire est reliée au CPU par un bus, et est connecté aux barrettes ou boitiers de DRAM via le bus mémoire proprement dit. Les anciens contrôleurs mémoire étaient des composants séparés du processeur, du chipset ou du reste de la carte mère. Par exemple, les composants Intel 8202, Intel 8203 et Intel 8207 étaient des contrôleurs mémoire pour DRAM qui étaient vendus dans des boitiers DIP et étaient soudés sur la carte mère. Par la suite, ils ont été intégré au chipset de la carte mère pendant les décennies 90-2000. Après les années 2000, ils ont été intégrés dans les processeurs.

Le contrôleur mémoire externe

[modifier | modifier le wikicode]

Le contrôleur mémoire gère le bus mémoire et tout ce qui est envoyé dessus. Il envoie des commandes aux barrettes de mémoire, commandes qui peuvent être des lectures, des écritures, ou des demandes de rafraichissement, parfois d'autres commandes. La mémoire répond à ces commandes par l'action adéquate : lire la donnée et la placer sur le bus de données pour une commande de lecture, par exemple.

Il est possible de connecter plusieurs barrettes sur le même bus mémoire, ou alors celles-ci sont connectées au contrôleur mémoire avec un bus par barrette/boitier. C'est ce qui permet de placer plusieurs barrettes de mémoire sur la même carte mère : toutes les barrettes sont connectées au contrôleur mémoire DRAM d'une manière ou d'une autre. Notons que le contrôleur mémoire est presque toujours un circuit synchrone, cadencé par une horloge, comme le processeur. Et ce peu importe que les mémoires DRAM soient elles-mêmes synchrones ou au contraire asynchrones (sans horloge).

Le rôle du contrôleur mémoire varie grandement suivant le contrôleur en question, ainsi que selon le type de DRAM. Par exemple, les contrôleurs mémoires des toutes premières DRAM ne géraient pas du tout le rafraichissement mémoire, qui était géré par le processeur. Par exemple, le processeur Zilog Z80 implémentait des compteurs pour gérer le rafraichissement mémoire. D'autres processeurs avaient des interruptions dédiées pour gérer le rafraichissement mémoire. Mais les contrôleurs mémoires modernes gèrent le rafraichissement mémoire de manière automatique.

Le bus d'adresse des DRAM est multiplexé

[modifier | modifier le wikicode]

Un point important pour le contrôleur mémoire est de transformer les adresses mémoires fournies par le processeur, en adresses utilisables par la DRAM. Car les DRAM ont une interface assez spécifique. Les DRAM ont ce qui s'appelle un bus d'adresse multiplexé. Avec de tels bus, 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.

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 DRAM é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.

Les bus multiplexés se marient bien avec le fait que les DRAM sont des mémoires à adressage par coïncidence ou à tampon de ligne. Sur ces mémoires, l'adresse est 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 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.

Le rafraichissement mémoire

[modifier | modifier le wikicode]

La spécificité des DRAM est qu'elles doivent être rafraichies régulièrement, sans quoi leurs cellules perdent leurs données. Le rafraichissement est basiquement une lecture camouflée. Elle lit les cellules mémoires, mais n'envoie pas le contenu lu sur le bus de données. Rappelons que la lecture sur une DRAM est destructive, à savoir qu'elle vide la cellule mémoire, mais que le système d'amplification de lecture régénère le contenu de la cellule automatiquement. La cellule est donc rafraichie automatiquement lors d'une lecture.

Les DRAM à rafraichissement externe et pseudo-statiques

[modifier | modifier le wikicode]

Sur la quasi-totalité des DRAM, modernes comme anciennes, le rafraichissement est géré par le processeur ou le contrôleur mémoire. Le rafraichissement est déclenché par une commande de rafraichissement, provenant du processeur ou du contrôleur mémoire. Une commande de rafraichissement est un troisième type d'accès mémoire, séparé de la lecture ou de l'écriture, qui n’existe que sur les DRAM de ce type.

Une commande de rafraichissement ordonne de rafraichir une adresse, parfois une ligne complète. Dans le premier cas, le rafraichissement se fait adresse par adresse, on doit préciser l'adresse à chaque fois. Le second cas est spécifique aux mémoires à tampon de ligne, organisées en lignes et colonnes. La lecture d'une ligne la rafraichit automatiquement, ce qui fait qu'il suffit de d'adresser une ligne, pas besoin de préciser la colonne. Les commandes sont donc plus courtes.

Enfin, un dernier cas permet d'envoyer des commandes de rafraichissement vides, qui ne précisent ni adresse ni numéro de ligne. Pour cela, la mémoire contient un compteur, qui pointe sur la prochaine ligne à rafraichir, qui est incrémenté à chaque commande de rafraichissement. Une commande de rafraichissement indique à la mémoire d'utiliser l'adresse dans ce compteur pour savoir quelle adresse/ligne rafraichir.

Rafraichissement mémoire automatique.

Il existe des mémoires qui sont des intermédiaires entre les mémoires SRAM et DRAM. Il s'agit des mémoires pseudo-statiques, qui sont techniquement des mémoires DRAM, utilisant des transistors et des condensateurs, mais qui gèrent leur rafraichissement mémoire toutes seules. Le rafraichissement mémoire est alors 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.

L'impact du rafraichissement sur les performances

[modifier | modifier le wikicode]

Le rafraichissement mémoire a un impact sur les performances. L'envoi des commandes de rafraichissement entre des lectures/écritures fait qu'une partie du débit binaire de la mémoire est gâché. De même, ces commandes doivent être envoyées à des timings bien précis, et peuvent entrer en conflit avec des lectures ou écritures simultanées.

L'envoi des commandes de rafraichissement peuvent se faire de deux manières : soit on les envoie toutes en même temps, soit on les disperse le plus possible. Le premier cas est un rafraichissement en rafale, le second un rafraichissement étalé. Le rafraichissement en rafale n'est pas utilisé dans les PC, car il bloque la mémoire pendant un temps assez long. Le rafraichissement étalé est étalé dans le temps, ce qui permet des accès mémoire entre chaque rafraichissement de ligne/adresse. Les PC gagnent en performance avec le rafraichissement étalé. Mais les anciennes consoles de jeu gagnaient parfois à utiliser eu rafraichissement en rafale. En effet, la mémoire était souvent effacée entre l'affichage de deux images, pour éviter certains problèmes dont on ne parlera pas ici. Le rafraichissement de la mémoire était effectué à ce moment là : l'effacement rafraichissait la mémoire.

Le temps mis pour rafraichir la mémoire est le temps mis pour parcourir toute la mémoire. Il s'agit du temps de balayage vu dans le chapitre sur les performances d'un ordinateur. Concrètement, il est défini en divisant la capacité de la mémoire par son débit binaire. C'est le temps nécessaire pour lire ou réécrire tout le contenu de la mémoire. Cependant, il faut signaler que l'usage de banques mémoire change la donne. Il est en effet possible de rafraichir des banques indépendantes en même temps, ce qui divise le temps de rafraichissement par le nombre de banques.

Les mémoires asynchrones à RAS/CAS : FPM et EDO-RAM

[modifier | modifier le wikicode]

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 par un signal d'horloge, c'était des mémoires asynchrones. 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 les plus connues étaient les mémoires FPM et mémoires EDO.

Les mémoires FPM

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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. 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, un peu comme sur les mémoires SRAM synchrones. La donnée pouvait être maintenue sur le bus de données durant un certain temps, même après la remontée du signal CAS. Le registre de sortie maintenait la donnée lu tant que le signal RAS restait à 0, et tant qu'un nouveau signal CAS n'a pas été envoyé. Faire remonter le signal CAS à 1 n'invalidait pas la donnée en sortie.

La conséquence est qu'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. Le pipeline obtenu avait deux étages : un où on présentait l'adresse et sélectionnait la colonne, un autre où la donnée était lue depuis le registre de sortie. Les mémoires EDO étaient donc plus rapides.

EDO RAM

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 sur une DRAM EDO.

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 | modifier le wikicode]

Les mémoires FPM et EDO doivent être rafraichies régulièrement. 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.

Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un timer interne permettait de savoir quand rafraichir la mémoire : quand ce timer atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le timer était reset.

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 déclenchement du rafraichissement se faisait toujours 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.

Les mémoires SDRAM

[modifier | modifier le wikicode]

Dans les années 90, 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.

Les mémoires SDRAM sont standardisées par un organisme international, le JEDEC. Le standard SDRAM impose des spécifications électriques bien précise pour les barrettes de mémoire et le bus mémoire, décrit le protocole utilisé pour communiquer avec les barrettes de mémoire, et bien d'autres choses encore. LE standard autorise l'utilisation de 2 à 8 banques dans chaque barrette de SDRAM, autorise une forme de pipeline (une commande peut démarrer avant que la précédente termine), les barrettes mémoires utilisent de l'entrelacement. Les SDRAM ont été déclinées en versions de performances différentes, décrites dans le tableau ci-dessous :

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

Le mode rafale

[modifier | modifier le wikicode]

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 délais mémoires

[modifier | modifier le wikicode]

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 du contrôleur mémoire

[modifier | modifier le wikicode]

Les mémoires SDRAM sont connectées à un bus mémoire spécifique, qui lui-même est commandé par un contrôleur mémoire externe. Et ce contrôleur mémoire est partiellement configurable pour les SDRAM. La configuration en question permet de gérer diverses options du mode rafale, comme le tableau ci-dessous le montre bien.

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, ainsi qu'un autre bit pour configurer le type de rafale (normale, entrelacée). 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 commandes SDRAM

[modifier | modifier le wikicode]

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 mémoires DDR

[modifier | modifier le wikicode]

Les mémoires SDRAM récentes sont des mémoires de type dual data rate, ce qui fait qu'elles portent le nom de mémoires DDR. Pour rappel, les mémoires dual data rate ont un plan mémoire deux fois plus large que le bus mémoire, avec un bus mémoire allant à une fréquence double. Par double, on veut dire que les transferts sur le bus mémoire 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 du plan mémoire lui-même.

Les mémoires DDR sont standardisées par un organisme international, le JEDEC, et ont été déclinées en plusieurs générations : DDR1, DDR2, DDR3, et DDR4. La différence entre ces modèles sont très nombreuses, mais les plus évidentes sont la fréquence de la mémoire et du bus mémoire. 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. ET elle a diminué dans le temps, d'une génération de DDR à l'autre. Avec les mémoires DDR2,la tension d'alimentation est passée de 2,5/2,6 Volts à 1,8 Volts. Avec les mémoires DDR3, la tension d'alimentation est notamment passée à 1,5 Volts.

Les performances des mémoires DDR

[modifier | modifier le wikicode]

Les mémoires SDRAM ont évolué dans le temps, mais leur temps d'accès/fréquence n'a pas beaucoup changé. Il valait environ 10 nanosecondes pour les SDRAM, approximativement 5 ns pour la DDR-400, il a peu évolué pendant la génération DDR et DDR3, avant d'augmenter pendant les générations DDR4 et de stagner à nouveau pour la génération DDR5. L'usage du DDR, puis du QDR, visait à augmenter les performances malgré la stagnation des temps d'accès. En conséquence, la fréquence du bus a augmenté plus vite que celle des puces mémoire pour compenser.

Année Type de mémoire Fréquence de la mémoire (haut de gamme) Fréquence du bus Coefficient multiplicateur entre les deux fréquences
1998 DDR 1 100 - 200 MHz 200 - 400 MHz 2
2003 DDR 2 100 - 266 MHz 400 - 1066 MHz 4
2007 DDR 3 100 - 266 MHz 800 - 2133 MHz 8
2014 DDR 4 200 - 400 MHz 1600 - 3200 MHz 8
2020 DDR 5 200 - 450 MHz 3200 - 7200 MHz 8 à 16

Une conséquence est que la latence CAS, exprimée en nombre de cycles, a augmenté avec le temps. Si vous comparez des mémoires DDR2 avec une DDR4, par exemple, vous allez voir que la latence CAS est plus élevée pour la DDR4. Mais c'est parce que la latence est exprimée en nombre de cycles d'horloge, et que la fréquence a augmentée. En comparant les temps d'accès exprimés en secondes, on voit une amélioration.

Les commandes des mémoires DDR

[modifier | modifier le wikicode]

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 VRAM des cartes vidéo

[modifier | modifier le wikicode]

Les cartes graphiques ont des besoins légèrement différents des DRAM des processeurs, ce qui fait qu'il existe des mémoires DRAM qui leur sont dédiées. Elles sont appelés des Graphics RAM (GRAM). La plupart incorporent des fonctionnalités utiles uniquement pour les mémoires vidéos, comme des fonctionnalités de masquage (appliquer un masque aux données lue ou à écrire), ou le remplissage d'un bloc de mémoire avec une donnée unique.

Les anciennes cartes graphiques et les anciennes consoles utilisaient de la DRAM normale, faute de mieux. La première GRAM utilisée était la NEC μPD481850, qui a été utilisée sur la console de jeu PlayStation, à partir de son modèle SCPH-5000. D'autres modèles de GRAM ont rapidement suivi. Les anciennes consoles de jeu, mais aussi des cartes graphiquesn utilisaient des GRAM spécifiques.

Les mémoires vidéo double port

[modifier | modifier le wikicode]

Sur les premières consoles de jeu et les premières cartes graphiques, le framebuffer était mémorisé dans une mémoire vidéo spécialisée appelée une mémoire vidéo double port. Le premier port était connecté au processeur ou à la carte graphique, alors que le second port était connecté à un écran CRT. Aussi, nous appellerons ces deux port le port CPU/GPU et l'autre sera appelé le port CRT. Le premier port était utilisé pour enregistrer l'image à calculer et faire les calculs, alors que le second port était utilisé pour envoyer à l'écran l'image à afficher. Le port CPU/GPU est tout ce qu'il y a de plus normal : on peut lire ou écrire des données, en précisant l'adresse mémoire de la donnée, rien de compliqué. Le port CRT est assez original : il permet d'envoyer un paquet de données bit par bit.

De telles mémoires étaient des mémoires à tampon de ligne, dont le support de mémorisation était organisé en ligne et colonnes. Une ligne à l'intérieur de la mémoire correspond à une ligne de pixel à l'écran, ce qui se marie bien avec le fait que les anciens écrans CRT affichaient les images ligne par ligne. L'envoi d'une ligne à l'écran se fait bit par bit, sur un câble assez simple comme un câble VGA ou autre. Le second port permettait de faire cela automatiquement, en permettant de lire une ligne bit par bit, les bits étant envoyés l'un après l'autre automatiquement.

Pour cela, les mémoires vidéo double port incorporaient un tampon de ligne spécialisé pour le port lié à l'écran. Ce tampon de ligne n'était autre qu'un registre à décalage, contrairement au tampon de ligne normal. Lors de l'accès au second port, la carte graphique fournissait un numéro de ligne et la ligne était chargée dans le tampon de ligne associé à l'écran. La carte graphique envoyait un signal d'horloge de même fréquence que l'écran, qui commandait le tampon de ligne à décalage : un bit sortait à chaque cycle d'écran et les bits étaient envoyé dans le bon ordre.

Les mémoires SGRAM et GDDR

[modifier | modifier le wikicode]

De nos jours, les cartes graphiques n'utilisent plus de mémoires double port, mais des mémoires simple port. Les mémoires graphiques actuelles sont des SDRAM modifiées pour fonctionner en tant que Graphic RAM. Les plus connues sont 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. Par exemple, les GDDR ont une fréquence plus élevée que les DDR normales, avec des temps d'accès plus élevés (sauf pour le tCAS). De plus, elles sont capables de laisser ouvertes deux lignes en même temps. Par contre, ce sont des mémoires simple port.

Les mémoires SLDRAM, RDRAM et associées

[modifier | modifier le wikicode]

Les mémoires précédentes sont généralement associées à des bus larges. Les mémoires SDRAM et DDR modernes ont des bus de données de 64 bits de large, avec des d'adresse et de commande de largeur similaire. Le nombre de fils du bus mémoire dépasse facilement la centaine de fils, avec autant de broches sur les barrettes de mémoire. Largeur de ces bus pose de problèmes problèmes électriques, dont la résolution n'est pas triviale. En conséquence, la fréquence du bus mémoire est généralement moins performantes comparé à ce qu'on aurait avec un bus moins large.

Mais d'autres mémoires DRAM ont exploré une solution alternative : avoir un bus peu large mais de haute fréquence, sur lequel on envoie les commandes/données en plusieurs fois. Elles sont regroupées sous le nom de DRAM à commutation par paquets. Elles utilisent des bus spéciaux, où les commandes/adresses/données sont transmises par paquets, par trames, en plusieurs fois. En théorie, ce qu'on a dit sur le codage des trames dans le chapitre sur le bus devrait s'appliquer à de telles mémoires. En pratique, les protocoles de transmission sur le bus mémoire sont simplifiés, pour gérer le fonctionnement à haute fréquence. Le processeur envoie des paquets de commandes, les mémoires répondent avec des paquets de données ou des accusés de réception.

Les mémoires à commutation par paquets sont peu nombreuses. Les plus connues sont les mémoires conçues par la société Rambus, à savoir la RDRAM (Rambus DRAM) et ses deux successeurs XDR RAM et XDR RAM 2. La Synchronous-link DRAM (SLDRAM) est un format concurrent conçu par un consortium de plusieurs concepteurs de mémoire.

[modifier | modifier le wikicode]

Les mémoires SLDRAM avaient un bus de données de 64 bits allant à 200-400 Hz, avec technologie DDR, ce qui était dans la norme de l'époque pour la fréquence (début des années 2000). Elle utilisait un bus de commande de 11 bits, qui était utilisé pour transmettre des commandes de 40 bits, transmises en quatre cycles d'horloge consécutifs (en réalité, quatre fronts d'horloge donc deux cycles en DDR). Le bus de données était de 18 bits, mais les transferts de donnée se faisaient par paquets de 4 à 8 octets (32-65 bits). Pour résumer, données et commandes sont chacunes transmises en plusieurs cycles consécutifs, sur un bus de commande/données plus court que les données/commandes elle-mêmes.

Là où les SDRAM sélectionnent la bonne barrette grâce à des signaux de commande dédiés, ce n'est pas le cas avec la SLDRAM. A la place, chaque barrette de mémoire reçoit un identifiant, un numéro codé sur 7-8 bits. Les commandes de lecture/écriture précisent l'identifiant dans la commande. Toutes les barrettes reçoivent la commande, elles vérifient si l'identifiant de la commande est le leur, et elles la prennent en compte seulement si c'est le cas.

Voici le format d'une commande SLDRAM. Elle contient l'adresse, qui regroupe le numéro de banque, le numéro de ligne et le numéro de colonne. On trouve aussi un code commande qui indique s'il faut faire une lecture ou une écriture, et qui configure l'accès mémoire. Il configure notamment le mode rafale, en indiquant s'il faut lire/écrire 4 ou 8 octets. Enfin, il indique s'il faut fermer la ligne accédée une fois l'accès terminé, ou s'il faut la laisser ouverte. Le code commande peut aussi préciser que la commande est un rafraichissement ou non, effectuer des opérations de configuration, etc. L'identifiant de barrette mémoire est envoyé en premier, histoire que les barrettes sachent précocement si l'accès les concerne ou non.

SLDRAM Read, write or row op request packet
FLAG CA9 CA8 CA7 CA6 CA5 CA4 CA3 CA2 CA1 CA0
1 Identifiant de barrette mémoire Code de commande
0 Code de commande Banque Ligne
0 Ligne 0
0 0 0 0 Colonne

Les mémoires Rambus

[modifier | modifier le wikicode]

Les mémoires conçues par la société Rambus regroupent la RDRAM (Rambus DRAM) et ses deux successeurs XDR RAM et XDR RAM 2.

Les toutes premières étaient les mémoires RDRAM, où le bus permettait de transmettre soit des commandes (adresse inclue), soit des données, avec un multiplexage total. Le processeur envoie un paquet contenant commandes et adresse à la mémoire, qui répond avec un paquet d'acquittement. Lors d'une lecture, le paquet d'acquittement contient la donnée lue. Lors d'une écriture, le paquet d'acquittement est réduit au strict minimum. Le bus de commandes est réduit au strict minimum, à savoir l'horloge et quelques bits absolument essentiels, les bits RW est transmis dans un paquet et n'ont pas de ligne dédiée, pareil pour le bit OE. Toutes les barrettes de mémoire doivent vérifier toutes les transmissions et déterminer si elles sont concernées en analysant l'adresse transmise dans la trame.

Elles ont été utilisées dans des PC ou d'anciennes consoles de jeu. Par exemple, la Nintendo 64 incorporait 4 mébioctets de mémoire RDRAM en tant que mémoire principale. La RDRAM de la Nintendo 64 était cadencée à 500 MHz, utilisait un bus de 9 bits, et avait un débit binaire maximal théorique de 500 MB/s. La Playstation 2 contenait quant à elle 32 mébioctets de RDRAM en dual-channel, pour un débit binaire de 3.2 Gibioctets par seconde. Les processeurs Pentium 3 pouvaient être associés à de la RDRAM sur certaines mères. Les Pentium 4 étaient eux aussi associés à la de RDRAM, mais les cartes mères ne géraient que ce genre de mémoire. La Playstation 3 contenait quant à elle de la XDR RAM.

Les barrettes de mémoire DRAM

[modifier | modifier le wikicode]
Barrette de mémoire RAM.

Dans les PC, les mémoires prennent la forme de barrettes mémoires. Les barrettes de mémoire se fixent à la carte mère sur un connecteur standardisé, appelé slot mémoire. Le dessin ci-contre montre une barrette de mémoire, celui-ci ci-dessous est celui d'un slot mémoire.

Slots mémoires.

Le format des barrettes de mémoire

[modifier | modifier le wikicode]

Sur le schéma de droite, on remarque facilement les boitiers de DRAM, rectangulaires, de couleur sombre. 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. Ils sont soudés sur un PCB en plastique vert sur lequel sont gravés des connexions métalliques. 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.

  • Les barrettes SIMM ont des puces sur une seule face de la barrette. Elles étaient utilisées pour les mémoires FPM et EDO-RAM.
  • Les barrettes DIMM ont des puces sur les deux côtés. Elles sont utilisées sur les SDRAM et les DDR.
Barrette SIMM
SIMM recto.
SIMM verso.

Les trucs dorés situés en bas des barrettes de mémoire sont des broches qui connectent la barrette au bus mémoire. Les barrettes des mémoires FPM/EDO/SDRAM/DDR n'ont pas le même nombre de broches, pour des raisons de compatibilité.

Type de barrette Type de mémoire Nombre de broches
SIMM FPM/EDO 30
72
DIMM 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.

Enfin, les barrettes n'ont pas le même format, car il n'y a pas beaucoup de place à l'intérieur d'un PC portable, ce qui demande de diminuer la taille des barrettes. Les barrettes SO-DIMM, pour ordinateurs portables, sont différentes des barrettes DIMM normales des DDR/SDRAM.

Barrettes de DDR pour PC de bureau.
Barrettes de DDR pour PC portables.

Les barrettes de Rambus ont parfois été appelées des barrettes RB-DIMM, mais ce sont en réalité des DIMM comme les autres. La différence principale est que 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.

Les interconnexions à l'intérieur d'une barrette de mémoire

[modifier | modifier le wikicode]

Les boîtiers de DRAM noirs sont connectés au bus par le biais de connexions métalliques. 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

Les barrettes tamponnées (à registres)

[modifier | modifier le wikicode]

Certaines barrettes intègrent un registre tampon, qui fait l'interface entre le bus et la barrette de RAM. L'utilité est d'améliorer la transmission du signal sur le bus mémoire. Sans ce registre, les signaux électriques doivent traverser le bus, puis traverser les connexions à l'intérieur de la barrette, jusqu'aux puces de mémoire. Avec un registre tampon, les signaux traversent le bus, sont mémorisés dans le registre et c'est tout. Le registre envoie les commandes/données jusqu'aux puces mémoire, mais le signal a été régénéré par le registre. Le signal transmis est donc de meilleure qualité, ce qui augmente la fiabilité du système mémoire. Le défaut est que la présence de ce registre fait que les 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.

Le Serial Presence Detect

[modifier | modifier le wikicode]
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. Le SPD est lu 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.

Les eDRAM : des DRAM adaptées aux chiplets

[modifier | modifier le wikicode]

Les mémoires eDRAM, pour embedded DRAM, sont des mémoires RAM qui sont destinées à être intégrée au processeur. Pour comparer, les DRAM normales sont placées sur des barrettes de RAM ou soudées à la carte mère. Dans la quasi-totalité des cas, l'eDRAM est utilisée pour implémenter une mémoire cache, elle ne sert pas de mémoire principale (cache L4, le plus proche de la mémoire sur ces puces). De ce fait, elles sont conçues pour être très rapides, avoir une grande bande passante, au détriment de leur capacité mémoire.

Pour être plus précis, l'eDRAM est une puce de DRAM conçue pour être intégrée dans un chiplet, , à savoir des circuits imprimés qui regroupent plusieurs puces électroniques distinctes, regroupées sur le même PCB. Typiquement, un processeur de type chiplet avec de l'eDRAM comprend deux puces séparées : une pour le processeur, une autre pour une puce de communication avec la RAM. Avec la mémoire eDRAM, les deux puces sont complétées par une troisième puce spécialisée qui incorpore l'eDRAM.

Elle a été utilisée sur quelques processeurs, mais aussi 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é. Ce n'est pas le cas sur une carte graphique moderne : regardez votre carte graphique avec attention et vous verrez que le GPU est une puce carrée située sous les ventilateurs, alors que les puces mémoires sont situées juste autour et soudées sur le PCB de la carte.

Les processeurs Intel Core de microarchitecture Broadwell disposaient d'un cache L4 de 128 mébioctets, intégralement implémenté avec de la mémoire eDRAM. Quelques processeurs de la microarchitecture précédente (Haswell), disposaient aussi de ce cache. Le cache L4 eDRAM était implémenté sur un chiplet à part, à savoir que le processeur était composé de trois puces séparées : une pour le processeur, une autre pour la gestion des entrées-sorties, et une autre pour le cache L4. La puce pour le cache L4 était appelée Crystal Well. La puce Crystal Well était une puce gravée en 22nm, ce qui était une finesse de gravure plus élevée que celle des processeurs associés.

Crystal Well était très optimisé pour l'époque. Par exemple, elle disposait de bus séparées pour la lecture et l'écriture, chose qu'on retrouve fréquemment sur les SRAM mais qui est absent sur les mémoires DRAM actuelles. Pour le reste, elle ressemblait beaucoup aux mémoires DDR de l'époque (système de double data rate, entres autres), mais elle allait à une fréquence plus élevée que les DRAM de l'époque et avait un débit bien plus élevé, pour une consommation moindre. Crystal Well consommait entre 1 à 5 watts (1 watt en veille, 5 à pleine utilisation), pour un débit binaire de 102 GB/s et fonctionnait à 3.2 GHz.


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.

Le contrôleur mémoire externe est relié au CPU par un bus, et est connecté aux barrettes ou boitiers de DRAM via le bus mémoire proprement dit. Les anciens contrôleurs mémoire étaient des composants séparés du processeur, du chipset ou du reste de la carte mère. Par exemple, les composants Intel 8202, Intel 8203 et Intel 8207 étaient des contrôleurs mémoire pour DRAM qui étaient vendus dans des boitiers DIP et étaient soudés sur la carte mère. Par la suite, ils ont été intégré au chipset de la carte mère pendant les décennies 90-2000. Après les années 2000, ils ont été intégrés dans les processeurs.

Les rôles et l'interface du contrôleur mémoire

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.


Les mémoires exotiques

[modifier | modifier le wikicode]

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. Les données manipulées par un programme sont très souvent 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é.

Les mémoires associatives sont assez peu utilisées, mais, les mémoires caches leur ressemblent beaucoup. Une bonne partie de ce que nous allons voir dans ce chapitre 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.

L'interface des mémoires associatives

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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, la troisième 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 | modifier le wikicode]

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 | modifier le wikicode]

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 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'une case mémoire 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 | modifier le wikicode]

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 | modifier le wikicode]

les mémoires associatives word-sérielles sont des mémoires associatives qui sont conçues totalement différemment des précédentes. Les mémoires associatives précédentes comparent la donnée d'entrée avec toutes les cases mémoires en même temps, en parallèle, ce qui fait que nous allons les appeler mémoires associatives word-parallèles. A l'opposé, les mémoires associatives word-sérielles comparent la donnée d'entrée avec chaque case mémoire, une par une. La différence explique la terminologie : comparaison en série ou parallèle.

Les mémoires associatives word-sérielles sont conçues à partir d'une mémoire RAM, à laquelle on rajoute des circuits pour simuler une mémoire réellement associative.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 adéquate.

Mémoire associative word-sérielle

Les avantages et inconvénients des mémoires associatives word-sérielles

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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

Une bonne partie de ce que nous venons de voir sera très utile quand nous verrons les mémoires caches. 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.

Annexe : les architectures associatives

[modifier | modifier le wikicode]

Les architectures associatives sont des mémoires associatives améliorées, auxquelles on aurait intégré des capacités de calcul. Elle reprennent le plan mémoire, mais modifient le comparateur pour en augmenter les capacités. Les mémoires associatives ne font que comparer la donnée d'entrée avec chaque case mémoire, la comparaison étant une comparaison d'égalité stricte. Mais on peut remplacer la comparaison par d'autres comparaisons, comme des comparaisons du type "supérieur ou égal", "inférieur ou égal", etc. De même, les mémoires associatives élaborées peuvent juste appliquer un masque à chaque case mémoire, mais il est possible d'ajouter d'autres opérations bit à bit au circuit sans trop de problèmes, voire des additions/soustractions.

En clair, l'idée est de remplacer le comparateur par une unité de calcul. Pour rappel, une unité de calcul est un circuit capable d'effectuer des opérations mathématiques, des comparaisons, des opérations bit à bit, etc. L'opération à effectuer est choisie en envoyant un code opération, un numéro qui sélectionne l'opération à effectuer, sur une entrée de commande dédiée.

Interface d'une ALU

Remplacer le comparateur par une ALU permet d'appliquer des opérations simples dans toutes les cas mémoire simultanément, en parallèle. Le résultat est ce qui s'appelle une architecture associative, qui fusionne processeur et mémoire associative en un seul circuit. Les architectures associatives font partie des techniques de processing in memory, qui visent à déléguer des calculs à une mémoire RAM/autre. Nous avions vu il y a quelques chapitres que la différence de vitesse entre processeur et RAM était un goulot d’étranglement qui limitait la performance. Le processing in memory permet de contourner le problème.

Il faut signaler que les architectures associatives sont très rares. Les deux plus connues sont l'architecture Parallel Element Processing Ensemble (PEPE) et l'architectures STARAN, fabriquée en un exemplaire durant les années 70, quelques autres machines de ce type ont vu le jour dans les années suivant PEPE et STARAN. Mais concrètement, a part quelques machines isolées et quelques prototypes, les architectures associatives existent surtout à l'état théorique. Il est cependant intéressant de les étudier, par intérêt historique.

Les architectures associatives sont basées sur une mémoire associative word-parallèle, les rares tentatives d'utiliser des mémoires associatives word-sérielles n'ont pas abouties. Il y a une unité de calcul par case mémoire, qui est soit une ALU dite sérielle, soit avec une ALU dite parallèle. Pour rappel, la différence entre les deux est que les ALU sérielles traitent les opérandes bit par bit, alors que les ALU parallèles traitent tous les bits du mot mémoire d'un seul coup.

Architectures associatives avec une unit de calcul par case mémoire.

Les unités de calcul sérielles gèrent les opérations bit à bit (NON, ET, OU, XOR, ...), les comparaisons, les additions et des soustractions, mais pas plus. Ce qui n'est pas un problème sur les architectures associatives. L'intérêt est d'économiser des portes logiques, pour une perte en performance mineure. L'architecture STARAN mentionnée plus haut était de ce type.


Fonctionnement des mémoires FIFO-LIFO.

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. Di autrement, une mémoire FIFO renvoie des données dans leur ordre d'arrivée, alors qu'une LIFO renvoie les données dans l'ordre inverse.

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'utilité des mémoires LFIO et FIFO

[modifier | modifier le wikicode]

En soi, les mémoires LIFO sont assez rares. Il existe quelques processeurs qui remplacent leurs registres par une mémoire LIFO intégrée au processeur, mais ils ont tous disparus depuis des décennies. Des mémoires LIFO sont intégrées dans les processeurs modernes, au niveau des unités de prédiction de branchement, pour la prédiction des retour de fonction, mais c'est un sujet que nous aborderons dans la fin du cours.

Les mémoires FIFO sont beaucoup plus utiles. 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. Elles sont 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.

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. Si le disque dur est déjà en train d'exécuter une commande, il doit mettre en attente les commandes/données réceptionnées pendant ce temps. Et il doit les conserver dans l'ordre d'arrivée, 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.

L'interface d'une mémoire FIFO/LIFO

[modifier | modifier le wikicode]

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.

La mémoire FIFO/LIFO indique quand elle 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. De ce fait, il n'y a pas de bit R/W, mais 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.

Les mémoires FIFO/LIFO sont souvent des mémoires synchrones, les implémentations asynchrones étant plus rares. 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 séparés : 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.

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 | modifier le wikicode]

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 séparés. Pour cela, il suffit d'enchainer des registres les uns à la suite des autres. Un registre est le sommet de la pile, toutes les lectures/écritures se font dedans. Lors d'une écriture/insertion, toutes les données sont donc décalées d'un registre, avant l'écriture de la donnée au sommet de pile. Idem lors d'une lecture : le sommet de la pile est lu, puis toutes les données sont déplacées d'un registre, dans le sens inverse de l'écriture. Les échanges de données entre registres sont 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 | modifier le wikicode]

Il existe deux types de mémoire FIFO : de taille fixe et de taille variable. 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 sont cependant peu utiles. Les FIFO les plus intéressantes peuvent mémoriser un nombre variable de données en attente, qui varie au cours du temps. Il peut y avoir entre 0 et N données en attente, N étant un nombre maximal.

Les FIFO de taille variable

[modifier | modifier le wikicode]

Les FIFO de taille variable peuvent se fabriquer à partir d'une mémoire RAM en y ajoutant deux compteurs 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. 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.

Un tel comportement de débordement est assez simple à implémenter. Il suffit d'utiliser des compteurs qui gère les débordements d'entiers avec l'arithmétique modulaire, à savoir qu'ils reviennent automatiquement à zéro en cas de débordement d'entier. Il faut noter qu'une FIFO de ce type a souvent deux ports séparés, un pour l'écriture, un autre pour la lecture. Il y a un compteur par port, le compteur du port d'écriture indique où se trouve la dernière donnée ajoutée, celui du port de lecture là où se trouve la donnée la plus ancienne.

Circuits internes d'une 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.

Vous vous attendez à ce que ces deux compteurs soient des compteurs binaires normaux, mais il est parfaitement possible d'utiliser des compteurs en anneau, des registres à décalage à rétroaction linéaire, ou des compteurs modulo.

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. Ils sont donc cadencés à deux fréquences différentes, mais ils 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. Faisons quelques rappels.

Lors de l'incrémentation d'un compteur binaire, tous les bits ne sont pas modifiés en même temps : les bits de poids faible sont typiquement modifiés avant les autres. Le compteur passe donc dans plusieurs états transitoires où seule une partie des bits a été incrémenté. Le comparateur qui détermine les bits EMPTY et FULL peut "voir" cet état transitoire et l'utiliser pour calculer un résultat, ce qui donnera un résultat temporairement faux. L'usage de compteurs en code Gray résout ce problème : un seul bit est modifié lors d'une incrémentation/décrémentation, les états transitoires n'existent pas.

Il est en théorie possible d'utiliser des registres à décalage à rétroaction linéaire (LFSR), au lieu de compteurs binaires. Le résultat fonctionne de la même façon vu de l'extérieur, même si les données sont réparties différemment dans la RAM interne à la FIFO. L'avantage est que les compteurs à base de LFSR sont moins gourmands en circuits et plus rapides. Le défaut est que les LFSR ne peuvent 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.

Les FIFO de taille fixe

[modifier | modifier le wikicode]

Les mémoires FIFO de taille fixe peuvent se fabriquer en enchainant des registres.

Register based parallel SAM

Une méthode alternative utilise plusieurs registres à décalages. Chaque registre à décalage contient un bit pour chaque byte mis en attente.

FIFO de m bytes de n bits fabriquées avec des registres à décalages


L'architecture externe

[modifier | modifier le wikicode]

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 boîte 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. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre !

Les instructions entières

[modifier | modifier le wikicode]

Les instructions arithmétiques sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD.

Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc.

De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car 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. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier.

La ou les instructions d'addition entière

[modifier | modifier le wikicode]

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 d'entier. Pour gérer les débordements d'entier, 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. Il est mis à jour à chaque addition/multiplication/soustraction.

Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. 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 différence, présente sur les vieux processeurs, tient dans la gestion de la retenue. À 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 .
  • À 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 division entière

[modifier | modifier le wikicode]

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. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée.

De plus, 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). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. 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é. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. 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.

Les instructions de multiplication entière

[modifier | modifier le wikicode]

Contrairement à la division, 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 entraîner l'apparition de débordements d'entiers.

Une autre solution est de calculer soit les bits de poids fort, soit les bits de poids faible, le choix étant laissé au programmeur. Des 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. Certains processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible.

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. Il faut noter que certains processeurs disposent de registres spécialisés pour cela. Par spécialisés, on veut dire qu'ils sont séparés des autres registres. Les autres registres, appelés les registres généraux, mémorisent n'importe quelle opérande entière et sont utilisables pour toutes les opérations. Mais la multiplication mémorise son résultat dans deux registres à part, qu'on nommera HO et LO. Seule l'instruction de multiplication peut écrire un résultat dans ces deux registres, avec HO pour les bits de poids fort, et LO pour les bits de poids faibles. Par contre, n'importe quelle instruction peut les lire, pour récupérer une opérande dans ces deux registres.

Les instructions sur les entiers BCD

[modifier | modifier le wikicode]

Parlons maintenant des instructions BCD, qui agissent sur des opérandes codées 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.

Les ordinateurs décimaux

[modifier | modifier le wikicode]

Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des processeurs décimaux. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables.

Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe.

Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications.

Les instructions BCD des processeurs binaires

[modifier | modifier le wikicode]

Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 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. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, 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 faire la correction, 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 processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs.

Les instructions flottantes

[modifier | modifier le wikicode]

Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des coprocesseurs arithmétiques, des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile.

Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD.

Les architectures taguées

[modifier | modifier le wikicode]

Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. 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. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des tag associés aux opérandes. Par exemple, si les deux opérandes avaient un tag "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un tag "flottant", l'instruction faisait un calcul flottant.

Le type de l'opérande était encodé avec un tag, une sorte de numéro codé en binaire. Le tag était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le tag concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le tag, de même que le processeur. Des processeurs de ce type s'appellent des architectures à tags, ou tagged architectures, en référence au tag ajouté dans la mémoire.

Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, 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.

Les processeurs avec des instructions flottantes dédiées

[modifier | modifier le wikicode]

Les processeurs modernes disposent d'instructions de calculs sur des nombres flottants, qui sont standardisées. Le standard IEEE 754 standardise aussi quelques instructions sur les flottants : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent 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. 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.

Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les underflow, overflow, les NaN, les infinis, ou même les flottants dénormaux. Précisons que 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.

Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur.

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. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui * se contentent très bien d'un résultat approché.

Les instructions logiques

[modifier | modifier le wikicode]

À 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 | modifier le wikicode]

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.

Le cas particuliers des processeurs ARM

[modifier | modifier le wikicode]

Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre.

Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM.

Les instructions de manipulation de bits

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Mais si c'est une possibilité théorique, aucun processeur connu n'a de telle instruction, qui serait peu utilisée.

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 | modifier le wikicode]

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.
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 logiques utilisées convenablement. L'idée est de faire une opération dont le résultat est l'opérande envoyée, et d'enregistrer le résultat dans le registre de destination. Par exemple, on peut faire un OU entre le registre opérande et 0 : le résultat sera l'opérande. Idem avec un ET et la valeur adéquate. Ou encore, on peut imaginer faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande.

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.

Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage.

Les instructions d'accès mémoire complexes

[modifier | modifier le wikicode]

Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés.

Les instructions de copie mémoire copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation.

Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une instruction d'accès mémoire généraliste, qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. 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 ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* (Move String ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...).

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 | modifier le wikicode]

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 ?).

Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...).

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

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. 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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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.

L'instruction SKIP

[modifier | modifier le wikicode]

L'instruction SKIP permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin.

Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi.

Skip instruction

Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage.

Les méta-instructions REPEAT et EXECUTE

[modifier | modifier le wikicode]

Enfin, il faut aussi citer les méta-instructions, des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls.

L’instruction REPEAT et le Zero-overhead looping

[modifier | modifier le wikicode]

La première est la méta-instruction REPEAT, qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions.

Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis.

Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. On parle d'ailleurs de Zero-overhead looping. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter fréquemment des boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe, ou du moins stocké dans un registres. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération.

Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction.

L'instruction EXECUTE

[modifier | modifier le wikicode]

La méta-instruction EXECUTE fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible.

Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible.

L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement.

L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un program counter. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable program counter. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre.

Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le late binding, les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres.

Les instructions d'arrêt et de mise en veille

[modifier | modifier le wikicode]

Les processeurs gèrent souvent des instructions de mise en veille le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le chipset de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation.

Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard.

Il existe aussi des instructions d'arrêt, qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue.

Les exemples sur les processeurs les plus connus

[modifier | modifier le wikicode]

Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0.

Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le program counter est réinitialisé.

Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son program counter. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog :

C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel.

x86 HLT (HALT)
ARM
  • WFI (Wait For Interrupt)
  • WFE (Wait For Event).
Motorola 6502 et plus
  • WAI (WAit for Interrupt)
  • STP (SToP)

Les instructions Halt And Catch Fire

[modifier | modifier le wikicode]

Le bug n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des instructions Halt And Catch Fire (stoppe et prend feu).

Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle opcode invalide. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception.

Elle agissait comme une instruction Halt And Catch Fire classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486.

Pour résumer, les instructions les plus courantes sont les suivantes :

Instruction Utilité
Instructions arithmétiques Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle.
Instructions logiques Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ;
  • Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique.
  • 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. Les deux les plus courantes sont les suivantes :
  • LOAD : charge une donnée dans un registre ;
  • STORE : copie le contenu d'un registre dans une adresse mémoire.

En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc.

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...

Mais d'autres sont franchement exotiques. 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).


Le processeur incorpore un ou plusieurs registres, des mémoires de petite taille, capables de mémoriser un nombre entier/flottant. Naïvement, les registres sont utilisés pour stocker les opérandes des instructions et leur résultat. Un programmeur (ou un compilateur) qui programme en langage machine manipule ces registres intégrés dans le processeur. Cependant, tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il faut distinguer les registres architecturaux, manipulables par des instructions, des registres internes aux processeurs.

Les différents types de registres architecturaux

[modifier | modifier le wikicode]

Dans ce qui suit, nous allons parler uniquement des registres architecturaux. Les registres internes seront vu dans les chapitre sur la microarchitecture d'un processeur. Ils servent à simplifier la conception du processeur, à mettre en œuvre des optimisations de performance. Les registres architecturaux, eux, font partie de l'interface que le processeur fournit aux programmeurs. Ils font partie du jeu d'instruction, qui liste les registres, les instructions supportées, comment instructions et registres interagissent, etc. Il existe plusieurs types de registres architecturaux, qui sont assez difficiles à classer, que nous allons décrire ci-dessous.

Le registre d'état (entier)

[modifier | modifier le wikicode]

Le registre d'état est un registre aux fonctions assez variées, qui varient selon le processeur. Au minimum, il contient 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. 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 registre d'état est mis à jour par les instructions de test, mais aussi par les instructions arithmétiques entières (sur des opérandes entiers). Par exemple, si 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.

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.

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.

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.

Le program counter

[modifier | modifier le wikicode]

Le Program Counter mémorise l'adresse de l’instruction en cours ou de la prochaine instruction (le choix entre les deux dépend du processeur). C'est bel et bien un registre architectural, car ils sont manipulés par les instructions de branchement, bien qu'implicitement. Ce n'est pas un registre utilisé à des fins d'optimisation ou de simplicité d'implémentation.

Lors du démarrage ou redémarrage, il faut initialiser le program counter avec l'adresse de la première instruction. Le processeur est câblé pour lire une adresse bien précise lors du boot, qui porte le nom d'adresse de démarrage, CPU Reset vector en anglais. 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. Pour déterminer l'adresse de démarrage, il y a deux solutions : soit on utilise une adresse fixée une fois pour toutes, soit l'adresse est configurable.

  • 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. Il s'agit typiquement de l'adresse 0 : le processeur lit le contenu de la toute première adresse et démarre l’exécution du programme à cet endroit.
  • Avec la seconde solution, on ajoute un niveau de redirection. Le processeur accède toujours à une adresse fixe au démarrage, mais l'adresse en question ne contient pas la première instruction à exécuter. À la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la copie dans son program counter. En clair, il effectue un branchement.

Il existe des processeurs où le Program Counter est adressable, via un nom de registre. 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.

Le program counter et le registre d'état sont parfois fusionnés en un seul registre appelé le Program status word, abrévié en PSW. L'avantage est que le Program status word regroupe tout ce qui est utile pour les branchements et test. Les branchements écrivent dans le program counter pour brancher à l'adresse finale, lire l'adresse dans le program counter pour certains branchements dits relatifs, les tests/branchements peuvent lire le registre d'état. Avec un PSW, tout cela est regroupé dans le PSW, les tests et branchements altérent tous deux le PSW. L'avantage est mineur et pose des problèmes niveau implémentation matérielle.

Il peut y avoir un avantage en terme de taille des registres. 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 ses registres font 32 bits, que le program counter n'a besoin que de 24 bits pour fonctionner ce qui laisse 8 bits pour le registre d'état. Précisément, le program counter est censé gérer des adresses de 26 bits, mais les instructions de ce processeur font exactement 32 bits et elles sont alignées en mémoire, ce qui fait que 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.

Les registres de contrôle

[modifier | modifier le wikicode]

Les registres de contrôle permettent de configurer le processeur pour qu'il fonctionne comme souhaité. Ils sont très variables et dépendent fortement du jeu d'instruction, mais aussi du modèle de processeur considéré. Quelques fonctionnalités importantes sont gérées par ce registre, même si on ne peut pas encore en parler. Des fonctionnalités comme la désactivation des interruptions ou la gestion du mode noyau/hyperviseur, par exemple.

Des bits de contrôle sont dédiés à la gestion du cache. Il est ainsi possible de configurer le cache, voire de le désactiver. Nous ne pouvons pas en parler en détail ici, car nous ne savons pas comment fonctionne une mémoire cache pour le moment. Mais nous détaillerons les bits de contrôle du cache dans le chapitre sur la mémoire cache. Pour le moment, nous ne pouvons parler que d'un seul bit de contrôle du cache :; celui qui l'active ou le désactive.

Les registres généraux : entiers et adresses

[modifier | modifier le wikicode]

Les registres de données mémorisent des informations comme des entiers, des adresses, des flottants, manipulés par un programme. Ils sont classés en deux grand types, appelés registres entiers et flottants, dont les noms sont assez transparents. Les registres entiers sont spécialement conçus pour stocker des nombres entiers. Les registres entiers sont aussi appelés des registres généraux, car ils servent non seulement pour les entiers, mais aussi les adresses et d'autres informations codées en binaire.

Les registres entiers ne font pas que mémoriser les opérandes/résultats, et peuvent contenir n'importe quelle information codée par des nombres entiers. Notamment, ils peuvent mémoriser des adresses mémoire. L'avantage est que cela permet de faire des calculs sur des adresses mémoire, chose très importante pour supporter des structures de données comme les tableaux. Nous en reparlerons plus en détail dans le chapitre sur les modes d'adressage.

Pour le moment, vous avez juste à savoir que les registres entiers sont en réalité des registres généraux utilisables pour tout et n'importe quoi, qui peuvent stocker toute sorte d’information codée en binaire. Par exemple, un processeur avec 8 registres généraux pourra les utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum.

De nombreux processeurs incorporent des registres entiers ou flottants en lecture seule, qui 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. Ils sont appelés des registres de constante, leur nom étant assez clair.

Les registres flottants

[modifier | modifier le wikicode]

Les registres flottants sont spécialement conçus pour stocker des nombres flottants. Ils ne sont présents que sur les processeurs qui supportent les nombres flottants. Tous les processeurs modernes séparent les registres flottants et entiers, pour de bonnes raisons. Une des raisons est que les flottants et entiers n'ont pas le même encodage et n'ont pas forcément la même taille. Les flottants font 32 et 64 bits, ce qui posait problème sur les architectures 32 bits. Mais surtout, les flottants et entiers sont vraiment traités séparément dans le processeur : ils ont des circuits de calcul distincts, ils sont traités par des instructions séparées. Les mettre dans des registres séparés aide beaucoup pour la conception du processeur, comme on le verra dans quelques chapitres. Et cela n'entraine pas de problèmes de performances.

Les processeurs qui gèrent les nombres flottants incorporent aussi un registre d'état flottant, qui s'occupe des nombres flottants. Sur les CPU x86, qui utilisaient l'extension x87, il était appelé le 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. Voici son contenu, à peu de chose près.

Bit Utilité
U Mis à 1 lorsqu'un débordement a lieu.
O Pareil que U, mais pour les overflow
Z Bit mis à 1 lors d'une division par zéro
D Bit 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

Les registres de contrôle flottant configurent les opérations flottantes. Ils configurent quel mode d'arrondi utiliser, comment traiter les infinis, si les flottants utilisés sont simple (32 bits) ou double précision (64 bits). Pour donner un exemple, voici le registre control word utilisé sur les anciens CPU x86, pour l'extension x87. L'extension x87 ajoutait le support des nombres flottants aux CPU x86, mais ceux-ci n'étaient pas tout à fait compatibles avec la norme IEEE 754. Une différence notable est que les flottants étaient codés sur 80 bits maximum.

Bit Utilité
Infinity Control Mode de gestion des infinis, codé sur 2 bits :
  • 0 : Les infinis sont tous traités comme s'ils valaient .
  • 1 : Les infinis sont traités normalement.
Rouding Control Mode d'arrondi codé sur 2 bits :
  • 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 Taille de la mantisse, configurée via deux bits. 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 registres d'adresse et d'indice

[modifier | modifier le wikicode]

Quelques processeurs incorporent des registres spécialisés dans les adresses et leur calcul. Les registres d'adresse contiennent des adresses. Ils étaient surtout présents sur les architectures 16 bits, plus rarement sur les architectures 32 bits. L'usage de registres d'adresse s'explique par le fait que sur les anciennes architectures, les adresses n'ont pas la même taille que les données.

Un exemple est celui des processeurs Motorola 68000, sur lequel les entiers faisaient 32 bits et les adresses faisaient 24 bits. Le packaging du processeur ne permettait pas de mettre trop de broches, ce qui fait que les broches d'adresse étaient limitée à 24 bits, ce qui était suffisant pour l'époque. L'usage de registres d'adresse séparés des registres entiers permettait de gérer au mieux cette différence de taille. Ce problème a été corrigé à l'arrivée du 68020, qui avait des adresses sur 32 bits et 32 broches d'adresse, mais a conservé la séparation entre registres d'adresse et entiers pour des raisons de compatibilité.

Un autre exemple est celui du processeur du CDC 6600, qui avait 8 registres d'adresse couplés à 8 registres d'entrée. Les registres d'adresse fonctionnaient d'une manière totalement inédite, qu'on ne retrouve pas sur d'autres processeurs avec registre d'adresse. Tout registre d'adresse était associé à un registre entier. Concrètement, les 8 registres d'adresse étaient numérotés de 0 à 7, idem pour les registres entier. Le CDC 6600 n'avait pas d'instruction LOAD ou STORE, tout passait par des écritures dans ces registres. Le comportement dépendait du registre concerné.

  • Une écriture dans le registre A0 ne faisait rien, il sert d'exception. Le registre D0 n'est pas altéré lors d'une écriture dans le registre A0.
  • Les registres A1 à A5 servaient pour les lectures. L'écriture d'une adresse dans un de ces registres entrainait une lecture de cette adresse. La donnée lue était copiée automatiquement dans le registre entier associé, le registre entier de même numéro.
  • Les registres A5 à A7 servaient pour les écriture. L'écriture d'une adresse dans un de ces registres entrainait une écriture à cette adresse. La donnée à écrire était prise dans dans le registre entier associé, le registre entier de même numéro.

L'usage de registres d'adresse dédiés est très rare, les processeurs préfèrent utiliser des registres généraux qui servent à la fois de registres entier et de registres d'adresse. La raison est que les adresses sont encodés avec des entiers en binaire. Les opérations effectuées sur les adresses sont des opérations entières basiques : additions/soustractions, parfois multiplications entières, opérations de masquage, bit à bit, etc. Aussi, séparer adresses et entiers dans des registres séparés n'est pas très pertinent.

Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers et de 4 registres d'adresse. Si j’exécute un morceau de programme qui ne manipule presque pas d'adresses, mais fait beaucoup de calcul, les 4 registres d'adresse seront sous-utilisés alors que je manquerais de registres entiers. Utiliser 8 registres généraux permet de contourner le problème. On peut se servir de ces 8 registres généraux pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum.

Les registres d'indice servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ils étaient présents sur les premiers ordinateurs et ont perduré jusqu’aux architectures 16 bits inclues. Dans les faits, ils étaient présent sur une classe particulière de processeurs, appelés les architectures à accumulateur, qui aura droit à son chapitre dédié. Nous parlerons en détail des registres d'indice dans ce chapitre dédié aux architectures à accumulateur.

L'adressage des registres architecturaux

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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. 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.

L'identifiant en question est ce qu'on appelle un nom de registre ou encore un numéro 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. Le numéro/nom de registre permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur, il 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 | modifier le wikicode]

Mais il existe une autre solution, utilisée sur de très vieux ordinateurs des années 50 à 70, ou quelques microcontrôleurs. C'est le cas du PDP-10.. L'idée est d'adresser les registres via une adresse mémoire. Les registres se voient attribuer les adresses mémoires les plus basses, à partir de l'adresse 0. Par exemple, un processeur avec 16 registres utilisait les 16 adresses basses, une par registre.

Adressage des registres via des adresses mémoires.

Les registres adressés implicitement

[modifier | modifier le wikicode]

Certains registres n'ont pas forcément besoin d'avoir un nom. Par exemple, c'est le cas du 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. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le Program Counter sont adressables.

La taille des registres architecturaux

[modifier | modifier le wikicode]

Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits, voire de processeurs 8 ou 16 bits pour les ordinateurs anciens. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple, appelé la taille des registres. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Le même terme était autrefois utilisé pour les consoles de jeu, où il était censé désigner la même chose, à savoir la taille des registres du processeur. Mais l'utilisation sur les consoles de jeu était moins stricte, les fabricants de consoles n'hésitait pas à faire gonfler les chiffres par intérêt marketing.

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, les autres registres peuvent avoir une taille totalement différente. Notamment, sur les processeurs 8 bits, il y a souvent une différence entre la taille des entiers (codés sur 8 bits) et les adresses (codées sur 16 à 24 bits). Dans ce cas, un processeur 8 bits peut parfaitement gérer des adresses 16 ou 24 bits, mais reste un processeur 9 bits par ses entiers sont codés sur 8 bits.

Aujourd'hui, les processeurs utilisent presque tous des registres dont la taille est une puissance de 2 : 8, 16, 32, 64, 128, 256, voire 512 bits. L'usage de registres qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra dans le chapitre sur l'alignement et le boutisme. Mais ca n'a pas toujours été le cas. Par exemple, 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.

Aux tout début de l'informatique, les processeurs utilisaient tous l'encodage BCD et codaient leurs chiffres sur 4/5/6/7 bits. La taille des registres était donc un multiple de 4/5/6/7 bits. Les registres de 36 bits et de 48 bits étaient la norme sur les gros ordinateurs de type mainframe, qu'ils soient commerciaux ou destinés au calcul scientifique. 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.

La taille d'un registre n'est pas toujours égale à celle du bus de données

[modifier | modifier le wikicode]

La taille d'un registre est souvent comparée à 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). Intuitivement, on s'attend à ce que le bus mémoire ait la même taille que les registres, ce qui permet de lire un registre en une fois, en un seul accès mémoire. Mais il existe des processeurs où le bus mémoire est plus petit ou plus grand que les registres.

Un exemple est celui du processeur MOS Technology 65C816, utilisé dans la console de jeu SNES. C'était un processeur 16 bits, avec des registres entiers de 16 bits. Cependant, le bus de données faisait lui 8 bits. Les adresses faisaient elles 24 bits, mais cela impacte le bus d'adresse, pas le bus de données. L'inconvénient est que les instructions LOAD et STORE demandaient deux accès mémoire : un pour les 8 bits de poids faible, un autre pour les 8 bits de poids fort.

Un autre exemple est celui des anciens processeurs x86 32 bits, sur lequels un registre entier fait 32 bits alors que le bus de données fait 64 bits. La raison à cela est la présence d'un cache entre la mémoire et le CPU. Le bus de données est utilisée pour échanger des données entre RAM et cache, pas directement entre registres et RAM. Pas étonnant donc que les deux n'aient pas la même taille. Cependant, le bus entre cache et registres fait lui bel et bien 32 bits, en théorie.

Le système d'aliasing de registres sur les processeurs x86

[modifier | modifier le wikicode]

Les processeurs 8086 et 8088 étaient des processeurs 16 bits, qu'on peut considérer comme des versions améliorées et grandement remaniées du 8008 8 bit. En théorie, la rétrocompatibilité n'était pas garantie, car 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 CPU Intel 16 bits avaient 4 registres de données, nommés AX, BX, CX et DX. Il faisaient 16 bits, soit deux octets. Et chaque octet était adressable comme des registres à part entière. On pouvait adresser un registre de 16, ou alors adresser seulement l'octet de poids fort ou l'octet de poids faible. Le registre AX fait 16 bits, l'octet de poids fort est un registre à part entière nommé AH, l'octet de poids faible est lui le 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. Les autres registres ne sont pas concernés par ce découpage.

Tout cela décrit 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. Une même opération peut donc agir sur 16 ou 8 bits suivant le registre sélectionné.

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.

Par la suite, le jeu d'instruction x86 a étendu ses registres à 32 et enfin 64 bits. Et les CPU 32 bits ont utilisé le même système d'alias que les CPU 16 bits, 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. Les registres 8 et 16 bits ont le même nom de registre que sur les CPU 16 bits, 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, les 16 bits de poids faible sont tout simplement le registre AX vu plus haut, qui lui-même est subdivisé en AH et AL. La même chose a lieu pour les registres EBX, ECX et EDX. Et cette fois-ci, 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. Pour le moment, bornons-nous à citer les exemples les plus frappants et parlons du MMX, du SSE et de l'AVX.

Le MMX est une extension du x86, qui ajoute des instructions au jeu d'instruction x86 de base. Elle ajoutait 8 registres entiers appelés MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7, d'une taille de 64 bits. En théorie, ces registres devraient être des registres séparés des autres, ajoutés aux anciens. Mais Intel utilisa le système d'alias pour éviter d'avoir à rajouter des registres. Il étendit les 8 registres flottants de 80 bits déjà existants. 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, l'extension SSE ajouta 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. Les registres SSE sont bien séparés des autres, Intel n'utilisa pas le système d'alias.

Puis, l'arrivée de l'extension AVX changea la donne. 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-512 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'ajouter des registres de plus grande taille, en étendant des registres existants pour en augmenter la taille. La longévité des architectures x86 a fait que cette technique a beaucoup été utilisée. 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 l'aliasing 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).

La taille des registres flottants et les doubles arrondis

[modifier | modifier le wikicode]

Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. Cependant, de nombreux processeurs ne suivent pas ce standard à la lettre. Par exemple, les coprocesseurs x87, ainsi que les processeurs x86 32 bits utilisaient des flottants codés sur 80 bits. Et leurs registres flottants faisaient eux aussi 80 bits, ce qui posait quelques problèmes.

Lors des accès mémoire, il y avait parfois des conversions entre flottants 80 bits et flottants 32/64 bits. L'instruction LOAD flottantes pouvait lire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. Les flottants 32 et 64 bits étaient convertis en flottants 80 bits lors du chargement. Même chose pour l'enregistrement en mémoire via l'instruction STORE flottante. Les flottants 80 bit était soit convertit en flottant 32 ou 64 bits, soit enregistrés directement avec 80 bits.

Le problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. De plus, la conversion lors des écritures mémoire effectue un arrondi pour faire rentrer le résultat sur 32/64 bits, arrondi qui modifie encore les résultats. Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires.

Phénomène de double arrondi sur les coprocesseurs x87

Une autre conséquence est que les résultats sont impactés par l'ordre des accès mémoire, par la manière dont sont gérés les registres flottants. En effet, les problèmes d'arrondis ont lieu lors de l'écriture. Plus longtemps les résultats intermédiaires sont enregistrés dans les registres, plus on retarde les problèmes. Mais il arrive fatalement un moment où des flottants doivent quitter les registres flottants pour arriver en RAM.

Et ce moment dépend du nombre de registres et du nombre d'opérandes traitées. Si vous vous débrouillez pour faire tous vos calculs flottants avec les 8 registres disponibles, vous ne ferez d'arrondi qu'à la toute fin de vos calculs, pour enregistrer les résultats. Si vous utilisez plus, vous aller devoir faire un vas et vient entre RAM et registres. Dans ce cas, suivant l'ordre des accès mémoire, les arrondis se feront à des instants différents.

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.


Une instruction n'est pas encodée n'importe comment, 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.

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 la majorité instructions ajoutent des bits 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. 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 pour les branchements.

Reste à savoir quelle est la nature de la référence : est-ce une adresse, un nombre, un nom de registre, de quoi calculer l'adresse ? Chaque manière d’interpréter la partie variable s'appellent un mode d'adressage. Un mode d'adressage indique au processeur que telle référence est une adresse, un registre, autre chose. 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).

Nous verrons dans le chapitre suivant comment sont encodées les instructions à plusieurs opérandes, ce qui dépend fortement du jeu d'instruction utilisé. Mais dans ce chapitre, nous allons nous limiter au cas où une instruction ne manipule qu'une seule opérande. De plus, nous allons nous limiter au cas où l'opérande est chargée dans un registre. La raison est que nous allons nous concentrer sur la description des modes d'adressage proprement dit. L'instruction encode donc un opcode et une référence, pas plus.

Les modes d'adressages pour les données

[modifier | modifier le wikicode]

Pour comprendre un peu mieux ce qu'est un mode d'adressage, nous allons voir les modes d'adressage les plus simples qui soient. Ils sont supportés par la majorité des processeurs existants, à quelques détails près que nous élaborerons dans le chapitre suivant. Il s'agit des modes d'adressage directs, qui permettent de localiser directement une donnée dans la mémoire ou dans les registres. Ils précisent dans quel registre, à quelle adresse mémoire se trouve une donnée.

L'adressage implicite

[modifier | modifier le wikicode]

Avec l'adressage implicite, il n'y a pas besoin de fournir une référence vers l'opérande ! La raison à cela est que l'instruction n'a pas besoin qu'on lui donne la localisation des données d'entrée et « sait » où sont les données. Comme exemple, on pourrait citer une instruction qui met tous les bits du registre d'état à zéro.

L'adressage inhérent (à registre)

[modifier | modifier le wikicode]

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 numéro de registre à chaque registre, parfois appelé abusivement un nom de registre. Pour rappel, ce dernier est un numéro attribué à chaque registre, utilisé pour préciser à quel registre le processeur doit accéder. On parle aussi d'adressage à registre, pour simplifier.

Adressage inhérent

L'adressage immédiat

[modifier | modifier le wikicode]

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 absolu

[modifier | modifier le wikicode]

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 RAM/ROM. Le terme "adressage par adresse" est aussi utilisé. Un défaut de ce mode d'adressage est que l'adresse en question a une taille assez importante, elle augmente drastiquement la taille de l'instruction. Les instructions sont donc soit très longues, sans optimisations.

Adressage direct

Pour raccourcir les instructions, il est possible de ne pas mettre des adresses complètes, mais de retirer les bits de poids forts. L'adressage absolu ne peut alors lire qu'une partie de la mémoire RAM. Il est aussi possible de ne pas encoder les bits de poids faible pour des questions d'alignement mémoire. Les processeurs RISC modernes gèrent parfois le mode d'adressage absolu, ils encodent des adresses sur 16-20 bits pour des processeurs 32 bits. Un exemple plus ancien est le cas de l’ordinateur Data General Nova. Son processeur était un processeur 16 bits, capable d'adresser 64 kibioctets. Il gérait plusieurs modes d'adressages, dont un mode d'adressage absolu avec des adresses codées sur 8 bits. En conséquence, il était impossible d’accéder à plus de 256 octets avec l'adressage absolu, il fallait utiliser d'autres modes d'adressage pour cela. Il s'agit d'un cas extrême.

Une solution un peu différente des précédentes utilise des adresses de taille variable, et donc des instructions de taille variable. Un exemple est celui du mode zero page des processeurs Motorola, notamment des Motorola 6800 et des MOS Technology 6502. Sur ces processeurs, il y avait deux types d'adressages absolus. Le premier mode utilisait des adresses complètes de 16 bits, capables d'adresser toute la mémoire, tout l'espace d'adressage. Le second mode utilisait des adresses de 8 bits, et ne permettait que d'adresser les premiers 256 octets de la mémoire. L'instruction était alors plus courte : avec un opcode de 8bits et des adresses de 8 bits, elle rentrait dans 16 bits, contre 24 avec des adresses de 16 bits. Un autre avantage était que l'accès à ces 256 octets était plus rapide d'un cycle d'horloge, ce qui fait qu'ils étaient monopolisés par le système d'exploitation et les programmes utilisateurs, mais ce n'est pas lié au mode d'adressage absolu proprement dit.

Les modes d'adressage indirects pour les pointeurs

[modifier | modifier le wikicode]

Les modes d'adressages précédents sont appelés les modes d'adressage directs car ils fournissent directement une référence vers la donnée, en précisant dans quel registre ou adresse mémoire se trouve la donnée. Les modes d'adressage qui vont suivre ne sont pas dans ce cas, ils permettent de localiser une donnée de manière indirecte, en passant par un intermédiaire. D'où leurs noms de modes d'adressage indirects.

L'intermédiaire en question est ce qui s'appelle un pointeur. 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. En clair, les modes d'adressage indirects ne disent pas où se trouve la donnée, mais où se trouve l'adresse de la donnée, un pointeur vers celle-ci.

L'utilité des pointeurs : les structures de données

[modifier | modifier le wikicode]

Les pointeurs ont une définition très simple, mais beaucoup d'étudiants la trouve très abstraite et ne voient pas à quoi ces pointeurs peuvent servir. Pour résumer rapidement, les pointeurs sont utilisées pour manipuler/créér des structures de données, à savoir des regroupements structurées de données plus simples, peu importe le langage de programmation utilisé. 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 explicite dans des langages comme le C, mais implicite dans les langages haut-niveau. 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.

Illustration du concept de pointeur.

Les structures de données les plus simples sont appelées "structures" ou enregistrements. Elles regroupent plusieurs données simples, comme des entiers, des adresses, des flottants, des caractères, etc. Par exemple, on peut regrouper deux entiers et un flottant dans une structure, qui regroupe les deux. Les données de la structure sont placées les unes à la suite des autres dans la RAM, à partir d'une adresse de début. Localiser une donnée dans la structure demande simplement de connaitre à combien de byte se situe la donnée par rapport à l'adresse de début. Une simple addition permet de calculer cette adresse, et des modes d'adressage permettent de faire ce calcul implicitement.

Un autre type de structure de donnée très utilisée est les tableaux, des structures de données où plusieurs données de même types sont placées les unes à la suite des autres en mémoire. Par exemple, on peut placer 105 entiers les uns à la suite des autres en mémoire. Toute donnée dans le tableau se voit attribuer un indice, un nombre entier qui indique la position de la donnée dans le tableau. Attention : les indices commencent à zéro, et non à 1, ce qui fait que la première donnée du tableau porte l'indice 0 ! L'indice dit si on veut la première donnée (indice 0), la deuxième (indice 1), la troisième (indice 2), etc.

Tableau

Le tableau commence à une adresse appelée l'adresse de base, qui est mémorisée dans un pointeur. Localiser un entier dans le tableau demande de faire des calculs avec le pointeur et l'indice. Intuitivement, on se dit qu'il suffit d'additionner le pointeur avec l'indice. Mais ce serait oublier qu'il faut tenir compte de la taille de la donnée. Le calcul de l'adresse d'une donnée dans le tableau se fait en multipliant l'indice par la taille de la donnée, puis en additionnant le pointeur. De nombreux modes d'adressage permettent de faire ce calcul directement, comme nous allons le voir.

L'adressage indirect à registre pour les pointeurs

[modifier | modifier le wikicode]

Les modes d'adressage indirects sont des variantes des modes d'adressages directs. Par exemple, le mode d'adressage inhérent indique le registre qui contient la donnée, sa version indirecte indique le registre qui contient le pointeur, qui pointe vers une donnée en RAM/ROM. Idem avec le mode d'adressage absolu : sa version directe fournit l'adresse de la donnée, sa version indirecte fournit l'adresse du pointeur.

Par contre, il n'est pas possible de prendre tous les modes d'adressage précédents, et d'en faire des modes d'adressage indirects. L'adressage implicite reste de l'adressage implicite, peu importe qu'il adresse une donnée ou un pointeur. Quand à l'adressage immédiat, il n'a pas d'équivalent indirect, même si on peut interpréter l'adressage absolu comme tel. Pour résumer, un pointeur peut être soit dans un registre, soit en mémoire RAM, ce qui donne deux classes de modes d'adressages indirect : à registre et mémoire. Nous allons d'abord voir l'adressage indirect à registre, ainsi que ses nombreuses variantes.

Avec l'adressage indirect à registre, le pointeur est stockée dans un registre. Le registre en question contient donc un l'adresse de la donnée à lire/écrire, celle qui pointe vers la donnée à lire/écrire. Lors de l'exécution de l'instruction, le pointeur dans le registre est envoyé sur le bus d'adresse, et la donnée est récupérée sur le bus de données.

Ici, la partie variable de l'instruction 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

L'adressage indirect à registre gère les pointeurs nativement, mais pas plus. Il faut encore faire des calculs d'adresse pour gérer les tableaux ou les enregistrements, et ces calculs sont réalisés par des instructions de calcul normales. Le mode d'adressage indirect à registre ne gére pas de calculs d'adresse en lui-même. Et les modes d'adressages qui vont suivre intègrent ce mode de calcul directement dans le mode d'adressage ! Avec eux, le processeur fait le calcul d'adresse de lui-même, sans recourir à des instructions spécialisées. Sans ces modes d'adressage, utiliser des tableaux demande d'utiliser du code automodifiant ou d'autres méthodes qui relèvent de la sorcellerie.

Pour faciliter ces parcours de tableaux, il existe des variantes de l'adressage précédent, qui incrémentent ou décrémentent automatiquement le pointeur à chaque lecture/écriture. Il s'agit des modes d'adressages indirect avec auto-incrément (register indirect autoincrement) et indirect avec auto-décrément (register indirect autodecrement). Avec eux, le contenu du registre est incrémenté/décrémenté d'une valeur fixe automatiquement. Cela permet de passer directement à l’élément suivant ou précédent dans un tableau.

Adressage indirect à registre post-incrémenté

En théorie, il y a une différence entre les deux modes d'adressages. Avec l'adressage indirect avec auto-incrément, l'incrémentation se fait APRES l'envoi de l'adresse, après la lecture/écriture. On effectue l'accès mémoire avec le pointeur, avant d'incrémenter le pointeurs. Par contre, pour l'adresse indirect avec auto-décrément, on décrémente le pointeur AVANT de faire l'accès mémoire. Les deux comportements semblent incohérents, mais ils sont en réalité très intuitifs quand on sait comment se fait le parcours d'un tableau.

Le parcours d'un tableau du début vers la fin commence à l'adresse de base du tableau, celle de son premier élément. Aussi, si on place l'adresse de base du tableau dans un pointeur, on accède à l'adresse, puis ensuite on incrémente le tout. Pour le parcours en sens inverse, on commence à l'adresse de fin du tableau, celle à laquelle on quitte le tableau. Ce n'est pas l'adresse du dernier élément, mais l'adresse qui se situe immédiatement après. Pour obtenir l'adresse du dernier élément, on doit soustraire la taille de l'élément à l'adresse initiale. En clair, on décrémente l'adresse avant d'y accéder.

Adresses lors du parcours d'un tableau
Pour ceux qui savent déjà ce qu'est une exception matérielle : les deux modes d'adressage précédents posent des problèmes avec les exceptions matérielles. Le problème vient du fait que l'accès mémoire peut générer une exception matérielle, comme un problème de mémoire virtuelle ou autres. Dans ce cas, l'exception matérielle est gérée par une routine d'interruption, puis la routine se termine et l'instruction cause est ré-exécutée. Mais la ré-exécution doit tenir compte du fait que le pointeur initial a été incrémenté/décrémentée, et qu'il faut donc le faire revenir à sa valeur initiale. Quelques machines ont eu des problèmes d'implémentation de ce genre, notamment le DEC VAX et le Motorola 68000.

Les modes d'adressage indirects indicés pour les tableaux

[modifier | modifier le wikicode]

Le mode d'adressage base + indice est utilisé lors de l'accès à un tableau, quand on veut lire/écrire un élément de ce tableau. L'adressage base + indice, fournit à la fois l'adresse de base du tableau et l'indice de l’élément voulu. Les deux sont dans un registre, ce qui fait que ce mode d'adressage précise deux numéros/noms de registre. En clair, indice et pointeur sont localisés via adressage inhérent (à registre). Le calcul de l'adresse est effectué automatiquement par le processeur.

Base + Index

Il existe une variante qui permet de vérifier qu'on ne « déborde » pas du tableau, qu'on ne calcule pas 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.

Le mode d'adressage absolu indexé (indexed absolute, ou encore base+offset) est une variante de l'adressage précédent, qui est spécialisée pour les tableaux dont l'adresse de base est fixée une fois pour toute, elle est connue à la compilation. Les tableaux de ce genre sont assez rares : ils correspondent aux tableaux de taille fixe, déclarée dans la mémoire statique. L'adresse de base du tableau est alors précisée via une adresse mémoire et non un nom de registre. En clair, l'adresse de base est précisée par adressage absolu, alors que l'indice est précisé par adressage inhérent. À 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

Les deux modes d'adressage précédents sont appelés des modes d'adressage indicés, car ils gèrent automatiquement l'indice. Ils existent en deux variantes, assez similaires. La première variante ne tient pas compte de la taille de la donnée. L'adresse de base est additionnée avec l'indice, rien de plus. Le programme doit donc incrémenter/décrémenter l'indice en tenant compte de la taille de la donnée. Par exemple, pour un tableau d'entiers de 4 octets chacun, l'indice doit être incrémenté/décrémenté par pas de 4. Pour éviter ce genre de choses, la seconde variante se charge automatiquement de gérer la taille de la donnée. Le programme doit donc incrémenter/décrémenter les indices normalement, par pas de 1, l'indice est automatiquement multiplié par la taille de la donnée. Cette dernière est généralement encodée dans l'instruction, qui gère des tailles de données basiques 1, 2, 4, 8 octets, guère plus.

Pour les deux modes d'adressage précédent, l'indice est généralement mémorisé dans un registre général, éventuellement un registre entier. Mais il a existé des processeurs qui utilisaient des registres d'indice spécialisés dans les indices de tableaux. Les processeurs en question sont des processeurs assez anciens, la technique n'est plus utilisée de nos jours.

Les modes d'adressage indirect à décalage pour les enregistrements

[modifier | modifier le wikicode]

Après avoir vu les modes d'adressage pour les tableaux, nous allons voir des modes d'adressage spécialisés dans les enregistrements, aussi appelées structures en langage C. Elles regroupent plusieurs données, généralement une petite dizaine d'entiers/flottants/adresses. Mais le processeur ne peut pas manipuler ces enregistrements : il est obligé de manipuler les données élémentaires qui le constituent une par une. Pour cela, il doit calculer leur adresse, et les modes d'adressage qui vont suivre permettent de le faire automatiquement.

Une donnée a une place prédéterminée dans un enregistrement : elle est donc a une distance fixe du début de celui-ci. En clair, l'adresse d'un élément d'un enregistrement se calcule en ajoutant une constante à l'adresse de départ de l'enregistrement. Et c'est ce que fait le mode d'adressage base + décalage. Il spécifie un registre et une constante. Le registre contient l'adresse du début de l'enregistrement, un pointeur vers l'enregistrement.

Base + offset

D'autres processeurs vont encore plus loin : ils sont capables de gérer des tableaux d'enregistrements ! Ce genre de prouesse est possible grâce au mode d'adressage base + indice + décalage. Il calcule l'adresse du début de la structure avec le mode d'adressage base + indice avant d'ajouter 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 branchements

[modifier | modifier le wikicode]

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.

Avec un branchement direct, l'opérande est simplement l'adresse de l'instruction à laquelle on souhaite reprendre. Il s'agit d'une sorte d'équivalent à l'adressage immédiat/absolu, mais pour les branchements.

Branchement direct.

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

Avec les branchements indirects, l'adresse vers laquelle on souhaite brancher peut varier au cours de l’exécution du programme. Il s'agit d'une sorte d'équivalent à l'adressage indirect à registre, mais pour les branchements. 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 branchements implicites se limitent aux instructions de retour de fonction, qu'on abordera dans quelques chapitres. L'instruction SKIP est équivalente à un branchement relatif dont le décalage est de 2. Il n'est pas précisé dans l'instruction, mais est implicite.

Les modes d'adressage pour les conditions/tests

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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 modes d'adressage obsolètes : données et pointeurs

[modifier | modifier le wikicode]

Dans cette section, nous allons voir quelques modes d'adressage autrefois utilisés sur les ordinateurs historiques, d'avant les années 90. Ils ne sont plus utilisés aujourd'hui, aucun processeur ne les supporte. Cependant, ils reviendront plus tard dans ce cours, aussi je préfère en parler maintenant. De plus, certains ont un lien avec ce qui a été dit précédemment. Nous allons tout d'abord voir un mode d'adressage pour les pointeurs.

Les modes d'adressage indirect mémoire

[modifier | modifier le wikicode]

Les modes d'adressage pour les pointeurs mémorisent les pointeurs dans des registres, mais il existe quelques modes d'adressage qui mémorisent les pointeurs en mémoire RAM, à une adresse bien précise. Avec de tels modes d'adressages, le processeur accède à une adresse mémoire pour récupérer le pointeur, et l'utiliser pour un second accès. L'accès est donc indirect, par l'intermédiaire du pointeur, d'où leur nom de modes d'adressage indirects mémoire. Ils étaient utilisés autrefois sur quelques vieux ordinateurs se débrouillaient sans registres pour les données/adresses.

Du moment qu'un mode d'adressage fournit une adresse mémoire, il peut être rendu indirect. Par exemple, on peut imaginer un mode d'adressage indirect Base + indice : la somme base + indice calcule l'adresse du pointeur et non l'adresse de la donnée. Un tel mode d'adressage serait utile pour gérer des tableaux de pointeurs. Tous les modes d'adressage précédents peuvent être modifiés de manière à ce que la donnée lue/écrite soit traitée comme un pointeur. Il y a donc un grand nombre de modes d'adressages indirects mémoire !

Le plus simple d'entre eux est le mode d'adressage absolu indirect. L'instruction incorpore une adresse mémoire, comme dans l'adressage absolu. Sauf qu'il s'agit d'un adressage indirect : l'adresse n'est pas l'adresse de la donnée voulue, mais l'adresse du pointeur qui pointe vers la donnée. Un exemple est le cas des instructions LOAD et STORE des ordinateurs Data General Nova. Les deux instructions existaient en deux versions, distinguées par un bit d'indirection. Si ce bit est à 0 dans l'opcode, alors l'instruction utilise le mode d'adressage absolu normal : l'adresse intégrée dans l'instruction est celle de la donnée. Mais s'il est à 1, alors l'adresse intégrée dans l'instruction est celle du pointeur.

Il a existé des modes d'adressage absolus indirects avec auto-incrément/auto-décrément, où le pointeur est incrémenté ou décrémenté automatiquement lors de l'exécution de l'instruction. Les deux exemples les plus connus sont le PDP-8 et le Data General Nova, les autres exemples sont très rares. Sur le PDP-8, les adresses 8 à 15 avaient un comportement spécial. Quand on y accédait via adressage mémoire indirect, leur contenu était automatiquement incrémenté. Le Data General Nova avait la même chose, mais pour ses adresses 16 à 31 : les adresses 16 à 24 étaient incrémentées, celles de 25 à 31 étaient décrémentées.

D'autres architectures supportaient des modes d'adressages indirects récursifs. l'idée était simple : le mode d'adressage identifie un mot mémoire, qui peut être soit une donnée soit un pointeur. Le pointeur peut lui aussi pointer vers une donnée ou un pointeur, qui lui-même... Une véritable chaine de pointeurs pouvait être supportée avec une seule instruction. Pour cela, chaque mot mémoire avait un bit d'indirection qui disait si son contenu était un pointeur ou une donnée. Des exemples d'ordinateurs supportant un tel mode d'adressage sont le DEC PDP-10, les IBM 1620, le Data General Nova, l'HP 2100 series, and le NAR 2. Le PDP-10 gérait même l'usage de registres d'indice à chaque étape d'accès à un pointeur.

Une curiosité historique : l'instruction Index next de l'Apollo Guidance Computer

[modifier | modifier le wikicode]

Les tout premiers ordinateurs ne supportaient aucun mode d’adressage indirect. L'utilisation de tableaux ou de structures de données était un véritable calvaire, qui se résolvait à grand coup de code automodifiant. Les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination été incorporée dans l'instruction via adressage absolu, mais était changée via code automodifiant. Et quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant, voire pour s'en passer.

Un exemple est celui de l'instruction Index next instruction, que nous appellerons INI, qui a été utilisée sur des architectures comme l'Apollo Guidance Computer et quelques autres. Elle additionne une certaine valeur à l'instruction suivante. Elle est utilisée pour émuler un adressage absolu indicé : on utilise l'INI pour ajouter l'indice à l'instruction LOAD suivante. La valeur à ajouter est précisée via mode d'adressage absolu est lue depuis la mémoire. Par exemple, si l’instruction suivante est une instruction LOAD adresse 50, l'INI permet d'y ajouter la valeur 5, ce qui donne LOAD adresse 55.

L'instruction est aussi utilisée pour modifier des branchements : si l'instruction suivante est l'instruction JUMP à adresse 100, on peut la transformer en JUMP à adresse 150. Elle peut en théorie changer l'opcode d'une instruction, ce qui permet en théorie de faire des calculs différents suivant le résultat d'une condition. Mais ces cas d'utilisation étaient assez rares, ils étaient peu fréquents.

Un point important est que l'addition a lieu à l'intérieur du processeur, pas en mémoire RAM/ROM. Le mode d'adressage ne fait pas de code auto-modifiant, l'instruction modifiée reste la même qu'avant en mémoire RAM. Elle est altérée une fois chargée par le processeur, avant son exécution.

L'adressage relatif pour les données

[modifier | modifier le wikicode]

L'adressage relatif est utilisé pour les branchements, pour calculer l'adresse de destination. Mais il peut en théorie être utilisé pour les données. En clair, l'adresse d'une donnée est calculée en ajoutant au program counter un décalage, un offset. Il s'agit d'une variante de l'adressage base + décalage, sauf que l'adresse de base est le program counter. Il était très utilisé sur les anciens ordinateurs, qui encodaient leurs instructions sur un faible nombre de bits et ne pouvaient pas encoder d'adresses complètes.

Un exemple est celui du PDP-8, encore lui. Il avait des instructions de 12 bits, et les adresses mémoire faisaient la même taille. L'opcode était codé sur 3 bits, l'instruction incorporait une adresse codée sur 7 bits, il restait deux bits pour le mode d’adressage. Vous remarquerez que les adresses font 12 bits, mais que les instructions incorporent seulement les 7 bits de poids faible. Il faut donc trouver les 5 bits de poids fort manquants. Pour cela, trois modes d'adressage sont possibles

  • avec l'adressage absolu, les 5 bits de poids fort sont mis à 0 ;
  • avec l'adressage PC-relatif, les 5 bits de poids fort étaient les 5 bits de poids fort du program counter ;
  • avec l'adresse indirect mémoire, l'adresse 7 bit poijnte vers un pointeur en mémoire, qui fait 12 bits.

Les deux bits du mode d'adressage permettent d'indiquer quelle option est choisie. Le premier bit indiquait si le mode d'adressage utilisé était le mode d'adressage indirect mémoire ou non. Il était à 1 pour le mode d'adressage indirect mémoire, à 0 sinon. Le second bit indiquait s'il fallait utiliser l'adressage absolu (0) ou relatif au program counter.

Opcode Mode d'adressage Adresse mémoire
Opcode Bit d'indirection Bit d'adressage absolu/relatif Adresse mémoire
3 bits 1 bit 1 bit 7 bits


Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. 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. 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, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données.

La taille d'une instruction : taille fixe ou variable

[modifier | modifier le wikicode]

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. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres !

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.

Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est 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.

L'encodage d'une instruction : généralités

[modifier | modifier le wikicode]

Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en champs, à savoir des suites de bits qui ont une signification.

L'opcode, aussi appelé "code opération"

[modifier | modifier le wikicode]

Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc.

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.

En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre.

Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique.

Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique.

L'encodage des opérandes et des références

[modifier | modifier le wikicode]

En plus de l'opcode, une instruction doit préciser quels sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode.

Le second cas ne fournit pas l'opérande, mais une référence qui indique où se trouve l'opérande. Elle précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. L'opérande est associé à un mode d'adressage qui précise comment interpréter la référence, si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer une adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence.

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. En conséquence, 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. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire.

Un problème est que les instructions n'ont pas le même nombre d'opérande. Les instructions arithmétiques et logiques sont souvent des instructions dyadiques, à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares instructions monadiques qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même.

Ne parlons pas des opérations d'accès mémoire qui n'ont ni opérande ni résultat. Elles copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes.

Les instructions de longueur variables permettent de résoudre ce problème de longueur. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou pour encoder sa source et de sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Mais les instructions de longueur fixe se débrouillent autrement. Elles se calent sur le pire des cas, l'instruction la plus longue, et doivent trouver un moyen de tout faire rentrer dans 16, 32 ou 64 bits. Généralement, elles réduisent les possibilités d'adressage, certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, etc. Leur encodage est donc soit plus compliqué, soit plus restrictif.

Une solution souvent utilisée réduit la taille des opérations dyadiques. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d'encodage 3-références, plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons.

Mais encoder 2 opérandes + 1 résultat prend de la place. L'encodage 3-référence peut être modifié de manière à adresser le résultat de manière implicite. Pour économiser un peu de place, l'idée est d'adresser le résultat de manière implicite. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d'encodage 2-références, d'encodage 2-adresses, ou encore d'encodage 2-opérandes.

Un exemple d'utilisation est celui des processeurs RISC-V, qu'on a mentionné plus haut. Nous avions dit qu'ils géraient des instructions 32 bits, et des instructions compressées de 16 bits. 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 et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois.

L'encodage 2-références peut aussi être utilisé avec l'adressage immédiat. Une instruction peut l'utiliser si un opérande est en adressage immédiat, les deux autres sont dans des registres. La raison est qu'une constante prend approximativement autant de bits qu'une adresse. Les deux sont souvent codées sur 16 bits, plus rarement 20/24 bits. Avec des instructions codées sur 32 bits, cela ne laisse qu'un ou deux octets pour coder l'opcode et les registres. L'encodage 2-opérande est alors une nécessité, économiser un numéro de registre est le seul moyen d'avoir assez de place si la constante est codée sur 20/24 bits.

Les instructions monadiques utilisent pas défaut l'encodage 2-références, pour préciser l'opérande et le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte.

Exemples d'encodage d'instruction sur une taille fixe.
0 - Opération sans opérande.
1 - Instruction mono-opérande en encodage 1-référence.
2 - Instruction de branchement.
3 - Instruction dyadique en encodage 2-opérande.
4 - Instruction dyadique en encodage 3-opérandes.

L'encodage du mode d'adressage

[modifier | modifier le wikicode]

Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire.

Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (Reduced Instruction Set Computer) , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de mode d'adressage implicite.

Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite.

Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.

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.

Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat.

Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat.

Encodage d'une instruction où chaque opérande/résultat est adressée explicitement
Opcode Mode d'adressage opérande 1 Référence vers l'opérande 1 Mode d'adressage opérande 2 Référence vers l'opérande 2 Mode d'adressage résultat Référence vers le résultat

L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable.

Encodage d'une instruction où chaque opérande/résultat est adressée explicitement
Opcode Mode d'adressage opérande 1 Mode d'adressage opérande 2 Mode d'adressage résultat Référence vers l'opérande 1 Référence vers l'opérande 2 Référence vers le résultat

Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place.

Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement
Opcode Mode d'adressage opérande 1 Mode d'adressage opérande 2/résultat Référence vers l'opérande 1 Référence vers l'opérande 2 / le résultat

Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit.

L'encodage de la taille des opérandes

[modifier | modifier le wikicode]

Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits.

Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit :

  • 00 pour des données de 8 bits ;
  • 01 pour des données de 16 bits ;
  • 10 pour des données de 32 bits ;
  • 11 pour des données de 64 bits.

En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'alias de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort.

Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres.

Les restrictions sur les modes d'adressage

[modifier | modifier le wikicode]

Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement.

La gestion du mode d'adressage immédiat

[modifier | modifier le wikicode]

Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois.

De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire.

Les restrictions de registres source/destination

[modifier | modifier le wikicode]

Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples.

Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort.

Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX.

De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate.

Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers.

Les restrictions sur les opérandes mémoire

[modifier | modifier le wikicode]

Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres.

Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles.

Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d'instruction simple-accès pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des instructions multi-accès. Et nous allons les voir séparément.

L'encodage des instructions simple-accès

[modifier | modifier le wikicode]

Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès.

Les instructions mémoire simple-accès et leur encodage

[modifier | modifier le wikicode]

Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont :

  • les instructions LOAD pour charger une donnée de la RAM dans un registre ;
  • l'instruction STORE pour copier une donnée d'un registre vers la RAM ;
  • l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ;
  • l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination.

D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté.

Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions.

Instruction LOAD
Opcode Adressage de la source
  • Adressage absolu (adresse)
  • Adressage indirect à registre
  • Adressage base + indice
  • etc.
Adresse ou référence de la source Nom de registre de destination
Instruction STORE
Opcode Nom de registre source Adressage de la destination
  • Adressage absolu (adresse)
  • Adressage indirect à registre
  • Adressage base + indice
  • etc.
Adresse ou référence de destination
Instruction MOV
Opcode
  • Adressage inhérent (registre)
  • Adressage immédiat (constante)
Nom de registre source Nom de registre destination
Constante immédiate

Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre.

Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination.

Encodage d'une instruction MOV généraliste
Opcode Bit qui indique l'ordre des champs Nom de registre source/destination
  • Adressage inhérent (registre)
  • Adressages mémoire (adresse, pointeur, autre)
Nom de registre source/destination
Adresse mémoire ou registres pour les pointeurs
Constante immédiate

Les instructions dyadiques simple-accès et leur encodage

[modifier | modifier le wikicode]

Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas.

Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant.

Instruction dyadique de type load-op.

Dans ce qui suit, nous parlerons d'instructions load-op pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux instructions reg-reg, ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les instructions cst-reg, où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent.

Encodage d'une instruction dyadique reg-reg
Opcode
  • Adressage inhérent (registre)
  • Adressage immédiat (constante)
Nom de registre pour l'opérande 1 Nom de registre pour l'opérande 2 Nom de registre pour le résultat
Constante immédiate
Encodage d'une instruction load-op
Opcode
  • Adressage inhérent (registre)
  • Adressage immédiat (constante)
  • Adressages mémoire (adresse, pointeur, autre)
Nom de registre source Nom de registre destination
Constante immédiate
Adresse mémoire ou registres pour les pointeurs

Encoder une instruction load-op est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions load-op, ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes.

Les instructions multi-accès et leur encodage

[modifier | modifier le wikicode]

Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre.

Les instructions mémoire-mémoire

[modifier | modifier le wikicode]

Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d'instruction mémoire-mémoire, ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire.

Les instructions mémoire-mémoire les plus communes sont les instructions de copie mémoire, où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation.

Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination.

Encodage d'une instruction de copie mémoire
Opcode Mode d'adressage de la source
  • Adressage absolu (adresse)
  • Adressages mémoire indirect à registre (pointeur)
  • Adressages mémoire base + indice
  • etc.
Mode d'adressage de la destination
  • Adressage absolu (adresse)
  • Adressages mémoire indirect à registre (pointeur)
  • Adressages mémoire base + indice
  • etc.
Référence(s) vers la source Référence(s) vers la destination

Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits.

Encodage d'une instruction de copie mémoire
Opcode Mode d'adressage de la source et de la destination
  • Adressage absolu (adresse)
  • Adressages mémoire indirect à registre (pointeur)
  • Adressages mémoire base + indice
Référence(s) vers la source Référence(s) vers la destination

Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées.

Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une instruction mémoire généraliste, souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination.

  • Si les deux opérandes sont des registres, le premier registre est copié dans le second.
  • Si le premier opérande est un registre et l'autre une adresse, c'est une écriture.
  • Si le premier opérande est une adresse et l'autre un registre, c'est une lecture.
  • Si les deux opérandes sont des adresses, c'est une copie mémoire.
Encodage d'une instruction mémoire généraliste
Opcode Mode d'adressage de la source
  • Adressage inhérent (registre)
  • Adressages mémoire (adresse, pointeur, autre)
  • Adressage immédiat (constante)
Mode d'adressage de la destination
  • Adressage inhérent (registre)
  • Adressages mémoire (adresse, pointeur, autre)
Nom de registre source Nom de registre destination
Adresse mémoire ou registres pour les pointeurs Adresse mémoire ou registres pour les pointeurs
Constante immédiate

Les instructions multi-accès monadiques et dyadiques

[modifier | modifier le wikicode]

Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques.

Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées instructions read-modify-write. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques.

Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs.

Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable.

Le cas des processeurs x86

[modifier | modifier le wikicode]

Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et load-op, mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général.

L'encodage d'une instruction x86

[modifier | modifier le wikicode]

L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes.

Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs.

Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M.

Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un offset, pour les adressages base + offset et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + offset. Ce n'est pas très courant.

Préfixes opcode Mod R/M SIB Displacement Immediate
Préfixes opcode Mode d'adressage registres pour l'adressage base + indice offset Constante immédiate
facultatif obligatoire facultatif facultatif facultatif
0 à 4 octets 1, 2, 3 octets, souvent un seul 1 octet 1 octet 1, 2 ou 4 octets 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits.

L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base.

Octet SIB
Taille de l'opérande Base Indice
Entier Numéro de registre Numéro de registre
2 bits 3 bits 3 bits

L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage :

  • 00 pour l'adressage indirect à registre ou l'adressage base + indice.
  • 01 pour dire qu'un offset de 1 octet est présent.
  • 10 pour dire qu'un offset de 4 octets est présent.
  • 11 si les deux opérandes sont dans un registre.

Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP.

Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande.

Octet SIB
Taille de l'opérande Base Indice
Entier Numéro de registre Numéro de registre
2 bits 3 bits 3 bits

Les string instructions

[modifier | modifier le wikicode]

Les instructions dont nous allons parler 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. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises.

Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes :

  • L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU.
  • L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs.

Les autres instructions de chaine de caractère regroupent trois instructions :

  • LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI.
  • STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI.
  • SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI.

Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant.

Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. 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 le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées.

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 ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. 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), et 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.


Les instructions d'un processeur dépendent fortement du processeur utilisé. Le jeu d'instructions d'un processeur définit la liste des 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 macintoshs utilisent un jeu d'instruction différent : le PowerPC. 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.

Les jeux d'instruction RISC vs CISC

[modifier | modifier le wikicode]

Dans le chapitre précédent, nous avons vu comment sont encodées les instructions, ainsi que de nombreux paramètres : les modes d'adressage supportés, leur encodage, la taille des instructions, l'encodage de l'opcode, etc. Et tous ces paramètres sont suffisant pour parler des processeurs RISC et CISC. Il s'agit d'une classification qui sépare les 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é.

Reste à expliquer ce que l'on veut dire quand on parler de jeu d'instruction "simple" ou "complexe". Et la différence n'est pas simple. Surtout que le terme CISC regroupe, comme on le verra, des architectures bien différentes.

Les différences entre CISC et RISC

[modifier | modifier le wikicode]

La différence la plus intuitive est le nombre d'instructions supporté. Les processeurs CISC supportent beaucoup d'instructions, , alors que les processeurs RISC se contentent d'un petit nombre d'instructions assez simples. Un autre critère, très lié au précédent, est la complexité des instructions en question. Les processeurs CISC incorporent souvent des instructions complexes, comme le calcul de la division, de la racine carrée, de l'exponentielle, des puissances, des fonctions trigonométriques. Plus fréquent, on trouvait des instructions de contrôle élaborées pour gérer les appels de fonction, simplifier l'implémentation des boucles, etc.

La différence principale fait la distinction entre les architectures LOAD-STORE (RISC) et celles qui ne le sont pas (CISC). Les processeurs RISC sont des architectures LOAD-STORE. Sur celles-ci, seules les instructions d'accès mémoire peuvent lire ou écrire en mémoire. Les autres instructions ne peuvent accéder qu'aux registres : elles lisent leurs opérandes dans les registres et enregistrent leur résultat dans les registres. En conséquence, les instructions de calcul ne peuvent prendre que des noms de registres ou des constantes comme opérandes, via les modes d'adressage immédiat et à registre. Pour le dire autrement, elles ne gèrent que les instructions reg-reg et cst-reg, pas les instructions load-op. La distinction se fait uniquement au niveau des instructions de calcul/branchement.

Les premiers processeurs RISC se contentaient de deux instructions d'accès mémoire : LOADq et STORE. Elles ont d'ailleurs donné leur nom à ce type d'architecture. Mais dans les faits, les processeurs RISC modernes peuvent gérer d'autres instructions d'accès à la mémoire. Par exemple, les processeurs ARM7 supportent des instructions de copie mémoire-mémoire nommées CPYP, CPYM et CPYE. Et ce n'est pas incompatible avec un processeur RISC, ni avec une architecture LOAD-STORE. De telles instructions ont beau être assez complexes, ça reste des instructions d'accès mémoire. Même si elles font plusieurs accès mémoire par instruction, c'est comme si on avait fusionné un LOAD suivi d'un STORE en une seule instruction.

Les architectures LOAD-STORE sont les seules à avoir une stricte séparation entre instructions d'accès mémoire et instructions de calcul. A l'opposé, les processeurs CISC ne sont pas des architectures LOAD-STORE, leurs instructions de calcul/branchement peuvent effectuer des accès mémoire, soit pour aller chercher leurs opérandes en RAM, soit pour écrire un résultat. Elles peuvent même en faire plusieurs si plusieurs opérandes sont en mémoire, encore que ce ne soit pas possible sur tous les jeux d'instructions. Au minimum, les processeurs CISC gèrent les instructions load-op. Quelques processeurs CISC anciens supportent tous les modes d'adressage possibles pour les opérandes et le résultat, qui peuvent tous trois être lus/écrits dans les registres ou la RAM.

Les deux propriétés précédentes, à savoir un grand nombre d'instruction et des modes d'adressages complexes, se marient bien avec des instructions de longueur variable. Les instructions d'un processeur CISC sont de taille variable pour diverses raisons, mais la variété des modes d'adressage y est pour beaucoup. Quand une même instruction peut incorporer soit deux noms de registres, soit deux adresses, soit un nom de registre et une adresse, sa taille ne sera pas la même dans les trois cas. Les processeurs RISC ne sont pas concernés par ce problème. Les instructions de calcul n'utilisent que deux-trois modes d'adressages simples, qui demandent d'encoder des registres ou des constantes immédiates de petite taille. Les autres instructions se débrouillent avec des adresses et éventuellement un nom de registre. Le tout peut être encodé sur 32 ou 64 bits sans problèmes.

Différences CISC/RISC
Propriété CISC RISC
Instructions
  • Nombre d'instructions élevé, parfois plus d'une centaine.
  • Parfois, présence 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.
  • 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.
Modes d'adressage
  • Support des instructions load op, systématique
  • Souvent, support d'instructions multi-accès (plusieurs accès mémoires par instruction).
  • Architecture LOAD-STORE.
  • Un seul accès mémoire par instruction maximum
Registres
  • Peu de registres : rarement plus de 16 registres entiers.
  • Beaucoup de registres, souvent plus de 32.

Les performances relatives des CISC/RISC

[modifier | modifier le wikicode]

Pour comprendre quels sont les avantages et désavantages des processeurs CISC comparé au RISC, nous allons étudier leur performance et leur code density.

Pour ce qui est de la performance, les choses ne sont pas clairement tranchées. Pour comprendre pourquoi, nous allons partir d'une équation déjà abordée dans un chapitre antérieur, qui donne le temps que met un programme à s'exécuter. 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, notamment en présence de conditions ou de boucles.

Le processeurs CISC intègrent des instructions complexes, que les processeurs RISC doivent émuler à partir d'une suite d'opérations plus simples. Par exemple, les instructions load-op sont émulées avec une instruction LOAD suivie d'une instruction de calcul. En clair, les processeurs CISC ont un avantage pour le paramètre N, la longueur du chemin d'instruction. Par contre, il n'est pas garantit que les instructions complexes soient aussi rapides que la suite d'instruction RISC équivalente, car les processeurs RISC ont un avantage pour le terme . Voyons pourquoi.

Les processeurs CISC gèrent un grand nombre d'instructions et de modes d'adressage, ce qui les rend plus gourmands en transistors. Non pas qu'il faille ajouter plus de circuits de calcul, les CISC se débrouillent bien avec les circuits usuels pour les 4 opérations basiques. Le vrai problème est le support des nombreux modes d'adressage, des instructions de taille variable, et le grand nombre de variantes de la même opération. En conséquence, les circuits de contrôle du processeur sont plus complexes et utilisent beaucoup de transistors. Le budget en transistors utilisé pour les circuits de contrôle ne sont pas disponibles pour autre chose, comme de la mémoire cache ou des registres. De plus, cela a tendance à limiter la fréquence du processeur.

Les processeurs RISC sont eux plus économes, ils ont moins d'instructions, moins de circuits de contrôle. Ils peuvent utiliser plus de transistors pour autre chose, souvent de la mémoire cache ou des registres. Les processeurs RISC peuvent se permettre d'utiliser beaucoup de registres, ils ont le budget en transistor pour. Et c'est sans compter que la simplicité des circuits de contrôle et des connexions intra-processeur (le bus interne au CPU qu'on verra dans quelques chapitres) fait qu'on peut faire fonctionner le processeur à plus haute fréquence.

Si les registres sont plus nombreux sur les architectures RISC, ce n'est pas qu'une question de budget en transistors. Une autre raison est que les architectures LOAD-STORE ont besoin de plus de registres pour faire le même travail que les architectures à registres, du fait de l'absence d'instructions load-op. La register pressure est donc légèrement plus importante, ce qui est compensée en ajoutant des registres.

La densité de code relative entre RISC et CISC

[modifier | modifier le wikicode]

Si la performance ne donne pas de vainqueur clair entre CISC et RISC, il y a cependant un second critère sur lequel la victoire est bien plus nette. Il s'agit de la densité de code, à savoir la taille des programmes mesurée en octets. De nos jours, la taille des programmes n'est pas importante au point d'être un problème, ce n'est pas ca qui prendra plusieurs gibioctets de mémoire. Par contre, au tout début de l'informatique, la mémoire était chère et limitée, avoir des programmes petits était un avantage clair.

La taille d'un programme dépend de deux choses : le nombre d'instructions et leur taille. Et ces deux paramètres sont influencés par le caractère CISC/RISC. Comme dit plus haut, les architectures CISC ont un avantage pour le nombre d'instruction N, du fait de la présence d'instructions load-op et d'instructions complexes. Mais elles ont aussi un avantage pour la taille des instructions. Les instructions des processeurs CISC sont de taille variable, là où celles des processeurs RISC sont de taille fixe. Les instructions CISC ont une taille allant de très courtes à très longues, celles des RISC sont de taille moyenne. Mais le fait d'avoir des instructions très courtes l'emporte sur les processeurs CISC.

Une minorité de processeurs RISC arrivent à obtenir une densité de code proche de celle des processeurs CISC. Pour cela, ils ont classes d'instructions de taille différente. Un exemple est 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. De telles instructions de taille quasi-variables permettent de grandement gagner en densité de code.

Il faut noter que sur les processeurs modernes, avoir une bonne densité de code a un impact secondaire sur les performances. En effet, les processeurs modernes ont une mémoire cache qui sert à la fois pour les données, mais aussi pour les instructions chargées depuis la mémoire. Les instructions sont conservées dans le cache après leur exécution, pour une exécution ultérieure (ce qui implique une boucle). Si une instruction est dans le cache, elle est lue directement depuis le cache, sans passer par un long accès en mémoire RAM. Les processeurs modernes ont notamment un petit cache d'instruction, spécialement dédié aux instructions. Plus la densité de code est bonne, plus le cache d'instruction pourra contenir d'instructions, plus il sera efficace, plus il filtrera d'accès mémoire.

Un petit historique de la distinction entre processeurs CISC et RISC

[modifier | modifier le wikicode]

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" facilitait la vie des programmeurs. Et leur meilleure densité de code était un sérieux avantage, car la mémoire était rare et chère.

Au cours des années 70-80, la mémoire est devenue moins chère et les langages de haut niveau sont devenus la norme. Le contexte technologique ayant changé, les processeurs CISC étaient à réévaluer. Est-ce que les instructions complexes des processeurs CISC sont vraiment utiles ? Pour le programmeur qui écrit ses programmes en assembleur, elles le sont. Mais avec les 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 utilisaient les instructions load-op correctement, mais n'utilisaient pas les instructions complexes. Les analyses plus récentes fournissent la même conclusion. La faible densité de code n'était pas un problème, vu que les mémoires ont une capacité suffisante, encore que ce soit à nuancer.

L'idée de créer des processeurs RISC commença à germer. Mais les processeurs RISC durent attendre un peu avant de percer. Par exemple, l'IBM 801, un processeur au jeu d'instruction très sobre, fût un véritable échec commercial. 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.

Les CISC et les RISC ont chacun 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 et être peu cher. Mais de nos jours, la performance d'un processeur dépend assez peu du fait que le processeur soit un RISC ou un CISC. Les processeurs modernes disposent de tellement de transistors qu'implémenter des instructions complexes a un impact négligeable. Pour donner une référence, le support des instructions complexes sur les processeurs x86 est estimé à 2 à 3% des transistors de la puce, alors que 50% du budget en transistor des processeurs modernes part dans le cache.

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 instructions qui fonctionnent, peu importe qu'elles viennent de processeurs purement RISC ou CISC. Par exemple, les processeurs ARM récents ont intégré des instructions de copie mémoire, qui sont assez borderline et sont plutôt attendues sur les processeurs CISC.

En parallèle de ces architectures CISC et RISC, qui sont en quelque 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 marché de niche comme les supercalculateurs, etc.

Les jeux d'instructions spécialisés

[modifier | modifier le wikicode]

En parallèle des architectures CISC et RISC, d'autres classes de jeux d'instructions sont apparus. Nous verrons beaucoup de jeux d'instructions dans la suite du cours. Entre les architectures à capacité, les processeurs VLIW, les architectures dataflow, les processeurs SIMD, les Digital Signal Processor, les transport-triggered architectures, les architectures associatives, les architectures neuromorphiques et les achitectures systoliques, il y aura de quoi faire. Malheureusement, nous ne pouvons pas en parler pour le moment. Dans ce qui va suivre, nous allons surtout nous concentrer sur les jeu d'instructions adaptés à certaines catégories de programmes ou de langages de programmation.

Les architectures dédiées à un langage de programmation

[modifier | modifier le wikicode]

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. 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 | modifier le wikicode]

Comme vous le savez sûrement, les langages de programmation de haut niveau sont traduits en langage machine. La traduction d'un programme en un fichier exécutable est donc un processus en deux étapes. Premièrement, la compilation traduit le code source en langage assembleur, une représentation textuelle du langage machine. Puis l'assembleur est assemblé en langage machine. Les deux étapes sont réalisées respectivement par un compilateur et un assembleur.

Représentation intermédiaire d'un compilateur.

Le compilateur ne fait pas forcément la traduction en assembleur directement, la plupart des compilateurs modernes passent par un langage intermédiaire, avant d'être transformés en langage machine. Pour cela, le compilateur est composé de deux parties : 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.

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 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.

Le langage 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 langage intermédiaire est conçu de manière à ce que la traduction en code machine soit la plus simple possible. Et surtout, il est conçu pour pouvoir être transformé en plusieurs langages machines différents sans trop de problèmes. Par exemple, il dispose d'un nombre illimité de registres. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise des registres ou des adresses, en insérant des instructions d'accès mémoire. Et cela permet de gérer des architectures très différentes qui n'ont pas les mêmes nombres de registres.

Control table

Mais si la majorité des langages de haut niveau sont compilés, il en existe qui sont interprétés, c'est à dire que le code source n'est pas traduit directement, mais transformé en code machine à la volée. Là encore, on trouve un langage intermédiaire appelé le bytecode . Le langage de haut niveau est traduit en bytecode, via un processus de pré-compilation, et c'est ce bytecode qui est "exécuté". Plus précisément, le bytecode est passé à un logiciel appelé l'interpréteur, qui lit une instruction à la fois dans le bytecode et exécute une instruction équivalente sur le processeur. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le bytecode est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur.

Fonctionnement d'un interpréteur.

L'avantage est celui de la portabilité, à savoir qu'un même code peut tourner sur plusieurs machines différentes. Le code machine est spécifique à un jeu d’instruction, la compatibilité est donc limitée. C'est très utilisé par certains langages de programmation comme le Python ou le Java, afin d'obtenir une bonne compatibilité : on compile le Python/Java en bytecode, qui lui-même est interprété à l’exécution. Tout ordinateur sur lequel on a installé une machine virtuelle Java/Python peut alors exécuter ce bytecode.

Le bytecode est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Le processeur en question n'existe pas forcément, mais il est possible de décrire son jeu d'instruction en détail. Lesbytecode assez anciens sont conçus pour un type d'architecture spécifique, appelé une machine à pile, qu'on verra dans quelques chapitres. Elles ont la particularité de ne pas avoir de registres et ont un avantage quant à la taille du bytecode. Il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés.

Si le jeu d'instruction d'un bytecode est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la machine SECD, qui sert de langage intermédiaires pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment 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 du langage FORTH.

Mais le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le bytecode Java est compilé ou interprété, 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. Leur langage machine est le bytecode Java lui-même, ce qui supprime l'étape d'interprétation. 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.


Dans ce chapitre, nous allons étudier une fonctionnalité du processeur appelée la pile d'appel. Sans elle, certaines fonctionnalités de nos langages de programmation actuels n'existeraient pas ! Et facilite l'implémentation des fonctions ré-entrantes et des fonctions récursives.

Les fonctions et procédures logicielles

[modifier | modifier le wikicode]

Un programme contient souvent des suites d'instructions dupliquées en plusieurs exemplaires, qui servent à effectuer une tâche bien précise : calculer une fonction mathématique, communiquer avec un périphérique, écrire un fichier sur le disque dur, etc. 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. 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.

Les langages de programmation actuels ont des fonctionnalités liées aux fonctions, qui simplifient bien la vie des programmeurs.

En premier lieu, 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.

En second lieu, la fonction peut prendre des opérandes et/ou calculer un résultat. Les fonctions mathématiques complexes sont dans ce cas : elles prennent des opérandes en entrée et fournissent un résultat. Mais ce ne sont pas les seules. Par exemple, une fonction qui ouvre un fichier prend en opérande le nom de ce fichier. Les opérandes envoyées à une fonction sont appelés des arguments ou paramètres. Les résultats sont eux appelés des valeurs de retour et sont récupérés par le programme principal pour qu'il en fasse quelque chose. Généralement, c'est le programmeur qui décide de conserver une donnée et d'en faire une valeur de retour.

Implémenter des fonctions peut se faire sans support de la part du processeur. Cependant, tous les processeurs modernes incorporent des techniques pour accélérer les fonctions ou rendre leur implémentation plus simple. Au-delà des instructions d'appel et de retour de fonction, les processeurs modernes incorporent une technique appelée la pile d'appel, que nous allons voir dans la section suivante.

La sauvegarde des registres

[modifier | modifier le wikicode]

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 mémoire RAM. 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é !

La gestion des variables locales, des arguments et des valeurs de retour est gérée par la sauvegarde/restauration des registres du processeur. Les arguments sont passés à une fonction dans un registre, qui est préparé avant l'exécution de la fonction. Lorsque la fonction est appelée, elle lit ce registre pour récupérer l'opérande/argument. Les registres pour les arguments ne sont pas sauvegardés ni restaurés après la fin de la fonction. Les valeurs de retour sont gérées de la même manière. Elles sont placées dans un registre spécifique, qui est lu par le programme principal pour consulter cette valeur de retour. Ce registre de retour n'est pas restauré à la fin de la fonction.

La sauvegarde des registres est le plus souvent effectuée par le logiciel, par un petit morceau de code se chargeant de sauvegarder les registres un par un. Plus rarement, certains processeurs ont une instruction pour sauvegarder les registres. 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 :

La sauvegarde de l'adresse de retour

[modifier | modifier le wikicode]

Une fois le sous-programme fini, il faut reprendre l’exécution du programme principal là où il s'était arrêté. Plus précisément, le programme doit 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. Reprendre au bon endroit demande d'exécuter un branchement inconditionnel vers cette 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 comment savoir à quelle instruction reprendre l'exécution de notre programme, une fois le sous-programme terminé ? La seule solution est de sauvegarder l'adresse de retour lorsqu'on appelle la fonction. Et cela peut se faire de plusieurs manières différentes, suivant là où est mémorisée l'adresse de retour.

Une solution dédie un registre spécialisé pour l'adresse de retour, appelé le registre d'adresse de retour. Lorsque le processeur appelle une fonction, l'adresse de retour est sauvegardée dans ce registre, grâce à une instruction Branch And Link. Lorsqu'une fonction termine, l'adresse dans ce registre est utilisée par le branchement qui termine la fonction. La technique a été utilisée sur le processeur TMS 1000 de Texas Instrument, et quelques autres processeurs très peu puissants comme le HP Nanoprocessor. Ce sont tous des processeurs anciens. Mais les autres processeurs utilisent d'autres solutions bien différentes.

La pile d'adresse de retour de certaines architectures embarquées

[modifier | modifier le wikicode]

La solution précédente sauvegarde l'adresse de retour d'une fonction dans un registre. Elle gère le cas à une seule fonction, mais ne marche pas si une fonction en exécute une autre. C'est une situation très courant, que tout programmeur rencontre dès les premiers cours/exercices portant sur les fonctions. Pour donner un exemple, une fonction mathématique qui calcule le nombre de permutations d'un ensemble devra exécuter la fonction de calcul de la factorielle (une opération mathématique). Ou encore une fonction qui vérifie si un numéro de sécurité sociale est correct devra utiliser plusieurs fonctions, chacune faisant une vérification indépendante (on vérifie que telle partie du numéro colle bien, au nom, telle autre au sexe, telle autre à la date de naissance, et ainsi de suite). Une telle situation porte le nom de fonctions imbriquées.

Et gérer les fonctions imbriquées demande de l'aide de la part du processeur. Il existe plusieurs méthodes pour cela. La solution la plus utilisée de nos jours, utilise une fonctionnalité des processeurs appelée la pile d'appel. Mais nous n'allons pas voir la pile d'appel immédiatement. A la place, nous allons en voir une variante fortement simplifiée, utilisée sur certaines architectures embarquées faible performance. Cette solution simplifiée gère les fonctions imbriquées, mais ne gère pas les fonctions dites récursives, terme qu'on expliquera plus bas.

Les piles : une structure de données ordonnée

[modifier | modifier le wikicode]

Avec des fonctions imbriquées, il faut sauvegarder plusieurs adresses de retour, une par fonction appelée. Par exemple, si une fonction A exécute une fonction B, qui elle-même exécute une fonction C, il faut sauvegarder trois adresses de retour : celle où retourner à la fin de la fonction A (dans le programme principal), celle où retourner à la fin de la fonction B (dans la fonction A), celle où retourner à la fin de la fonction C (dans la fonction B).

De plus, elles doivent être sauvegardées dans un certain ordre : de la plus récente à la plus ancienne. Elles doivent être sauvegardées dans l'ordre A, B, C. Par contre, elles seront utilisées dans l'ordre inverse lors du retour de la fonction. En clair, une fois que la fonction C termine, on utilise l'adresse de retour C associée, puis celle de la fonction B quand elle termine, puis celle de A quand elle termine.

Une manière de décrire ce fonctionnement est de le comparer à 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. On ne peut accéder qu'à l'adresse située au sommet de la pile. Comme pour une pile d'assiette, on peut rajouter ou enlever une adresse au sommet de la pile, mais pas toucher aux adresses en dessous, ni les manipuler. L'idée est d'organiser les adresses de retour de la même manière, en utilisant une structure de donnée appelée une pile, qui est bien connue des programmeurs.

Le nombre de manipulations possibles sur cette pile d'adresse se résume donc à deux manipulations de base qu'on peut combiner pour créer des manipulations plus complexes. On peut ainsi :

  • retirer l'adresse de pile au sommet de la pile, pour l'utiliser : on dépile.
  • ajouter une adresse au-dessus des adresses existantes : on empile.
Primitives de gestion d'une pile.

Si vous regardez bien, vous remarquerez que l'adresse 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 à être dépilée (si on n'empile pas d'adresse au-dessus). Ainsi, on sait que dans cette pile, les données sont dépilées dans l'ordre inverse d'empilement. Il s'agit d'un comportement dit LIFO : dernier arrivé, premier sorti. Ce qui est exactement ce qu'on recherche pour la gestion des adresses de retour.

Stack (data structure) LIFO.

La pile d'adresse de retour

[modifier | modifier le wikicode]

La pile d'adresse de retour est une pile, qui mémorise des adresses de retour des fonctions. Lorsqu'une fonction est exécutée, elle empile son adresse de retour au sommet de la pile. Lorsqu'elle se termine, elle dépile l'adresse de retour, celle qui est au sommet de la pile, et effectue un branchement vers celle-ci. Reste à implémenter la pile d'adresse de retour. Pour cela, il y a deux solutions : l'intégrer au processeur, la placer dans la mémoire RAM;

La première utilise une mémoire LIFO, qui n'est autre qu'une pile implémentée avec des transistors. Toute écriture dans ces mémoires ajoute/empile une donnée dedans, toute lecture dépile la donnée la plus récente. Pour rappel, une mémoire LIFO est fabriquée à partir d'une RAM dédiée, qui utilise une case mémoire par adresse de retour. L'idée est de commencer à remplir les cases mémoires à partir de l'adresse 0, l'adresse suivante est empilée à l'adresse suivante (l'adresse 1), celle qui suit est placée dans l'adresse encore suivante (l'adresse 2), et ainsi de suite. Les adresses seront alors naturellement mémorisées dans l'ordre, il y a juste à mémoriser où se trouve le sommet de la pile. Pour cela, on mémorise l'adresse du sommet de la pile dans un registre appelé le pointeur de pile, stack pointer an anglais. Il est incrémenté à chaque fois qu'on empile une donnée, décrémenté quand on dépile une donnée.

Microarchitecture d'une mémoire LIFO

Maintenant qu'on a une LIFO pour la pile d'adresse de retour, reste à voir où la placer. Une solution simple est d'intégrer la mémoire LIFO au processeur. Un exemple est celui des processeurs Intel 4004 et 4040, qui disposaient d'une pile intégrée au processeur. Le processeur avait une pile de 3 adresses maximum, ce qui est peu, mais était assez pour un CPU limité et basse performance qu'est le 4004. Il intégrait un banc de registre qui regroupait les registres de la pile, ainsi que le program counter.

Microarchitecture de l'Intel 4004.

Intégrer la pile dans le processeur a un défaut de taille, si j'ose dire : la taille de la pile est limitée par le budget à transistors, elle dépasse rarement la dizaine d'adresses de retour. La petite taille de la pile d'adresse de retour n'est pas un problème pour les processeurs basse performance, ce qui explique que de nombreux microcontrôleurs et microprocesseurs embarqués utilisent cette technique. Les microcontrôleurs PIC sont dans ce cas, par exemple. Mais pour les processeurs haute performance, d'autres solutions sont utilisées.

Si quelques architectures à pile ont utilisé une LIFO séparée du processeur, c'était une solution loin d'être idéale. Il fallait ajouter un bus dédié entre la mémoire LIFO et le processeur, donc beaucoup de broches. Une meilleure solution place la pile d'adresse de retour en mémoire RAM. L'avantage est qu'on n’est pas limité par la taille de la mémoire LIFO. Le problème est qu'il faut émuler une piler à partir d'un morceau de mémoire RAM. Et la solution est très simple, bien qu'elle vous paraitra un peu contrintuitive. L'idée s'inspire beaucoup de l'implémentation d'une mémoire LIFO, à savoir qu'on commence à remplir la RAM à partir d'une adresse, et qu'on mémorise où se trouve le sommet de la pile.

Pile d'adresse de retour en mémoire RAM.

Cependant, on ne peut pas commencer à remplir la RAM à partir de l'adresse 0 : le programme à exécuter ou des données sont à cet endroit-là. La solution est alors de commencer à remplir à partir de l'adresse haute, la dernière adresse de la mémoire RAM. On remplit alors la pile du haut de la mémoire vers le bas. Les adresses sont empilées dans l'ordre décroissant des adresses, de la dernière adresse vers la première. Le tout est illustré ci-contre.

Une fois cela fait, il suffit d'ajouter un registre qui mémorise où se situe le sommet de la pile. Le registre qui indique où est le sommet de la pile, quelle est son adresse, est appelé le pointeur de pile, ou encore le Stack Pointer (SP). Il est décrémenté lorsqu'on empile une adresse, incrémenté lorsqu'on en dépile une. Il est incrémenté/décrémenté de X si une adresse prend X bytes.

Pile d'adresses de retour

Les instructions d'appel et de retour de fonction

[modifier | modifier le wikicode]

Disposer d'une pile d'adresses de retour est un premier pas. Mais il manque de quoi la manipuler, pour empiler ou dépiler des adresses de retour. Pour cela, une solution est d'utiliser des instructions spécialisées pour les appels et retour de fonction, qui modifient automatiquement la pile d'adresse. Les instructions en question sont des améliorations de l'instruction branch and link vue plus haut.

Appeler une fonction demande d’empiler l'adresse de retour, de modifier le pointeur de pile, puis de brancher vers l'adresse de la fonction. Il existe une instruction pour empiler l'adresse de retour et décrémenter le pointeur de pile. Elle est suivie par un branchement pour appeler la fonction. Le branchement d'appel de fonction, la sauvegarde de l'adresse de retour et la gestion du pointeur de pile sont fusionnés en une seule instruction d'appel de fonction.

De même, le retour de la fonction demande de faire un branchement vers l'adresse de retour et de restaurer le pointeur de pile, etc. Là encore, certains processeurs disposent d'une instruction de retour de fonction dédiée.

L'instruction d'appel de fonction est souvent appelée l'instruction CALL, l'instruction de retour de fonction est souvent appelée l'instruction RET. Des processeurs émulent ces instructions d'appel/retour de fonction avec un branchement complété par d'autres instructions.

Function call in assembly.

Vous remarquerez que ces instructions adressent le pointeur de pile implicitement. Elles manipulent le pointeur de pile, mais n'ont pas besoin qu'on fournisse son nom de registre. Il s'agit là d'un des rares exemple d'adressage implicite. Et encore une fois, il s'agit d'un cas où l'adressage implicite est le fait de branchements.

La sauvegarde des registres et la gestion des arguments sans récursivité

[modifier | modifier le wikicode]

La sauvegarde des registres est réalisée par la fonction elle-même, grâce à un code de sauvegarde au tout début de la fonction et un code de restauration à sa toute fin. Pour cela, le compilateur alloue une petite portion de mémoire, dans laquelle les registres sont copiés lors d'un appel de fonction. La zone de mémoire en question n'a pas de nom consacré par la terminologie, mais je vais l'appeler la register save area. Chaque fonction a sa propre portion de mémoire allouée rien que pour elle. Elle contient juste ce qu'il faut pour sauvegarder les registres, rien de plus. Chaque registre a sa position attitrée dans cette portion de RAM, le code de sauvegarder/restauration des registres en tient compte.

Pour la gestion des arguments, la même méthode peut être utilisée. Le compilateur attribue à chaque fonction une petite portion de RAM où les arguments sont écrits avant l'appel de la fonction. Là encore, la zone de mémoire en question n'a pas de nom consacré par la terminologie, mais je vais l'appeler la function argument area. Le code qui appelle la fonction écrit les arguments dans cette portion de RAM, chaque argument a sa position attitrée, le code est conçu pour en tenir compte. Puis il appelle la fonction et celle-ci lit les arguments en RAM quand elle en a besoin, elle connait leur adresse à l'avance.

Et même chose pour les variables locales, avec une function local area.

Dans les deux cas, le processeur ne se préoccupe pas vraiment de la sauvegarde des registres ou des arguments. Il n'a aucun moyen d'accélérer le processus. La seule possibilité est de fournir des instructions dédiées à la sauvegarde des registres, qu'on a mentionné plus haut.

La pile d'appel : le support des fonctions récursives

[modifier | modifier le wikicode]

L'usage d'une pile d'adresses de retour est une solution intelligente, simple, mais avec un gros défaut qu'il faut aborder. Le problème est qu'elle ne gère pas les fonctions dites récursives. Les fonctions récursives sont des fonctions qui s'appellent elles-mêmes. Elles sont assez rares, mais les programmeurs ont tous eu un cours sur le sujet lors de leurs études, c'est un passage obligé. Il existe aussi des fonctions dites indirectement récursives, où une fonction A appelle une fonction B, qui appelle une fonction C, qui appelle la fonction A (mais avec des opérandes/arguments différents).

Les cadres de pile

[modifier | modifier le wikicode]

En soi, une pile d'adresse de retour fonctionne avec des fonctions récursives. Mais le mécanisme de sauvegarde des registres et de gestion des arguments ne fonctionne pas. Un exemple : imaginez une fonction A qui appelle la fonction B, qui appelle la fonction C, qui elle-même appelle la fonction B. Il y a deux instances de la fonction B en même temps : celle appelée par la fonction A, celle appelée par la fonction C. Et les deux instances ont chacune avec ses propres arguments, sans compter que chacune doit sauvegarder les registres qu'elle modifie. Le même problème a lieu dans le cas général : avec une fonction récursive, il est possible que plusieurs instances de la fonction s'exécutent.

Idéalement, il faudrait plusieurs register save area, plusieurs function local area et plusieurs function argument area, une par instance de la fonction. Mais on ne peut pas préallouer plusieurs register save area, plusieurs function local area et plusieurs function argument area, cela prendrait trop de place, sans compter qu'on sera limité en terme de nombre d'instances par fonction. Une autre solution réalloue dynamiquement plusieurs function local area/register save area/function argument area, exactement autant qu'il y a d'instances. C'est ce qui est fait sur les processeurs modernes, qui utilisent pour ce faire une pile d'appel.

L'idée est d'élargir la pile d'adresse de retour, de manière à ce qu'elle mémorise aussi des register save area, des function local area et des function argument area. Précisément, la pile mémorise des cadres de pile, des espèces de blocs de mémoire de taille fixe ou variable suivant le processeur. Chaque cadre de pile mémorise l'adresse de retour d'un appel de fonction, une register save area, une function local area et une function argument area, avec parfois des informations en plus.

Pile d'appel et cadres de pile.

Les cadres sont créés à chaque appel de fonction et respectent l'organisation en pile. La pile d'adresse de retour est remplacée par une pile appelée la pile d'appel, qui contient plusieurs cadres de pile placés les uns à la suite des autres dans la mémoire. Et comme pour la pile d'adresses de retour, la pile d'appel démarre à l'adresse maximale, et descend vers les adresses plus basses, au fur et à mesure qu'on ajoute des cadres de pile, comme illustré ci-dessous.

Pile d'appel avec les cadres de pile.

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 !

Avec une pile d'appel, le pointeur de pile existe toujours. Cependant, ce n'est plus forcément un registre séparé des autres, comme c'est le cas avec une pile d'adresses de retour. A la place, le processeur utilise un registre général, une registre de données, pour mémoriser le pointeur de pile. La raison deviendra plus clair dans la suite de ce cours.

Quelques compilateurs gardent une trace des limites de chaque cadre de pile. Pour cela, ils complémentent le pointeur de pile avec une autre adresse, qui indique la base du cadre de pile. Elle est appelée le Frame Pointer (FP), ou pointeur de contexte. Il est surtout utile pour les outils de debogages, mais est souvent omis dans les codes optimisés.

Frame pointer.

Les instructions PUSH et POP pour gérer la pile

[modifier | modifier le wikicode]

Lorsqu'on appelle une fonction, on crée un cadre de pile qui une taille suffisante pour 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. Cependant, il nous manque un mécanisme pour ajouter ces données dans un cadre de pile. Les instructions d'appel/retour de fonction permettent d'empiler/dépiler l'adresse de retour, mais pas plus. Heureusement, le processeur intègre des instructions pour empiler ou dépiler des données sur la pile d'appel.

Les données sont ajoutés ou retirés de la pile grâce à deux instructions nommées PUSH et POP.

  • 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.

Les instructions PUSH/POP ont une source et une destination. L'instruction PUSH prend une source, et empile son contenu sur le sommet de la pile, qui est la destination. La source peut être un registre ou une adresse mémoire, éventuellement une constante intégrée dans l'instruction. L'instruction POP fait l'inverse. Sa source est le sommet de la pile, il est envoyé vers une destination qui est soit un registre, soit une adresse mémoire.

Une instruction PUSH/POP effectue un accès mémoire pour transférer la source vers la destination. Mais elles altèrent aussi le pointeur de pile. Il est décrémenté à chaque instruction PUSH, incrémenté à chaque instruction POP. Il est incrémenté/décrémenté de la taille d'un registre, donc de 2 octets sur un processeur 16 bits, 4 octets sur un processeur 32 bits, 8 sur un processeur 64 bits. L'incrémentation/décrémentation du pointeur de pile est donc une vulgaire opération arithmétique, réalisée soit dans des circuits spécialisés, soit dans les circuits de calcul normaux.

Les instructions PUSH et POP adressent le pointeur de pile implicitement. Seule la source est adressée explicitement pour les instructions LOAD, la destination pour les instructions STORE. Les instructions PUSH et POP ont donc un encodage assez court, surtout comparé aux instructions LOAD et STORE. Mais fondamentalement, les instructions PUSH et POP sont des instructions d'accès mémoire LOAD/STORE/MEMCOPY couplées à une incrémentation/décrémentation du pointeur de pile.

La sauvegarde/restauration des registres et la valeur de retour

[modifier | modifier le wikicode]

Les instructions PUSH et POP sont utilisées pour sauvegarder les registres et les restaurer. Pour cela, imaginons qu'ils sont sauvegardés et restaurés à l'intérieur de la fonction. Ils sont sauvegardés au tout début de la fonction, la restauration a lieu à la toute fin, juste avant l'instruction de retour de fonction. Sauvegarder un registre se fait avec une instruction PUSH, qui a pour source le registre à sauvegarder. Le registre est alors placé au sommet de la pile, juste après les données déjà empilées. La restauration se fait avec une instruction POP ayant pour destination ce même registre. Les registres sont sauvegardés dans un ordre précis, avec les instructions PUSH dans un certain ordre, les instructions POP pour dépiler se font dans l'ordre inverse.

Par exemple, pour un processeur avec 4 registres nommés R0, R1, R2 et R3, voici ce que donnerait le code de la fonction :

push R0 ;
push R1 ;
push R2 ;
push R3 ;

...

pop R3 ;
pop R2 ;
pop R1 ;
pop R0 ;

Cependant, la restauration doit tenir compte de la valeur de retour de la 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. Dans ce cas, un registre au moins n'est pas restauré directement, mais laisse la place à la valeur de retour. C'est le code après la fonction qui gère la situation et qui restaure lui-même le registre en question.

Une autre solution est de mettre la valeur de retour sur la pile d'appel, en l'empilant. La valeur de retour est transférée dans la pile avec une instruction PUSH, le code appelant la récupére avec une instruction POP.

Les arguments, les variables locales et la valeur de retour

[modifier | modifier le wikicode]

La transmission des arguments à une fonction peut se faire en les copiant soit dans la pile, soit dans les registres. Avec le passage par la pile, les arguments sont empilés sur la pile et la fonction les récupéré dans la pile directement. Avec le passage par les registres, les paramètres sont copiés dans des registres, qui ne sont pas sauvegardés lors de l'appel de la fonction. Si on utilise le passage par les registres, il faut que le nombre de registres soit suffisant, ce qui dépend du nombre d'argument, mais aussi du nombre de registres. En conséquence, 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.

La gestion des variables locales, arguments et valeurs de retour sont assez complexes. Et surtout, il y a beaucoup de manière de faire, qui dépendent de comment les compilateurs utilisent la pile d'appel. Les différentes manières sont appelées des conventions d'appel, calling convention en anglais. Ils standardisent des choses assez variées : comment sont organisées les données dans un cadre de pile, dans quel ordre sont envoyés les arguments, etc.

Voyons une version simplifiée de la convention d'appel des processeurs x86, sans frame pointer. Pour simplifier les explications, nous allons introduire les termes de code appelant et de code appelé. Le code appelant est celui qui exécute la fonction, le code appelé est la fonction elle-même.

Voici comment se déroule un appel de fonction :

  • En premier lieu, le code appelant PUSH les arguments sur la pile. Il pourrait les passer par les registres, comme c'est le cas sur d'autres architectures avec plus de registres comme les architectures RISC-V, mais passons. Les arguments sont empilés avant d'appeler la fonction, car celle-ci ne sait pas dans quels registres ils sont, ni à quelles adresses.
  • En second lieu, une instruction d'appel de fonction est émise. Elle sauvegarde l'adresse de retour et effectue un branchement vers l'adresse de la fonction.
  • En troisième lieu, le pointeur de pile est modifié de manière à faire de la place aux variables locales sur la pile, ainsi qu'à la register save area. La zone réservée regroupe la register save area et la function local area. Les variables locales ne sont pas empilées sur la pile, ni dépilées. Le code appelant réserve juste de la place et il lira/écrira dedans comme bon lui semble. Pour cela, on soustrait la taille de la function local area au pointeur de pile.

La pile ressemble donc à ceci :

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).

Le retour d'une fonction effectue le même processus, mais en sens inverse.

  • Premièrement, les registres sont restaurés, en les lisant depuis la register save area. Sauf pour le registre qui contient la valeur de retour de la fonction, si elle existe.
  • Deuxièmement, la place prise par les variables locales et les registres est libérée. Pour cela, on ajoute/retire la taille de la function local area du pointeur de pile, pour qu'il pointe avant la function local area, plutôt que après. Mais cela n'est possible que si le pointeur de pile est un registre nommé ou adressable explicitement.
  • Troisièmement, on effectue une instruction de retour de fonction, qui lit l'adresse de retour depuis la pile et effectue un branchement dessus. Le pointeur de pile est aussi mis à jour.
  • Enfin, les arguments sont enlevés de la pile. Les arguments ne sont pas dépilés, ils sont juste "effacés". L'idée est de modifier le pointeur de pile de manière à ce qu'il pointe avant la function argument area, plutôt que après, comme pour le retrait des variables locales.

L'exemple qu'on vient de effectue le retrait des arguments de la pile après l'instruction de retour de fonction. En clair, il délègue le retrait des arguments au code appelant. Le retrait des arguments s'appelle le nettoyage de la pile et il peut être délégué au code appelant ou à la fonction, tout dépend de la convention d'appel utilisée. L'exemple précédent a montré comment le code appelant s'occupe de nettoyer la pile. Mais on aurait pu montrer un autre exemple où le nettoyage est fait par la fonction, avant l'instruction de retour.

Dans l'exemple précédent, vous aurez remarqué que les arguments sont situés sous l'adresse de retour dans la pile. Comment donc la fonction pourrait s'occuper de nettoyer la pile ? La réponse est que l'instruction de retour de fonction s'en occupe ! Pour cela, elle a juste à soustraire la taille de la function argument area du pointeur de pile. Rien de compliqué, elle s'occupe déjà de faire une soustraction sur ce pointeurt en dépilant l'adresse de retour. Pour gérer le nettoyage de la pile, elle peut recevoir une opérande qui lui dit combien soustraire pour dépiler les arguments.

L'adressage du pointeur de pile

[modifier | modifier le wikicode]

La section précédente a introduit un concept assez important : on doit parfois additionner ou soustraire une constante du pointeur de pile. Le pointeur de pile n'est plus seulement altéré par les instructions CALL, RET, PUSH et POP. Il est aussi altéré par des instructions d'addition et de soustraction. Pour cela, les processeurs "récents" ont un pointeur de pile adressable comme le serait n'importe quel registre général, en lui donnant un nom/numéro de registre.

L'adressage du pointeur de pile peut se faire soit de manière explicite, soit de manière implicite, tout dépend de l'instruction. Les instructions d'addition/soustraction adressent ce registre, ce qui permet de manipuler les cadres de pile. Pour cela, elles l'adressent explicitement, en utilisant son nom/numéro de registre. Pour les instructions d'appel/retour de fonction, ainsi que PUSH et POP, l'adressage peut être soit explicite, soit implicite. S'il est implicite. Le processeur est conçu pour aller chercher le pointeur de pile dans un registre général bien précis.

Il y a généralement des limitations quant à l'usage du registre pointeur de pile. Les limitations varient cependant grandement selon le processeur. Sur les plus laxistes, il est possible d'utiliser le registre pointeur de pile comme un registre général supplémentaire, ce qui n'est cependant pas conseillé. Par exemple, sur les processeurs x86, le registre SP a pour seule limitation qu'il ne peut pas être utilisé dans l'adressage base + indice, et encore : seulement en tant qu'indice.

Il peut aussi être utilisé comme opérande pour des instructions LOAD/STORE, ce qui permet de simplifier l'adressage des arguments et variables locales dans un cadre de pile. La function local area et la function argument area sont deux zones mémoire séparées, mais placées dans un cadre de pile. Reste à récupérer les arguments ou les variables locales pour les charger dans les registres, quand on en a besoin. Pour cela, les instructions LOAD et STORE ne peuvent pas utiliser leur adresse mémoire, vu qu'elle change selon la position dans la pile. A la place, les instructions LOAD et STORE utilise une variante du mode d'adressage base + décalage, adaptée pour la pile.

Elles adressent arguments et variables locales par leur position dans le cadre de pile. Ils disent que telle variable locale est 8 octets avant le sommet de la pile, que tel argument est 16 octets avant, etc. L'adresse à lire/écrire se calcule en prenant le pointeur de pile et en additionnant/soustrayant la position, le décalage. Une instruction LOAD/STORE précise alors le décalage/position avec une constante immédiate, le pointeur de pile est lui adressé implicitement ou explicitement. L'instruction LOAD/STORE ajoute alors automatiquement cette position au pointeur de pile, pour générer l'adresse à lire/écrire. Si le pointeur de pile est adressable, l'adresse peut être calculée dans les registres et accédée via un simple adressage indirect.

Les optimisations et fonctionnalités de la pile d'appel

[modifier | modifier le wikicode]

Les processeurs modernes incorporent de nombreuses fonctionnalités de sécurité, ainsi que des optimisations de la pile d'appel. Dans cette section, nous allons voir certaines d'entre elles.

[modifier | modifier le wikicode]

L'optimisation que nous allons voir porte sur la gestion de l'adresse de retour. Plus haut, nous avons vu qu'elle est empilée sur la pile d'appel, donc en mémoire RAM. L'instruction d'appel de fonction effectue cette sauvegarde automatiquement, ou alors c'est le fait d'une instruction dédiée, peu importe. L'optimisation que nous allons voir sauvegarde l'adresse de retour dans les registres, soit dans un registre dédié, soit dans les registres généraux. Il s'agit d'une technique utilisée sur les processeurs RISC, qui ont beaucoup de registres. Ils peuvent donc dédier des registres pour l'adresse de retour, pour optimiser les appels de fonction.

Dans ce qui suit, nous désignerons le registre où est sauvegardé l'adresse de retour par le terme Link Register, qui sera abrévié LR dans ce qui suit. Il peut désigner soit un registre dédié, soit un registre général. Les architectures PA-RISC, RISC-V, SPARC et ARM utilisent un registre général comme link register, les processeurs POWER PC utilisaient un link register dédié, qui ne sert qu'à stocker l'adresse de retour et ne sert pas à autre chose. Le cas le plus simple est clairement celui où l'adresse de retour est sauvegardée dans un registre général arbitraire, donc le premier cas de la liste précédente. Mais nous ferons un abus de langage : nous parlerons de processeur à link register dans les trois cas précédents.

Les processeurs avec un Link Register disposent de deux instructions simplifiées pour les appels et retour de fonction. L'instruction d'appel de fonction est l'instruction Branch And Link. Elle effectue deux opérations : un branchement pour appeler la fonction, une copie de l'adresse de retour dans le link register. L'instruction Branch From Link Register est l'instruction de retour de fonction. Elle récupére l'adresse de retour dans le link register et effectue un branchement vers celle-ci. Il s'agit donc d'un simple branchement indirect, rien de plus.

L'idée est que la sauvegarde de l'adresse de retour sur la pile se fait en deux temps : la sauvegarde de l'adresse de retour dans le link register, puis la copie de ce registre dans la pile d'appel. Les instructions d'appel et de retour de fonction sont donc fortement simplifiées, vu qu'elles ne font pas d'accès mémoire. L'instruction Branch And Link est suivie par une instruction PUSH pour sauvegarder l'adresse de retour sur la pile. A l'inverse, un retour de fonction demande d'exécuter une instruction POP pour copier l'adresse de retour depuis la pile, puis de faire un branchement Branch From Link Register pour le retour de fonction.

L'avantage est que cela découple le branchement de la sauvegarde de l'adresse de retour sur la pile. L'instruction Branch And Link n'accède qu'aux registres, idem avec l'instruction Branch From Link Register. Et c'est pour ça qu'on doit les coupler avec une instruction PUSH ou POP. On utilise deux instructions pour l'appel d'une fonction, idem pour le retour, mais le tout est plus flexible. Les instructions PUSH et POP peuvent être éliminées dans certains cas, notamment pour ce qui s'appelle les fonctions terminales.

Une fonction terminale est une fonction qui n'appelle pas d'autres fonctions lors de son exécution. De telles fonctions tendent à être assez simples, surtout avec les pratiques de programmation actuelles. En conséquence, elles utilisent peu de registres, bien que ce ne soit qu'une tendance, pas une généralité. Sur les processeurs RISC, il y a assez de registres pour ne pas avoir besoin de sauvegarder l'adresse de retour sur la pile, elle peut rester dans les registres, il y en aura assez pour exécuter la fonction. Elles sauvegardent leur adresse de retour dans le link register, utilisent les registres généraux pour faire leur travail, et branchent vers l'adresse dans le link register une fois qu'elles ont terminé leur travail, pas besoin d’accéder à la pile. On évite donc deux accès mémoire pour de telles fonctions terminales, ce qui est plus rapide.

Le cas des fonctions imbriquées est cependant assez simple à gérer. L'adresse de retour étant dans un registre général, elle est sauvegardée en même temps que les autres, idem pour sa restauration. Imaginons le cas où une fonction A appelle une fonction B, qui appelle une fonction C. La fonction A effectue un appel de fonction avec l'instruction branch and link. La fonction B fait ses calculs, puis vient le moment d'exécuter la fonction C. Elle sauvegarde alors le link register, puis effectue l'instruction branch and link et exécute la fonction C. La fonction C est une fonction terminale : elle s'exécute et retourne en utilisant l'adresse dans le link register. La fonction B reprend la main et restaure alors le link register qu'elle avait sauvegardée, pour restaurer la bonne adresse de retour, celle de A. Lorsqu'elle retourne, elle utilise l'adresse dans le link register et reprend à la fonction A.

Les processeurs RISC sauvegardent l'adresse de retour dans un registre général. Sur les processeurs PA-RISC, RISC-V, IBM System/360 et z/Architecture, n'importe quel registre général peut mémoriser l'adresse de retour, le programme est codé pour en choisir un bien précis, de préférence un registre inoccupé ou à écraser. Les instructions branch and link et branch from link register adressent le registre choisit avec l'adressage inhérent, avec son nom de registre.

Sur les processeurs SPARC et ARM, le link register est un registre général, mais où les instructions branch and link et branch from link register adressent un registre général précis. Par exemple, sur les processeurs ARMv7, le link register est le registre général R14. L'adresse de retour est automatiquement sauvegardée dans le registre R14 par branch and link, elle est lue depuis ce registre par l'instruction branch from link register. Mais pendant l'exécution de la fonction, l'adresse peut être envoyée sur la pile d'appel ou déplacée dans un autre registre. Le link register est unique, il est adressé implicitement par des instructions.

Sur les architectures POWER PC, le link register est un registre dédié, qui n'est pas un registre général. Il ne sert qu'à stocker l'adresse de retour, pas autre chose. Mais il est possible de sauvegarder/restaurer le link register sur la pile d'appel. Il faut pour cela utiliser des instructions spéciales, qui copient le link register vers un registre général, ou directement sur la pile d'appel.

Le fenêtrage de registres

[modifier | modifier le wikicode]

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.

Fenêtre de registres.

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. Mais elle ne peut pas toucher aux registres dans les autres fenêtres, les fenêtres sont isolées les unes des autres. S'il ne reste pas de fenêtre inutilisée, on est obligé de sauvegarder une fenêtre complète dans la pile.

L'implémentation précédente a des fenêtres de taille fixe. 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.

La pile fantôme pour vérifier les adresses de retour

[modifier | modifier le wikicode]

Les compilateurs modernes peuvent complémenter la pile d'appel avec une pile d'adresse de retour. La pile d'appel stocke toujours les adresses stockées dans la pile d'appel, ce qui fait que la pile d'appel est redondante. Et c'est justement cette redondance qui est utile, dans le sens où certaines techniques de sécurité sont plus faciles à implémenter avec une pile d'appel séparée, comme on le verra plus bas.

Diverses attaques informatiques modifient l'adresse de retour d'une fonction pour exécuter du code malicieux. L'attaque insère un code malicieux dans un programme (par exemple, un virus) et l’exécute 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 le devait une fois la fonction détournée terminée. 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.

Il existe diverses techniques pour éviter cela et certaines de ces techniques se basent sur une shadow stack, une pile fantôme. Celle-ci est une pile d'adresse de retour, 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. 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.

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 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 | modifier le wikicode]

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 | modifier le wikicode]

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. 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 soit par une bibliothèque logicielle, soit par le système d'exploitation par le biais d'exceptions matérielles. 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 | modifier le wikicode]

Les interruptions matérielles, aussi appelées IRQ, sont des interruptions déclenchées par un périphérique ou un circuit extérieur au processeur. Elles sont soit générées par un circuit sur la carte mère, soit par un périphérique, l'essentiel est qu'elles proviennent de l'extérieur du processeur et ne sont pas d'origine logicielle.

L'exemple d'utilisation typique des interruptions matérielles est 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. 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 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 fini de compter, le jeu vidéo est alors prévenu, et fait ce qu'il a à faire.

Un autre exemple, qui n'est plus d'actualité, est le rafraichissement mémoire des DRAM sur quelques anciens ordinateurs. Dans l'ancien temps, le rafraichissement mémoire était géré par le processeur, pas par le contrôleur mémoire. La plupart des processeurs intégraient des optimisations pour gérer le rafraichissement mémoire par eux-même, sans recourir à des interruptions. Mais quelques ordinateurs ont tenté de relier des processeurs très simples à une mémoire DRAM, directement, alors que les processeurs n'avaient aucune optimisation du rafraichissement mémoire. La gestion du rafraichissement était alors gérée via des interruptions : tous les x millisecondes, un timer déclenchait une interruption de rafraichissement mémoire, qui rafraichissait la mémoire ou une adresse précise. Cette solution était très peu performante.

Les interruptions logicielles

[modifier | modifier le wikicode]

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 | modifier le wikicode]

Le BIOS fournit des routines d'interruption pour gérer les périphériques et matériels les plus courants. Il y a une interruption pour communiquer avec le port série RS232 de notre ordinateur, une autre pour le port parallèle, une autre pour le clavier, une autre pour la carte graphique, et quelques autres. Les interruptions en questions gèrent des standards de base, utilisés pour gérer les périphériques et matériels les plus courants. Par exemple, tant que la carte graphique supporte le standard VGA, le BIOS peut l'utiliser, bien que partiellement et seulement pour gérer les fonctions de base. idem avec le clavier : les standards PS/2 et USB sont gérés de base par le BIOS. 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 ».

Par exemple, si aucune ROM vidéo n'est détectée, le BIOS peut quand même communiquer directement avec la carte graphique en utilisant une interruption dédiée. Elle a plusieurs utilités différentes, mais est généralement limitée à l’affichage de texte (la carte graphique est gérée en mode texte). Dans ce mode, elle peut tout aussi bien envoyer du texte à l'écran (sortie) que renvoyer la position du curseur à l'écran (entrée).

L'usage de ces standards matériel était extrêmement puissant malgré sa simplicité. 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 interruptions du BIOS ! Mais une fois le système d'exploitation démarré, les interruptions du BIOS ne servent plus, les pilotes de périphériques prennent le relai. De nos jours, l'UEFI fournit encore un équivalent des anciennes interruptions du BIOS, mais seulement pour la rétrocompatibilité. Les interruptions ne sont plus utilisées lors du démarrage de l'ordinateur, le firmware est programmé plus finement et gére le matériel d'une manière autre.

Certaines routines peuvent 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 | modifier le wikicode]
Différence entre le système d'exploitation et les applications.

Avant de poursuivre, 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é et de sécurité. 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. Il y a donc une séparation stricte entre :

  • les programmes systèmes qui gèrent la mémoire et les périphériques ;
  • les programmes applicatifs ou applications, qui délèguent la gestion de la mémoire et des périphériques aux programmes systèmes.

Les programmes systèmes sont en réalité des sous-programmes, des fonctions utilisées pour accéder à la carte graphique, manipuler la mémoire, gérer des fichiers, etc. Les fonctions en question sont exécutées en faisant des appels de fonction classiques, appelés 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 fonctions pré-programmées, qui agissent sur le matériel. 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 de plusieurs manières différentes, mais ils sont presque toujours des interruptions logicielles.

Les programmes systèmes sont le plus souvent des routines d'interruptions, fournies par l'OS ou les pilotes de périphérique. Un appel système n'est donc qu'une interruption logicielle qui exécute à la demande la routine adéquate. Pour simplifier, l'ensemble de ces routines d'interruption porte le nom de noyau du système d'exploitation. Il regroupe les programmes systèmes, qu'on peut appeler avec des appels système. Le noyau d'un OS est la partie de l'OS qui s'occupe de la gestion du matériel, des périphériques et des opérations demandant de reconfigurer le processeur.

Mais sans intervention du matériel, rien n’empêche à un programma applicatif de lire ou d'écrire dans les registres des périphériques, par exemple. Mais les processeurs utilisent des sécurités pour cela, que nous allons voir dans ce qui suit.

Les niveaux de privilège (anneaux mémoire)

[modifier | modifier le wikicode]

Nous venons juste de voir que les interruptions logicielles sont surtout utilisées pour manipuler un périphérique, accéder au matériel. Et ce qu'elles soient fournies par le BIOS ou le système d'exploitation. C'est une différence fondamentale entre interruption logicielle et simple appel de fonction. Les interruptions peuvent manipuler le matériel, mais du code normal en est incapable. La raison tient à une sécurité incorporée dans tous les systèmes modernes : les niveaux de privilèges, aussi appelés des anneaux mémoires.

Les niveaux de privilèges varient grandement d'un jeu d'instruction à l'autre, mais il y en a au minimum deux : le mode noyau et le mode utilisateur. Le mode noyau est réservé au noyau du système d'exploitation, le mode utilisateur est réservé aux autres logiciels. En plus des modes utilisateur et noyau, les processeurs gèrent souvent un autre mode appelé mode système, réservé au firmware/BIOS. D'autres modes sont parfois présents sur les processeurs modernes, avec des intermédiaires entre mode noyau et utilisateur. Pour résumer : le mode utilisateur est réservé aux logiciels applicatifs, le mode noyau est réservé au noyau du système d'exploitation (d'où son nom), le mode système est réservé au firmware/BIOS.

Suivant le mode de fonctionnement, certaines opérations sensibles sont interdites. Par exemple, l'accès aux périphériques est interdit en mode utilisateur, mais autorisé dans le mode noyau. De même, certaines instructions sont autorisées seulement en mode noyau. Par exemple, les instructions permettant de configurer le processeur, en changeant ses registres de contrôle, sont accessibles seulement en mode noyau et en mode système. L'avantage est que cela empêche les programmes d’accéder directement au matériel, seul le système d'exploitation et le BIOS en sont capables. Généralement, le mode utilisateur est le plus limité, le mode système permet absolument tout, le mode noyau est assez proche du mode système avec quelques limitations en plus.

Les anneaux mémoire/niveaux de privilèges étaient initialement gérés par des mécanismes purement logiciels, mais sont actuellement gérés par le processeur. Pour cela, le registre de contrôle du processeur contient un bit qui précise si le programme en cours est en mode noyau, utilisateur, hyperviseur ou système. À 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 pour le mode en cours, une exception matérielle est levée. Généralement, le programme est arrêté sauvagement et un message d'erreur est affiché.

Les interruptions basculent en mode noyau/système

[modifier | modifier le wikicode]

L'ordinateur démarre généralement en mode système, puis il bascule en mode hyperviseur, puis en mode noyau, et enfin en mode utilisateur. Il peut revenir vers un mode antérieur sous certaines conditions. Et justement, toute interruption bascule automatiquement le processeur dans l'espace noyau, voire système. C'est une nécessité pour les interruptions logicielles, afin de passer d'un programme en espace utilisateur à une routine qui est en espace noyau. Les interruptions matérielles doivent aussi faire la transition en espace noyau ou système, car l'accès au matériel n'est pas possible en espace utilisateur.

Par exemple, une interruption qui fait passer du mode utilisateur vers le noyau permet à un logiciel de déléguer une tâche au noyau du système d'exploitation. De même, une interruption qui fait passer du mode noyau vers le mode système permet à l'OS de communiquer avec le firmware, de déléguer une fonction vers le BIOS. Il s'agit là d'une sécurité : le passage d'un mode à un autre est contrôlé et n'est autorisé qu'en utilisant des instructions très précises, en l’occurrence des interruptions.

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.

Le mode noyau et le mode utilisateur : logiciels et OS

[modifier | modifier le wikicode]
Espace noyau et utilisateur.

Tous les processeurs des PC modernes (x86 64 bits) gèrent au moins deux niveaux de privilèges : un mode noyau pour le noyau de l'OS et un mode utilisateur pour les applications. Tout est autorisé en mode noyau, alors que le 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.

Le mode utilisateur n'a pas accès à certaines instructions importantes, appelées des instructions privilégiées, qui ne s'exécutent qu'en espace noyau. Elles regroupent les instructions pour accéder aux entrées-sorties et celles pour configurer le processeur. On peut considérer qu'il s'agit d'instructions que seul l'O.S peut utiliser. À côté, on trouve des instructions non-privilégiées qui peuvent s’exécuter aussi bien en mode noyau qu'en mode utilisateur. Si un programme tente d'exécuter une instruction privilégiée en espace utilisateur, le processeur considère qu'une erreur a eu lieu et lance une exception matérielle.

De plus, l'espace utilisateur restreint l'accès à la mémoire par divers mécanismes dits de protection mémoire, alors que le mode noyau n'a pas de restrictions. 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.

La séparation en mode noyau et utilisateur explique pourquoi les appels systèmes sont implémentés avec des interruptions, et non des appels de fonction basiques. La raison est qu'un appel système branche vers un programme système en espace noyau, alors que le programme qui lance l'appel système est en espace utilisateur. Même sans accès aux périphériques, le passage en mode noyau est nécessaire pour passer outre la protection mémoire. La routine de l'appel système est dans une portion de mémoire réservée à l'OS, auquel le programme exécutant n'a pas accès en espace utilisateur. Il en est de même pour certaines structures de données du système d'exploitation, accessibles seulement dans l'espace noyau. Or, les appels de fonction et branchements ne permettent pas de passer de l'espace utilisateur à l'espace noyau, alors que les interruptions le font automatiquement.

Vu qu'une interruption logicielle est assez lente, 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. Ce qui fait qu'assimiler interruptions logicielles et appels systèmes est en soi une erreur, mais même si les deux sont très liés.

Quelques processeurs ont des registres d'état séparés pour le mode noyau et le mode utilisateur. Le registre d'état du mode noyau ne peut être consulté que si le processeur est en mode noyau, l'autre registre d'état est consultable à la fois en mode noyau et utilisateur.

Le mode système pour les firmwares

[modifier | modifier le wikicode]

Dans le mode de gestion système, dans lequel l'exécution du système d'exploitation est suspendue et laisse la main au firmware. Il est possible d’entrer dans ce mode en utilisant une interruption spéciale, appelée System Management Interrupt (SMI ). Il est utilisé pour la gestion de l'énergie de l'ordinateur, la gestion thermique, pour gérer des interruptions non-masquables, pour gérer des défaillances matérielles graves, pour gérer certains périphériques (émuler les claviers/souris PS/2, certaines fonctionnalités USB/Thunderbolt), éventuellement pour communiquer avec la puce TPM du chipset.

Sur les CPU x86, il s'appelle le System Management Mode, abrévié SMM. Il permet à l'OS de laisser la main au BIOS pour exécuter des interruptions spécifiques. Il a été rendu disponible sur les CPU 386, et a ensuite été utilisé pour faciliter l'implémentation du standard APM, un standard de gestion de l'énergie assez ancien qui a laissé sa place à l'ACPI. ACPI qui utilisait aussi ce mode dans ses premières implémentations et l'utilise encore sur certaines cartes mères. Par sécurité, ce mode utilise un espace d'adressage différent de celui utilisé par l'OS afin de garantir un minimum de protection mémoire. Ce qui empêche pas certains malwares d'utiliser le mode SMM pour faire leur travail, voire se cacher de l'OS.

Il faut noter que le passage en mode système se fait en mode noyau, mais n'est pas disponible en mode utilisateur. Il s'agit d'une sécurité, qui garantit que les logiciels n'ont pas accès aux interruptions du firmware/BIOS directement. Le système d'exploitation peut communiquer avec le BIOS, pour que ce dernier l'aide à gérer le matériel. Par exemple, le système d'exploitation peut demander au BIOS quels sont les périphériques installés sur l'ordinateur. L'OS peut ainsi savoir quelle est la carte graphique ou la carte son installée, il a juste à demander au BIOS. Mais un logiciel utilisateur n'est pas censé pouvoir faire ça, seul le noyau est censé le faire.

Les modes intermédiaires pour les pilotes de périphériques

[modifier | modifier le wikicode]
Niveaux de privilèges sur les processeurs x86.

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, de même que le Multics system original. À 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.

Sur PC, les 4 niveaux de privilèges étaient autrefois utilisés pour la virtualisation. Les anciens processeurs x86 n'avaient pas de mode hyperviseur. Alors à la place, le noyau du système d'exploitation était placé dans le second niveau de privilège, celui juste après le mode noyau. La majorité des opérations de l'OS étaient possibles dans ce mode, sauf quelques unes qui requéraient le mode noyau. L'hyperviseur émulait ces interruptions qui demandaient le mode noyau en fournissant ses routines à lui, conçues pour gérer la virtualisation. L'ajout d'un réel mode hyperviseur a changé la donne.

Sur les processeurs Data General Eclipse MV/8000, les modes disposaient chacun de zones mémoires séparées. Les trois bits de poids fort du program counter étaient utilisés pour déterminer le niveau de privilége. Le processeur était un processeur 32 bits, ce qui fait que les 4 gibioctets de RAM adressables étaient découpés en 8 blocs de 512 mébioctets. Le code exécuté avait son niveau de privilège qui dépendait du bloc de 512 mébioctet dans lequel il était. Tout branchement qui modifiait les 3 bits de poids fort du program counter entrainait automatiquement un changement de niveau de privilège.

L'implémentation des interruptions

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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. Appeler une interruption demande techniquement de connaitre son adresse, pour effectuer un branchement vers celle-ci. Mais comment déterminer son adresse ?

La solution la plus simple est de placer chaque routine d'interruption systématiquement au même endroit en mémoire, ce qui fait que l'adresse est connue à l'avance. Les appels systèmes sont alors des appels de fonctions basiques, avec un branchement inconditionnel vers une adresse fixe. La technique marche bien pour les interruptions du firmware, comme celles du BIOS, qui sont placées en ROM à une position fixe. Mais elle est trop contraignante dès qu'un système d'exploitation est impliqué. Fixer la position et la taille de chaque routine d'interruption ne marche pas si on ne connait pas à l'avance ni le nombre, ni la taille, ni la fonction des routines.

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, le système d'exploitation mémorise une table de correspondance qui associe chaque numéro à l'adresse de l'interruption. La table de correspondance s'appelle le vecteur d'interruption. Par exemple, la dixième adresse de la table pointe vers la dixième interruption, à savoir l'interruption qui gère le disque dur ou le lecteur de disquette. Il s'agit plus précisément un tableau d'adresses, à savoir que les adresses de chaque interruption sont placées dans un bloc de mémoire, les unes à la suite des autres.

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.

Pour ceux qui connaissent la programmation, le vecteur d'interruption est un tableau de pointeurs sur fonction, les fonctions étant les routines à exécuter.

L'avantage est que l'adresse de la routine n'a pas à être précisée lors de la conception de l'OS, et elle peut même changer lors de l'exécution d'un programme ! Le vecteur d'interruption peut être mis à jour, les adresses changées, ce qui permet de remplacer à la volée les routines d'interruptions utilisées. Une adresse qui pointe vers telle routine peut être remplacée par une autre adresse qui pointe vers une autre routine. 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. Le vecteur d'interruption est placé en mémoire RAM est initialisé au démarrage de l'ordinateur. Il est initialisé avec les adresses des routines du Firmware, à savoir les routines du BIOS ou de l'UEFI sur les PCs. Mais une fois que le système d'exploitation démarré, les adresses sont mises à jour pour pointer vers les routines du système d'exploitation et des pilotes de périphériques. Cette mise à jour est effectuée par le système d'exploitation, une fois que le BIOS lui a laissé les commandes.

L'usage d'un vecteur d'interruption permet donc une plus grande flexibilité et une compatibilité maximale. Elle permet au système d'exploitation de configurer les interruptions comme il le souhaite et de placer les routines d'interruption où il veut. Par contre, elle a un léger cout en performance, très mineur. La raison est que déterminer l'adresse d'une routine d'interruption se fait en deux temps, au lieu d'un. Au lieu de faire un branchement vers une adresse connue à l'avance, on doit récupérer l'adresse dans le vecteur d'interruption, puis faire le branchement. Il y a un accès mémoire en plus, il y a un niveau d'indirection en plus. Cela explique pourquoi un appel système n'est pas qu'un simple appel de fonction, pourquoi 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, l'instruction d'interruption logicielle lit l'adresse automatiquement dans le vecteur d'interruption, idem avec les exceptions matérielles et les IRQ. 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 masquage d'interruptions : désactiver les interruptions

[modifier | modifier le wikicode]

Il est possible de désactiver temporairement l’exécution des interruptions, quelle qu’en soit la raison. Le terme utilisé n'est pas désactivation des interruption, mais masquage des interruptions. Le masquage d'interruption permet de bloquer des interruptions temporairement, pour soit les ignorer, soit les exécuter ultérieurement. La désactivation peut-être totale ou partielle : totale quand toutes les interruptions sont désactivées, partielle quand seule une minorité l'est.

Le registre de contrôle, qui permet de configurer le processeur, incorpore souvent un bit qui permet d'activer/désactiver les interruptions de manière globale. En modifiant ce bit, on peut activer ou désactiver les interruptions. Le bit en question n'est modifiable qu'en mode noyau. D'autres bits du registre de contrôle permettent de désactiver certaines interruptions précises, voir de choisir lesquelles activer/désactiver. Et ce n'est pas le seul, d'autres bits de configuration ne sont modifiables qu'en mode noyau, pas en mode utilisateur.

Désactiver les interruptions est utile dans certaines situations assez complexes, notamment quand le système d'exploitation en a besoin. 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. Une contrainte est que chaque fonction doit s’exécuter en un temps définit à l'avance, qu'il ne doit pas dépasser. Par exemple, prenons le cas d'une fonction devant s’exécuter en moins de 300 millisecondes. Le code en question prend 200 ms sans interruption, ce qui fait 100ms de marge de sureté. Si plusieurs interruptions surviennent, les 100ms de marge de sureté peuvent être dépassées. Désactiver les interruptions pendant le temps d’exécution du code permet d'éviter cela.

Et à ce petit jeu, il faut distinguer les interruptions masquables qui peuvent être ignorées ou retardées, des interruptions non-masquables, à savoir des interruptions qui ne doivent pas être masquées, quelle que soit la situation. Le terme "interruption non-masquable" est souvent abrévié en NMI, ce qui signifie Non Maskable Interrupt. Dans ce qui suit, nous parlerons parfois de NMI par abus de langage, pour simplifier l'écriture.

Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Les défaillances matérielles en question regroupent des situations très variées : une perte de l'alimentation, une erreur de parité mémoire, une surchauffe du processeur, etc. Elles sont généralement détectées par un paquet de circuits dédiés, souvent par des circuits placés sur la carte mère, en dehors d'un contrôleur de périphérique : un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.

Un exemple d'utilisation des interruptions non-masquable est celui d'une surchauffe du processeur. Le processeur et la carte mère contiennent de nombreux capteurs de température, eux-même connectés à des circuits de surveillance. Si la température est trop élevée, les circuits de surveillance déclenchent une interruption non-masquable. La routine d'interruption non-masquable effectue quelques manipulations d'urgence et éteint l'ordinateur par sécurité. Mais elle l'éteint d'une manière assez propre, en faisant quelques manipulations de dernière seconde.

Même chose en cas de défaillance de l'alimentation électrique, par exemple lorsqu'on débranche la prise, une coupure de courant, un problème matériel avec les régulateurs de tension, des condensateurs de la carte mère qui fondent, etc. Les ordinateurs modernes peuvent fonctionner durant quelques millisecondes lors d'une défaillance de l'alimentation, parce que la carte mère contient des condensateurs qui maintienne la tension d'alimentation pendant quelques millisecondes. Ce qui lui laisse le temps de faire quelques sauvegardes mineures, comme générer un crash dump, avant d'éteindre l'ordinateur proprement.

Outre les défaillances matérielles, les interruptions non-masquables sont aussi utilisée pour la gestion du watchdog timer. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'il suspecte qu'il a planté. C'est un compteur/décompteur connecté à l'entrée RESET du processeur, pour qu'un débordement d'entier du compteur déclenche un RESET. Pour éviter cela, une interruption non-masquable réinitialise le watchdog timer régulièrement, avant qu'il déborde. L'interruption est programmée soit par le watchdog timer, soit par un autre timer, peu importe. L'interruption en question doit être non-masquable, car on ne veut pas que l’ordinateur redémarre car l'interruption du watchdog timer a été masqué pendant trop longtemps, même si le masquage était pertinent.

Le Watchdog Timer et l'ordinateur.

Les optimisations des interruptions

[modifier | modifier le wikicode]

En soi, les interruptions sur des appels de fonction améliorés. Les optimisations générales pour les appels de fonction marchent aussi pour les interruptions. Par exemple, les interruptions peuvent profiter du fenêtrage de registres. Lorsqu'une interruption se déclenche, elle se voit allouer sa propre fenêtre de registres, séparée des autres. Cependant, de nombreux processeurs incorporent des optimisations pour accélérer spécifiquement le traitement des interruptions, pas seulement les appels de fonction. Il s'agit souvent de processeurs dédiés à l'embarqué, qui sont peu puissants et doivent consommer peu, et qui doivent communiquer avec un grand nombre d'entrée-sorties. Les optimisations en question fournissent des registres dédiés aux interruptions, parfois une pile d'appel dédiée.

Les registres dédiés aux interruptions

[modifier | modifier le wikicode]

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. L'ensemble de registres dédié aux interruptions regroupe les registres d'interruption.

Ce système permet de simplifier grandement la gestion des interruptions matérielles. Lors d'une interruption sur un processeur sans registres d'interruption, l'interruption doit sauvegarder les registres qu'elle manipule, pour ne pas perturber le programme interrompu. Avec des registres d'interruption, il n'y a pas besoin de sauvegarder les registres lors d'une interruption, car les registres normaux et les registres d'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. La sauvegarde de l'adresse de retour sur la pile est réalisée automatiquement par le processeur.

La pile dédiée aux interruptions

[modifier | modifier le wikicode]

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.

Annexe : la multiprogrammation et les commutations de contexte

[modifier | modifier le wikicode]

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. Il s'agit de la technique dite du multiplexage temporel, terme bien complexe pour simplement dire : exécution à tour de rôle.

Du moins, c'était le fonctionnement avant l'arrivée des processeurs multi-cœurs. Maintenant, des logiciels distincts s’exécutent sur des processeur distincts. Du moins, tant qu'on a moins de logiciels que de cœurs. Si le nombre de cœurs n'est pas suffisant, le multiplexage temporel est utilisé.

Les commutations de contexte

[modifier | modifier le wikicode]

Passer d'un programme à un autre est un processus complexe, résumé sous le terme de commutation de contexte. Une commutation de contexte demande de stopper l'exécution d'un programme pour laisser la main à un autre. Et cela devrait vous rappeler quelque chose : c'est ce qui se fait lors d'une interruption, par exemple.

L'idée est de stopper le programme en cours avec une interruption de commutation de contexte, qui donne la main au programme suivant. Pour cela, l'interruption de commutation de contexte agit sur les registres du processeur, au même titre que le fait une interruption ou un appel de fonction. Premièrement, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour va lui restaurer son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.

Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des contextes matériels. Les processeurs incorporent des instructions pour dire : je veux sauvegarder l'état du processeur dans tel contexte, restaurer celui-ci, etc.

Par exemple, les processeurs x86 peuvent faire des commutations de contexte presque totalement en matériel, grâce à des techniques de Hardware Task Switching. Malheureusement, ces techniques ne sont pas utilisées, à cause de quelques défauts. Par exemple, le Hardware Task Switching des CPU x86 ne sauvegarde pas tous les registres : il oublie les registres flottants !

Les identifiants de processus intégrés au processeur

[modifier | modifier le wikicode]

Sur les processeurs simples, une commutation de contexte se contente de sauvegarder et restaurer les registres. Mais sur les processeurs modernes, ce n'est pas le cas. Une commutation de contexte doit faire d'autres opérations absolument nécessaires. Par exemple, elle doit aussi remettre à zéro les caches, réinitialiser certaines unité matérielles qu'on n'a pas encore abordées (la TLB), et bien d'autres. Pour le moment, nous ne pouvons pas expliquer pourquoi dans le détail, car cela demande de connaitre la mémoire virtuelle, qui est l'objet du chapitre suivant. Mais nous pouvons dire que c'est une question de protection mémoire. L'idée est que si un programme charge une donnée dans le cache, les autres programmes ne doivent pas y avoir accès, même par erreur.

Pour simplifier ces opérations supplémentaires, le processeur numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé les identifiants de processus CPU. 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 aussi utilisé lors des accès mémoire. Par exemple, il peut être utilisé pour les accès au cache. Les données dans le cache sont concaténées avec l'identifiant du processus qui les chargé dans le cache, 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.


Dans ce chapitre, on va parler de l'endianess du processeur et de son alignement mémoire. Concrètement, on va s'intéresser à la façon dont le processeur repartit 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. Pour simplifier, ils sont à prendre en compte quand on échange des données entre registres et mémoire RAM.

La différence entre mots et bytes

[modifier | modifier le wikicode]

Avant toute chose, nous allons reparler rapidement de la différence entre un byte et un mot. Les deux termes sont généralement polysémiques, avec plusieurs sens. Aussi, définir ce qu'est un mot est assez compliqué. Voyons les différents sens de ce terme, chacun étant utile dans un contexte particulier. La distinction entre un octet, un byte et un mot, se fait au niveau du processeur. Précisément, elle intervient au niveau des instructions d'accès mémoire, éventuellement de certaines opérations de traitement de données. Dans ce qui va suivre, nous allons faire la différence entre les architectures à mot, à byte, et à chaines de caractères.

Les mots et bytes : entiers et caractères

[modifier | modifier le wikicode]

Vous savez sans doute qu'il existe des processeurs 8, 16, 32 ou 64 bits, ce nombre indiquant le nombre de bits utilisés pour coder un nombre entier. Et bien un mot est une donnée de taille fixe, qui a la même taille. En clair, un mot est toute donnée qui a la même taille qu'un entier : ça peut être un entier, une adresse mémoire, des données bit à bit, ou toute autre donnée. Précisons qu'il s'agit de la taille maximale, précision qui est importante car certains processeurs gèrent des entiers de taille distinctes. Par exemple, un processeur 32 bits peut avoir des instructions de calcul 16 ou 8 bit. Dans ce cas, c'est la taille maximale qui compte.

La taille des nombres entiers se répercute sur beaucoup d'aspects du jeu d'instruction, notamment la taille des registres. Un processeur 32 bits utilise des entiers codés sur 32 bits, et il a donc des registres entiers de 32 bits. Précisons qu'il s'agit là uniquement des registres entiers, les registres flottants peuvent être plus larges.

Un byte est une unité plus petite que le mot, typiquement un octet, initialement utilisée pour faciliter l'encodage du texte. Un byte correspondait dans le temps à un caractère de texte, codé en ASCII ou un autre codage similaire. La norme actuelle est de 8 bits par byte, mais ça n'a pas toujours été le cas. Elle s'est instaurée au début des années 1970, quand les processeurs ont commencés à s'adapter au traitement du texte. Avant, les processeurs utilisaient des bytes de 6, 7, 9 ou 10 bits.

Pour bien faire les choses, un mot contient un nombre entier de bytes, pas un bit de plus ou de moins. Le nombre d'octets dans un mot est généralement une puissance de deux pour simplifier les calculs. Cette règle souffre évidemment d'exceptions, qui posent quelques problèmes techniques en termes d’adressage, comme on le verra plus bas.

Sur les machines modernes, chaque byte a une adresse, ce qui fait que le processeur peut lire un byte indépendamment des autres. Le byte est souvent décrit comme la plus petite unité de mémoire que le processeur peut adresser. Mais quelques architectures très rares ne respectent pas cette règle, en permettant d'adresser carrément chaque bit de la mémoire indépendamment des autres.

Voyons rapidement la distinction entre mot et byte sur les processeurs 8, 16, 32 bits et plus. La distinction entre mot et byte est la plus simple sur les processeurs 16 bits : le mot fait 16 bits, le byte en fait 8, un mot est donc composé de deux bytes. Les processeurs 8 bits récents ne gèrent que des données de 8 bits, ce qui fait que le mot et le byte sont confondus. Le processeur manipule des mots qui sont de la même taille que le byte. Il a existé quelques processeurs capables de manipuler des données de 4 bits, notamment pour supporter le BCD, mais cela ne concernait que les instructions. Les registres et la mémoire utilisaient des bytes d'un octet. Sur les processeurs 32 bits, le byte fait un octet, le mot en fait 4, le processeur gère souvent des données de 16 bits intermédiaires.

Le lien avec le bus mémoire

[modifier | modifier le wikicode]

Le terme "mot" vous a sans doute rappelé les chapitres sur les mémoires RAM/ROM, quand nous avons parlé des mots mémoire. Pour rappel, une mémoire RAM/ROM est découpée en mots mémoire, des groupes de bits qui sont transmis en une seule fois sur le bus mémoire. Par exemple, les mémoires modernes permettent de transmettre 64 bits en une seule fois sur le bus mémoire. La mémoire RAM est donc composée de groupes de 64 bits, chacun ayant une adresse mémoire, chaque groupe étant appelé une case mémoire ou encore un mot mémoire. Pour le dire autrement, il n'y a pas un octet par adresse, mais un mot mémoire dont la taille est de un ou plusieurs octets.

En théorie, les notions de mot et de mot mémoire sont distinctes. La notion de mot mémoire est une définition basée sur les transferts entre processeur et mémoire RAM/ROM, alors que la notion de mot processeur est liée au jeu d'instruction et aux registres. La distinction est importante car il est possible qu'un processeur utilise des mots plus grands que le bus mémoire. par exemple, un processeur 16 bits peut utiliser un bus mémoire 8 bits, bien que cela se fasse au détriment des performances (il faut charger les opérandes en deux fois, un octet après l'autre). Il faut donc distinguer le mot processeur, décrit dans la section précédente et contrasté au byte, du mot mémoire.

Il y a alors deux possibilités principales : le bus mémoire a la même taille qu'un byte, le bus a la même taille qu'un mot. En pratique, c'est la seconde solution qui est la plus utilisée, à savoir que le bus mémoire a la même taille qu'un mot processeur. Par exemple, les processeurs 16 bits gèrent des entiers codés sur 16 bits, ont des registres de 16 bits pour les entiers et adresses, et sont reliés à un bus mémoire de 16 bits. Le fait que le bus mémoire a la même taille qu'un mot a de nombreuses conséquences qu'on étudiera plus bas.

Il a existé des systèmes où le bus mémoire avait une taille égale à la moitié d'un mot processeur, mais passons.

Sur les systèmes modernes, la présence de mémoires caches fait que le bus mémoire n'a pas de rapport direct avec la taille d'un mot. Le bus mémoire sert pour les transferts entre le cache et la RAM, pqui n'impliquent pas le processeur et les registres. Par contre, le taille du mot est importante pour les transferts entre cache et registres. Le bus qui relie le cache au processeur a typiquement une largeur égale à un mot mémoire.

L'adressage par mot et par byte

[modifier | modifier le wikicode]

Pour résumer, un mot correspond à une donnée de même taille qu'un registre, ou du moins qu'un opérande/résultat entier. Un byte est une subdivision d'un mot, censé stocker un caractère, un mot contenant plusieurs bytes. La différence entre mot et byte est assez peu intuitive, mais elle va devenir claire avec ce qui suit. Nous allons aborder l'adressage par mot et l'adressage par byte. Le premier était utilisé sur d'anciens ordinateurs, dans les années 50 et 60, mais il permet d'introduire l'adressage par byte utilisé sur les ordinateurs modernes.

Les architectures à adressage par mot pures

[modifier | modifier le wikicode]

Au tout début de l'informatique, avant les années 80, les ordinateurs ne géraient que des nombres entiers ou flottants, qui faisaient tous la même taille. La taille d'un mot, d'un nombre entier, était de 3, 4, 5, 6 7, 13, 17, 23, 36 ou 48 bits. La taille la plus courante était 36 bits, suivie par 48 bits, le reste étant assez anecdotique. Pour donner quelques exemples, l'ordinateur ERA 1103 utilisait des opérandes/résultats de 36-bits, idem sur le PDP-10. Les processeurs avaient des registres de la taille d'un mot, à savoir 36/48 bits, et le bus mémoire faisait la même taille.

Un point très important est que les instructions de lecture/écriture lisaient/écrivaient des mots entiers, elles ne géraient pas de tailles autres. Par exemple, sur les processeurs 36 bits, les instructions mémoire lisaient ou écrivaient 36 bits, ni plus, ni moins. Pas de lecture/écriture sur 18 ou 12 bits, ni sur 8 ou 16 bits, par exemple. Le PDP-10, qui était un processeur 36 bits, ne gérait pas d'autre taille pour les données : c'était 36 bits pour tout le monde. De telles architectures n'utilisaient pas encore des octets, ni même de bytes, qui se sont démocratisés après. La raison à cela est qu'elles étaient conçues pour faire des calculs entiers/flottants, mais pas pour gérer du texte ou l'encodage BCD. N'utiliser que des mots suffisait, car un mot correspondait à un nombre entier, seuls opérandes acceptées par le processeur.

De plus, les ordinateurs de l'époque faisaient en sorte que la mémoire ait des cases mémoire de la même taille qu'un mot. Non seulement le bus mémoire avait la même taille, mais en plus, les adresses mémoire adressaient des mots entiers, pas des octets ni des bytes, ni quoique ce soit d'autre. Par exemple, le PDP-10 gérait des mots de 36 bits, ce qui fait que ses registres faisaient 36 bits, mais aussi qu'une adresse adressait un bloc de 36 bits. En clair, une adresse mémoire adressait un mot complet de 36 bits, pas un octet. De tels ordinateurs étaient appelés des architectures à adressage par mot. La mémoire était donc découpée en mots, chacun avait sa propre adresse, et le processeur échangeait des mots entre registres et mémoire RAM/ROM. Il n'y avait qu'une seule unité d'adressage, ce qui fait que les bytes n'existaient pas encore. La distinction entre byte et mot est apparue après, sur des ordinateurs/processeurs différents.

Par la suite, des processeurs ont permis d'adresser des données plus petites qu'un mot. L'intérêt était de faciliter la gestion du texte, qui est devenue importante quand les ordinateurs ont commencés à être utilisés en entreprise, puis dans les foyers. Les premiers mainframes étaient spécialisés dans le calcul scientifique ou dans les applications purement calculatoires, ils n'avaient pas, à gérer de texte, ce qui fait que l'adressage par mot était simple : il y avait une adresse par nombre entier, une case mémoire faisait la même taille qu'un registre, etc. Mais pour gérer du texte, c'est autre chose, car les caractères sont souvent encodés sur 6, 7, 8 bits. L'idée est alors de découper les mots en bytes de 6/7/8/9/10 bits, chacun correspondant à un caractère.

Les architectures à adressage par mot hybrides

[modifier | modifier le wikicode]

Pour gérer des bytes, les architectures à adressage par mot se sont adaptées. L'idée est que le processeur ajoutait des instructions pour sélectionner un byte dans le mot. Une instruction d'accès mémoire devait alors préciser deux choses : l'adresse du mot à lire/écrire, et la position du byte dans le mot adressé. Par exemple, on pouvait demander à lire le mot à l'adresse 0x5F, et de récupérer uniquement le byte numéro 6. Il s'agit d'architectures adressables par mot car une adresse identifie un mot, pas un byte.

La sélection des bytes se faisait avec des instructions spécialisées : le processeur lisait des mots entiers, avant que le hardware du processeur sélectionne automatiquement le byte voulu. Un exemple est le PDP-6 et le PDP-10, qui avaient des instructions de lecture/écriture de ce type. Elles prenaient trois informations : l'adresse d'un mot, la position du byte dans le mot, et enfin la taille d'un byte ! L'adressage était donc très flexible, car on pouvait configurer la taille du byte. Outre l'instruction de lecture LDB et celle d'écriture DPB, d'autres instructions permettaient de manipuler des bytes. L'instruction IBP incrémentait le numéro du pseudo-byte, par exemple.

Mais de telles instructions avaient un problème : il fallait calculer l'adresse du mot mémoire et la position du byte dans le mot. Rien de compliqué en soit, mais cela demande une division et une opération modulo, ce qui n'est pas très pratique. En général, le texte est stocké dans un tableau de caractères, qui est parcouru du premier caractère vers le dernier. Gérer du texte demandait donc de gérer adresse et position : on devait incrémenter la position pour passer au caractère suivant, puis incrémenter l'adresse tous les 2/3/4/5/6 caractères. Pour éviter cela, les processeurs ont inventé l'adressage par byte.

Les architectures à adressage par byte

[modifier | modifier le wikicode]

L'idée derrière l'adressage par byte est très simple : pour le processeur, chaque byte a sa propre adresse. La mémoire est vue par le processeur comme un regroupement de bytes, chacun numérotés avec une adresse. Concrètement, sur les processeurs modernes, chaque octet de la mémoire a sa propre adresse, peu importe la taille du mot processeur. Pour résumer, la distinction entre byte et mot est la suivante : le byte est utilisé pour adresser des caractères ou des octets, le mot pour adresser des entiers/flottants. Souvent, le byte est la plus petite donnée adressable et le mot est la plus grande, mais ce n'est pas systématique, quelques architectures ne respectent pas cette règle.

Les processeurs à adressage par byte ont souvent plusieurs instructions de lecture/écriture, chacune pour une taille précise. Il y a au minimum une instruction de lecture qui lit un byte en mémoire, une autre qui lit un mot complet, éventuellement des instructions pour les tailles intermédiaires. Par exemple, un processeur 64 bit a des instructions pour lire 8 bits, une autre pour lire 16 bits, une autre pour en lire 32, et enfin une pour lire 64 bits. Idem pour les instructions d'écriture, et les autres instructions d'accès mémoire. Dans ce cas, le byte vaut 8 bits, le mot en fait 64, les tailles intermédiaires n'ont pas de nom.

Une autre possibilité est celle d'une instruction de lecture unique, qu'on peut configurer pour lire/écrire un octet, deux, quatre, huit. Reprenons l'exemple de l'instruction LOAD qui lit une donnée en mémoire. Pour lire un byte isolé, il suffit de préciser l'adresse du byte, et qu'il faut lire un seul byte. Pour lire 16 ou 32 bits, l'instruction LOAD prend l'adresse du premier byte des 16/32 bits, et lit les 16/32 bits à partir de ce byte. En clair, seuls les bytes ont une adresse du point de vue du processeur. Je dis du point de vue du processeur, car nous verrons que c'est plus compliqué pour ce qui est de la mémoire RAM/ROM.

Ainsi, le processeur peut adresser un caractère directement, sans devoir calculer une adresse et une position : l'adresse du caractère suffit. L'avantage de l'adressage par byte est que l'on peut plus facilement modifier des données de petite taille. Par exemple, imaginons qu'un programmeur manipule du texte, avec des caractères codés sur un octet. S'il veut remplacer les lettres majuscules par des minuscules, il doit changer chaque lettre l'une après l'autre. Avec un adressage par mot, il doit lire un mot entier, modifier chaque octet en utilisant des opérations de masquage, puis écrire le mot final. Avec un adressage par byte, il peut lire chaque byte indépendamment, le modifier sans recourir à des opérations de masquage, puis écrire le résultat.

Un désavantage de l'adressage par byte est que l'on adresse moins de mémoire pour un nombre d'adresses égal. Si on a un processeur qui gère des adresses de 16 bits, on peut adresser 2^16 = 65 536 adresses. Avec l'adressage par byte, on ne pourra adresser que 65 536 bytes/octets, peu importe la taille du mot mémoire. Avec l'adressage par mot, on pourra adresser 65 536 mots de plusieurs octets chacun. Par exemple, avec un mot mémoire de 4 octets, on pourra adresser 65 536 × 4 octets. L'adressage par mot permet donc d'adresser plus de mémoire avec les mêmes adresses. C'est d'ailleurs pour cette raison que certains processeurs 16 bits spécialisés dans le traitement de signal (des DSP 16 bits) utilisent l'adressage par mot : pour adresser plus de mémoire avec des adresses codées sur 16 bits.

Adressage par mot et par Byte.

Les architectures à adressage par byte ont d'autres défauts. Le fait qu'un mot contienne plusieurs octets/bytes a de nombreuses conséquences, desquelles naissent les contraintes d'alignement, de boutisme et autres. Dans ce qui suit, nous allons étudier les défauts des architectures adressables par byte, et allons laisser de côté les architectures adressables par mot. La raison est que toutes les architectures modernes sont adressables par byte, les seules architectures adressables par mot étant de très vieux ordinateurs aujourd'hui disparus.

Les architectures à mot de taille variable

[modifier | modifier le wikicode]

D'anciens ordinateurs codaient leurs nombres sur un nombre variable de bytes, ce qui leur a valu le nom d'architectures à mots de taille variable. La grande majorité étaient des architectures décimales, à savoir des ordinateurs qui utilisaient des nombres encodés en BCD ou dans un encodage similaire. De tels processeurs encodaient des nombres sous la forme d'une suite de chiffres décimaux codés en BCD sur 4 bits, voire de 5/6 bits pour les ordinateurs qui ajoutaient un bit de parité/ECC par chiffre décimal. Les calculs se faisaient chiffre par chiffre, au rythme d'un chiffre utilisé comme opérande par cycle d'horloge. Le processeur passait automatiquement d'un chiffre au suivant pour chaque opérande.

Suivant l'ordinateur, la suite de chiffres était sois de même taille pour tous les nombres, soit avait une taille variable avec une taille maximale, soit n'avait tout simplement pas de taille maximale. Ce sont ces deux derniers cas qui intéressent ici.

Le premières architectures mot variable encodaient un entier BCD avec : une suite de chiffres BCD, précédée par un ou deux bytes pour encoder la longueur de la suite de chiffres. Les opérandes avaient ainsi une taille maximale, qui dépendait du codage de la longueur, du nombre de bits utilisés l'encoder. Par exemple, si on utilisait 8 bits pour encoder la longueur, les opérandes allaient de 0 à 255 chiffres BCD. Mais la plupart des ordinateurs ne gérait pas des tailles aussi importantes. Par exemple, de nombreux processeurs IBM géraient des opérandes allant jusqu'à 10 chiffres BCD maximum, pas plus.

D'autres architectures codaient les nombres par des chaines de caractères terminées par un byte de terminaison. La chaine de caractère contenait uniquement des chiffres, codés en BCD. Les bytes stockaient chacun un caractère, qui était utilisé pour encoder soit un chiffre décimal, soit un byte de terminaison. La taille d'un caractère était généralement de 5/6 bits, vu qu'il fallait au minimum coder les chiffres BCD et des symboles supplémentaires. Un exemple est celui des IBM 1400 series, qui utilisaient des chaines de caractères séparées par deux bytes : un byte de wordmark au début, et un byte de record mark à la fin. Les caractères étaient codés sur 6 bits. Chaque caractère/chiffre avait sa propre adresse, ce qui fait que l'architecture est techniquement adressable par byte, alors que les mots correspondaient aux nombres de taille variable.

L'adressage à la granularité d'un bit

[modifier | modifier le wikicode]

Nous venons de voir que l'adressage par byte est utile pour modifier des caractères sans avoir à modifier un mot complet. Sans adressage par byte, modifier un caractère demande de lire un mot entier, de modifier le caractère, puis d'enregistrer le mot final. Maintenant, rappelons-nous les chapitres sur les opérations logiques. Nous avions vu qu'il est fréquent de faire la même chose, mais avec des bits : modifier certains bits d'un nombre, sans modifier les autres. Il arrive occasionnellement que les programmeurs utilisent des bitfields, à savoir qu'ils mémorisent plusieurs informations dans un seul entier. Un exemple classique est le stockage des dates : on stocke le jour, le mois et l'année dans un seul entier, dont quelques bits encodent la journée, d'autres le mois, d'autres l'année. De telle situations demandent de modifier certains bits d'un bitfield, mais pas les autres.

Un autre exemple est celui de la configuration des périphériques. Nous en avions déjà parlé rapidement dans le chapitre "l'architecture de base", mais configurer des périphériques demande de modifier la valeur de registres de configuration, qui contiennent justement des bitfields. Par exemple, pour configurer une carte graphique, il y a un registre pour configurer la résolution et la fréquence d'affichage, ces trois nombres étant concaténés et stockés dans un registre. De plus, la plupart des registres de configuration disposent de bits qui permettent d'activer ou de désactiver des fonctionnalités. Activer ou désactiver une fonctionnalité demande alors de modifier un seul bit dans le registre de configuration adéquat.

Les registres de configuration des périphérique sont adressés avec une adresse mémoire. Et cela peu importe qu'ils soit mappés en mémoire RAM, ou dans un espace d'adressage séparés : il y a toujours une adresse mémoire bien précise qui permet d'adresser un registre. Vous l'avez sans doute vu venir, mais modifier un seul bit demande de charger un mot/byte complet, de modifier le bit dans les registres, puis d'enregistrer le byte/mot final. Encore une fois, on doit effectuer un accès en lecture-modification-écriture (Read-Modify-Write). Et cela pose de nombreux problèmes techniques dont on ne pas vraiment parler pour le moment, notamment des problèmes d'atomicité sur les architectures multicœurs. Et au-delà de ces problèmes techniques, ce n'est pas pratique pour les programmeurs ou les compilateurs.

Heureusement, à problème identique, solution identique. Une première solution a été d'ajouter des instructions processeurs capables d'effectuer l'accès en lecture-modification-écriture automatiquement. Les instructions en question sont des instructions Clear Bit, Set Bit, Invert Bit, et quelques autres. Elles ne se contentent pas de faire une opération logique, mais font aussi la lecture et l'écriture du résultat en mémoire RAM. L'instruction prend comme opérandes deux informations : l'adresse de l'octet qui contient le bit à modifier, la position de ce bit dans l'octet. Il faut évidemment les calculer, rien de compliqué, dans le programmeur doit faire cela à la main. Il s'agit d'une solution similaire à celle des architecture à adressage par mot hybrides, avec des instructions pour gérer des bytes.

Et enfin, une autre solution utilise l'adressage à la granularité d'un bit, que nous raccourcirons en adressage par bit. Ce terme compliqué cache un concept tout simple : il n'y a pas une adresse par byte, ni une adresse par mot processeur/mémoire, mais une adresse pour chaque bit de la mémoire ! S'il a existé des processeurs et des mémoires bit-adressables, il faut avouer qu'ils sont très rares. De tels processeurs gardent la capacité d'adresser un byte ou un mot. L'adresse d'un byte/mot étant l'adresse de son premier bit, de la même manière que l'adresse d'un mot est l'adresse de son premier byte avec l'adressage par byte. Sur ces architectures, le byte n'est pas la plus petite unité mémoire adressable.

Il est possible d'avoir un adressage par bit au niveau du processeur, alors que les registres de configurations ne permettent pas d'adresser un bit distinct. Dans ce cas, c'est soit le périphérique, soit le processeur, qui se chargent d'effectuer un accès en lecture-modification-écriture en sous-main, de manière automatique, sans que le programmeur ait quoique ce soit à faire.

Une des rares utilisation actuelle de l'adressage par bit est la technique dite de bit-banding. Elle est présente sur certains processeurs ARM, potentiellement d'autres. L'idée est que l'on peut manipuler les registres de configuration de deux manières : soit avec l'adressage par byte/mot, soit avec l'adressage par bit. Un registre de configuration a alors plusieurs adresses : une adresse globale pour le modifier entièrement en écrivant un byte/mot dedans, une adresse pour chacun de ses bit.

Les deux adresses peuvent en théorie être dans deux espaces d'adressage séparés, mais ce n'est pas la solution retenue sur les processeurs ARM, qui mettent ces adresses dans un seul espace d'adressage. Elle ne concerne qu'une petite partie de la mémoire RAM de l'ordinateur, à savoir deux blocs de 1 mébi-octet sur l'ARM Cortex M3. Le premier bloc est mappé dans l'intervalle allant des adresses 0x20000000h à 0x20100000h, ses bits sont adressés dans l'intervalle allant des adresses 0x22000000h à 0x23FFFFFFh. Le seconde intervalle est plus grand, car le premier utiliser une adresse par mot, le second a une adresse par bit.

L'implémentation matérielle de l'adressage par byte

[modifier | modifier le wikicode]

Implémenter l'adressage par byte demande de faire quelques modifications au niveau du processeur, mais aussi parfois du bus mémoire et de la mémoire RAM. Avant de poursuivre, rappelons que la notion de byte est avant tout liée au jeu d'instruction, mais qu'elle ne dit rien du bus mémoire ! Il est parfaitement possible d'utiliser un bus mémoire d'une taille différente de celle du byte ou du mot. La largeur du bus mémoire, la taille d'un mot, et la taille d'un byte, ne sont pas forcément corrélées. Néanmoins, deux implémentations principales sont possibles.

La première utilise une mémoire dont chaque case mémoire est un byte. Le bus mémoire a la même taille qu'un byte, ce qui fait que les lectures/écritures se font byte par byte, ce qui est très lent. Une autre solution utilise un bus mémoire de la même taille qu'un mot. Dans ce cas, le processeur lit/écrit des mots mémoire entiers, mais il peut sélectionner les bytes qui l'intéressent si besoin. Il peut donc lire/écrire un mot entier en une seule fois s'il manipule des entiers, mais ne garder que les bytes adéquats quand il travaille sur du texte.

Les architectures avec une mémoire adressable par byte

[modifier | modifier le wikicode]

La première implémentation a un bus mémoire de la taille d'un byte, qui transmet un byte à la fois. En clair, la largeur du bus mémoire est celle du byte. Le moindre accès mémoire se fait byte par byte, donc en plusieurs cycles d'horloge. Par exemple, sur un processeur 64 bits, la lecture d'un mot complet se fera octet par octet, ce qui demandera 8 cycles d'horloge, cycles d'horloge mémoire qui plus est. Par contre, lire ou écrire un byte se fera en un seul cycle d'horloge mémoire, ce qui n'est pas le cas avec l'autre implémentation. Cette implémentation a donc un désavantage en termes de performance quand on manipule des mots, mais un avantage quand on manipule des bytes. La performance dépend de plus de la taille des données lue/écrites. On prend moins de temps à lire une donnée courte qu'une donnée longue.

Un autre avantage est qu'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 à l'adresse 4, puis lire une autre donnée de 16 bits à l'adresse 5 sans aucun problème. En conséquence, il n'y a pas de contraintes d'alignements et les problèmes que nous allons aborder dans la suite n'existent pas.

Chargement d'une donnée sur un processeur sans contraintes d'alignement.

Les architectures avec une mémoire adressable par mot

[modifier | modifier le wikicode]

Avec la seconde implémentation, le bus mémoire a la largeur nécessaire pour lire un mot entier. Il y a alors confusion entre un mot au sens du jeu d'instruction, et un mot mémoire. Pour rappel, une donnée qui a la même taille que le bus de données est appelée un mot mémoire.

Le processeur peut lire/écrire un mot mémoire entier dans ses registres, en un seul accès mémoire, en un cycle d'horloge mémoire. Il y a donc un avantage en termes de performance quand on manipule des mots. Pour la lecture/écriture de données plus petites qu'un mot, tout dépend de si on fait une lecture ou une écriture. Lire un byte prend le même temps : il suffit de lire un mot entier et de ne sélectionner que le byte voulu. Et il en est de même pour les tailles intermédiaires entre mot et byte : le processeur charge un mot complet, puis sélectionne les octets adéquats, grâce un multiplexeur.

La gestion des écritures est par contre désavantagée. Le problème est que les écritures se font mot par mot, pas byte par byte. Ne modifier qu'un seul byte demande de lire le mot qui contient le byte à modifier, modifier le byte, et enfin écrire le résultat. Et le même problème a lieu pour les tailles intermédiaires. Par exemple, pour un processeur 32 bits, écrire 16 bits demande de lire un mot de 32 bits, ne modifier que les 126 bits adéquats dedans, et écrire le résultat dans le mot mémoire final. Le tout demande des circuits de masquage et de décalage assez complexe.

Exemple du chargement d'un octet dans un registre de trois octets.

Un problème avec cette implémentation est que l'adressage de la mémoire et du CPU ne sont pas compatibles : le processeur utilise une adresse par byte, la mémoire une adresse par mot mémoire ! Il faut donc distinguer les adresses gérées par la mémoire et celles gérées par le processeur. Le processeur génère des adresses d'octet, qui permettent de sélectionner un octet bien précis (remplacez octet par byte pour plus de généralité). L'octet en question est dans une case mémoire bien précise, qui a elle-même une adresse mémoire. Lors d'un accès mémoire, l'adresse d'octet est convertie en une adresse mémoire, la case mémoire entière est lue, puis le processeur ne récupère que les données adéquates. Pour cela, des circuits d'alignement mémoire se chargent de faire la conversion entre adresses du processeur et adresse mémoire, nous allons détailler comment ils fonctionnent dans ce qui suit.

Chargement d'une donnée sur un processeur avec contraintes d'alignement.

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 demande de déterminer la position de l'octet dans le mot à partir de l'adresse de l’octet.

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. Si un mot contient N bytes, alors l'adresse du mot se calcule en divisant l'adresse du byte par N. La position du byte dans le mot est quant à elle le reste de cette division. 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. Et c'est cette position qui est utilisée pour configurer les multiplexeurs, quand on accède à un byte isolé.

Adresse d'un mot avec alignement mémoire strict.

Le processeur peut donc adresser la mémoire RAM en traduisant les adresses des octets en adresses de mot. Il lui suffit de faire une division pour cela. Il conserve aussi le reste de la division dans un registre pour sélectionner l'octet une fois la lecture terminée. Un accès mémoire se fait donc comme suit : il reçoit l'adresse à lire, il calcule l'adresse du mot, effectue la lecture, reçoit le mot à lire, et utilise le reste pour sélectionner l'octet final si besoin. La dernière étape est facultative et n'est présente que si on lit une donnée plus petite qu'un mot.

La division est une opération assez complexe, mais il y a moyen de ruser. L'idée est de faire en sorte que N soit une puissance de deux. La division se traduit alors par un vulgaire décalage vers la droite, le calcul du reste par une simple opération de masquage. C'est la raison pour laquelle les processeurs actuels utilisent des mots de 1, 2, 4, 8 octets. Sans cela, les accès mémoire seraient bien plus lents. De plus, 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.

L'alignement mémoire

[modifier | modifier le wikicode]

Dans la section précédente, nous avons évoqué le cas où un processeur à adressage par byte est couplé à une mémoire adressable par mot. Sur de telles architectures, des problèmes surviennent quand les lectures/écritures se font par mots entiers. Le processeur fournit l'adresse d'un byte, mais lit un mot entier à partir de ce byte. Par exemple, prenons une lecture d'un mot complet : celle-ci précise l'adresse d'un byte. Sur un CPU 64 bits, le processeur lit alors 64 bits d'un coup à partir de l'adresse du byte. Et cela peut poser quelques problèmes, dont la résolution demande de respecter des restrictions sur la place de chaque mot en mémoire, restrictions résumées sous le nom d'alignement mémoire.

L'exemple sur les processeurs 16 bits

[modifier | modifier le wikicode]

Pour faire comprendre ce qu'est l'alignement mémoire, nous allons prendre l'exemple d'un processeur 16 bits connecté à une mémoire de 16 bits, via un bus mémoire de 16 bits. Le processeur utilisant l'adressage par byte, chaque octet de la mémoire a sa propre adresse. Par contre, le processeur lit et écrit des paquets de 16 bits, soit deux octets.

Pour rappel, un groupe de deux octets est appelé un doublet.

Pour la mémoire, les octets sont regroupés en groupes de deux, en doublets mémoire. Pour la mémoire, un doublet mémoire a une adresse unique. Et cela ne colle pas avec l'adressage par byte utilisé par le processeur, il y a une différence entre les adresses du processeur et celles de la mémoire. Il y a une adresse par doublet pour la mémoire, une adresse par octet pour le processeur. L'adresse mémoire a donc un bit de moins que l'adresse processeur. Pour faire la distinction, nous utiliserons les termes : adresse mémoire et adresse processeur.

Lorsque le processeur lit un doublet, il lit le premier octet à une adresse processeur et l'octet suivant dans l'adresse processeur suivante. Les adresses processeur sont donc regroupées par groupes de deux : l'adresse 0 et 1 adressent toutes deux le premier doublet, l'adresse 2 et 3 adressent le second doublet, etc. Un doublet est donc identifié par deux adresses processeur : une adresse paire et une adresse impaire. Nous allons partir du principe que l'octet de poids faible est dans l'adresse paire, l'octet de poids fort dans l'adresse impaire. Les règles de boutisme autorise de faire l'inverse, mais ce n'est pas le choix le plus intuitif.

L'alignement mémoire dit quoi faire lorsque le processeur veut lire/écrire 16 bits à une adresse impaire. S'il veut lire 16 bits à une adresse impaire, les deux octets seront dans des doublets mémoire différents. Le premier sera l'octet de poids fort d'un doublet, l'autre sera l'octet de poids faible du doublet suivant. Le doublet que le processeur veut lire/écrire est à cheval sur deux doublets mémoire. Et le processeur ne gère pas cette situation naturellement.

Alignement mémoire sur 16 bits

Pour résoudre ce problème, il y a deux solutions : imposer l'alignement mémoire, supporter les accès non-alignés.

L'alignement mémoire strict n'autorise que les accès mémoire à des adresses mémoires paires. Tout accès à une adresse mémoire impaire lève une exception matérielle, signe que c'est une erreur matérielle. Par contre, les adresses paires sont autorisées car une adresse paire identifie un doublet mémoire, que le processeur peut lire/écrire en une seule fois, à travers le bus mémoire. Le processeur gère donc uniquement des lectures/écritures de doublet, au sens de doublet mémoire.

Sans alignement mémoire, on peut lire/écrire 16 bits à partir d'une adresse paire comme impaire, les deux sont autorisées. Pour cela, le processeur doit gérer les lectures/écritures de 16 bits à des adresses impaires. Vu que les 16 bits demandés sont à cheval sur deux doublets mémoire, le processeur doit lire les deux doublets mémoire, sélectionner les octets adéquats, et les concaténer pour obtenir les 16 bits finaux.

Pour ce qui est de lire/écrire des octets, l'alignement mémoire ne pose aucune contrainte, vu qu'un octet n'est jamais à cheval sur un doublet,quadruplet ou autre. Le processeur peut demander à lire ou écrire un seul octet, mais la mémoire fournira deux octets et le processeur devra n'en conserver qu'un. Donc, il faut prévoir un système pour sélectionner l'octet demandé dans un doublet.

Pour la lecture d'un octet à une adresse paire, le processeur lit un doublet et masque l'octet de poids fort, chargé en trop. Pour une lecture d'un octet à une adresse impaire, le processeur lit un doublet, déplace l'octet de poids fort dans l'octet de poids faible, et masque l'octet inutile. Dans tous les cas, le processeur ne lit/écrit qu'un doublet à la fois et sélectionne les octets demandés. Les écritures sont plus complexes, car elles demandent de lire un doublet, de modifier l'octet adéquat, et de réécrire le doublet final en RAM.

Pour les lectures, le tout est réalisé par un circuit intégré au processeur, qui est directement relié au bus mémoire. Il prend en entrée deux bits : le premier indique s'il faut faire une lecture sur 8 ou 16 bits, le second qui indique s'il faut lire l'octet de poids faible ou fort. Le circuit est alors composé d'un circuit de masquage et d'un multiplexeur. Le multiplexeur est utilisé pour choisir l'octet de poids faible ou fort. Le circuit de masquage met à zéro l'octet de poids fort en cas d'accès sur 8 bits.

Circuit d'accès 16-8 bits

Une autre possibilité est celle utilisée sur les anciens processeurs Intel 16 bits, qui utilisaient deux bits. Le premier est le bit de poids fort de l'adresse processeur, qui indiquait si l'adresse est paire ou non. Le second bit, nommé BHE, indique s'il fallait masquer l'octet de poids fort ou non.

Adresse paire Adresse impaire
BHE = 0 Lecture 16 bits Lecture octet de poids fort
BHE = 1 Lecture octet de poids faible Pas de lecture

L'alignement mémoire des données

[modifier | modifier le wikicode]

L'exemple précédent nous a montré ce qu'il en était sur les processeurs 16 bits. Mais pour généraliser le concept, nous allons voir le cas des processeurs 32 bits et plus. Nous venons de voir que l'alignement mémoire sur 16 bits impose des contraintes quant à l'adressage de la mémoire, en empêchant de lire des données qui sont à cheval sur deux doublets. Sur 32 bits, c'est la même chose, mais avec des quadruplets (des groupes de 4 octets, soit 32 bits) : impossible de lire des données si elles sont à cheval sur deux quadruplets mémoire. Sur 64 bits, c'est la même chose, mais avec des octuplets de 8 octets/ 64 bits : impossible de lire des données si elles sont à cheval sur deux octuplets mémoire.

La différence, c'est que la situation peut se présenter même si on ne lit pas 32/64 bits. Imaginons le cas particulier suivant : 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

La lecture ou écriture du caractère ne pose pas de problème, vu qu'il ne fait qu'un seul byte. Pour la donnée de 2 octets, c'est la même chose, car elle tient toute entière dans un mot mémoire. La lire demande de lire le mot et de masquer les octets inutiles. Mais pour l'entier, ça ne marche pas car il est à cheval sur deux mots ! On dit que l'entier n'est pas aligné en mémoire. En conséquence, impossible de le charger en une seule fois

Pour résumer, avec un bus mémoire de 32 bits, le problème peut survenir si le processeur veut lire 32 bits, mais aussi 16 bits. Si on veut lire 16 bits, mais que le premier octet est dans un quadruplet, et l'autre octet dans un autre quadruplet, l'alignement mémoire intervient. Idem si on veut lire 4 octets, mais que les 3 premiers sont dans un quadruplet, pas le dernier. Tout cela est plus facile à comprendre avec un exemple.

La situation est gérée différemment 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. On dit que le processeur gère des accès mémoire non-alignés. D'autres processeurs ne gérent pas ce genre d'accès mémoire et les traitent comme une erreur, similaire à une division par zéro, et lève une exception matérielle. Si on est chanceux, la routine d'exception charge la donnée en deux fois. 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, de la mémoire est gâchée inutilement. Et 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).

L'alignement mémoire se gère dans certains langages (comme le C, le C++ ou l'ADA), en gérant l'ordre de déclaration des 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 des données en regardant la norme de vos langages.

L'alignement des instructions en mémoire

[modifier | modifier le wikicode]

Le processeur peut incorporer des contraintes sur l'alignement des instructions, au même titre que les contraintes d'alignement sur les données vues précédemment. Concrètement, il vaut mieux que les instructions rentrent dans un mot mémoire. La raison est que si on veut lire une instruction en un seul cycle, il faut qu'une instruction rentre toute entière dans le bus mémoire, donc dans un mot mémoire. Les processeurs RISC se débrouillent donc pour que les instructions rentrent dans un mot mémoire, donc dans un mot processeur.

Les instructions sont donc alignées sur un mot mémoire, elles doivent respecter les mêmes contraintes d'alignement que les données. Elles peuvent être plus courtes ou plus longues qu'un mot, mais elles doivent commencer à la première adresse d'un mot mémoire. La conséquence est que les instructions sont placées à des adresses précises. Par exemple, prenons un ordinateur avec des mots mémoire de 8 octets. La première instruction prend les 8 premiers octets de la mémoire, la seconde prend les 8 octets suivants, etc. En faisant cela, l'adresse d'une instruction est toujours un multiple de 8. Et on peut généraliser pour toute instruction de taille fixe : si elle fait X octets, son adresse est un multiple de X.

Généralement, on prend X une puissance de deux pour simplifier beaucoup de choses. Notamment, cela permet de simplifier le program counter : quelques bits de poids faible deviennent inutiles. Par exemple, si on prend des instructions de 4 octets, les adresses des instructions sont des multiples de 4, donc les deux bits de poids faible de l'adresse sont toujours 00 et ne sont pas intégrés dans le program counter. Le program counter est alors plus court de deux bits. Idem avec des instructions de 8 octets qui font économiser 3 bits, ou avec des instructions de 16 octets qui font économiser 4 bits.

Les processeurs CISC, avec leurs instructions de longueur variable potentiellement lues en plusieurs fois, ne sont pas concernés. Les instructions de taille variable ne sont généralement pas alignées. Sur certains processeurs, les instructions n'ont pas de contraintes d'alignement du tout. Leur chargement est donc plus compliqué et demande des méthodes précises qui seront vues dans le chapitre sur l'unité de chargement du processeur. Évidemment, le chargement d'instructions non-alignées est donc plus lent. En conséquence, même si le processeur supporte des instructions non-alignées, les compilateurs ont tendance à aligner les instructions comme les données, sur la taille d'un mot mémoire, afin de gagner en performance. D'autres architectures CISC ont des contraintes d'alignement bizarres, pour des raisons historiques. Par exemple, les premiers processeurs x86 16 bits imposaient des instructions alignées sur 16 bits et cette contrainte est restée sur les processeurs 32 bits.

Que ce soit pour des instructions de taille fixe ou variables, les circuits de chargement des instructions et les circuits d'accès mémoire ne sont pas les mêmes, ce qui fait que leurs contraintes d'alignement peuvent être différentes. On peut avoir quatre possibilités : des instructions non-alignées et des données alignées, l'inverse, les deux qui sont alignées, les deux qui ne sont pas alignées. Par exemple, il se peut qu'un processeur accepte des données non-alignées, mais ne gère pas des instructions non-alignées ! Le cas le plus simple, fréquent sur les architectures RISC, est d'avoir des instructions et données alignées de la même manière.

De plus, sur les processeurs où les deux sont alignés, on peut avoir un alignement différent pour les données et les instructions. Par exemple, pour un processeur qui utilise des instructions de 8 octets, mais des données de 4 octets. Les différences d'alignements posent une contrainte sur l'économie des bits sur le bus d'adresse. Il faut alors regarder ce qui se passe sur l'alignement des données. Par exemple, pour un processeur qui utilise des instructions de 8 octets, mais des données de 4 octets, on ne pourra économiser que deux bits, pour respecter l'alignement des données. Ou encore, sur un processeur avec des instructions alignées sur 8 octets, mais des données non-alignées, on ne pourra rien économiser.

Les architectures CISC utilisent souvent des contraintes d'alignement, avec généralement des instructions de taille variables non-alignées, mais des données alignées. Les deux dernières possibilités ne sont presque jamais utilisées. Un cas particulier est celui de l'Intel iAPX 432, dont les instructions étaient non-alignées au niveau des bits ! Leur taille variable faisait que la taille des instructions n'était pas un multiple d'octets. Il était possible d'avoir des instructions larges de 23 bits, d'autres de 41 bits, ou toute autre valeur non-divisible par 8. Un octet pouvait contenir des morceaux de deux instructions, à cheval sur l'octet. Ce comportement fort peu pratique faisait que l'implémentation de l'unité d"e chargement était complexe.

Le boutisme : une spécificité de l'adressage par byte

[modifier | modifier le wikicode]

Un autre problème lié à l'adressage par byte est lié au fait que l'on a plusieurs bytes par mot : dans quel ordre placer les bytes dans un mot ? On peut introduire le tout 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 bytes/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).

Dans ce qui suit, nous allons partir du principe que le byte fait un octet, mais gardez dans un coin de votre tête que ce n'a pas toujours été le cas. Les explications qui vont suivre restent valide peu importe la taille du byte.

Les différents types de boutisme

[modifier | modifier le wikicode]

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 | modifier le wikicode]

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, juste quelques avantages ou inconvénients 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, 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 sur les architectures qui gèrent des données de taille intermédiaires entre le byte et le mot. C'est le cas sur le x86, où l'on peut décider de lire des données de 8, 16, 32, ou 64 bits à partir d'une adresse mémoire. Avec le petit-boutisme, on s'assure qu'une 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. 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 en question sont les opérations où on doit additionner d'opérandes codées sur plusieurs octets; sur un processeur qui fait les calculs 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. En gros-boutisme, les choses sont beaucoup plus compliquées...

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.


La micro-architecture

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les composants d'un processeur Fonctionnement d'un ordinateur/Le chemin de données Fonctionnement d'un ordinateur/L'unité de chargement et le program counter Fonctionnement d'un ordinateur/L'unité de contrôle

Les jeux d’instructions spécialisés

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les architectures à accumulateur Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins Fonctionnement d'un ordinateur/Les architectures à pile et mémoire-mémoire 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 | modifier le wikicode]

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 | modifier le wikicode]

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.

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 | modifier le wikicode]

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 | modifier le wikicode]

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 | modifier le wikicode]

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-opérations, elle encodent directement les signaux de commandes à destination du chemin de données. De tels processeurs n'ont pas besoin d'un décodeur d'instruction pour traduire les instructions machines en signaux de commandes, elles se contentent d'une unité de chargement, du chemin de données 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 | modifier le wikicode]

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 | modifier le wikicode]

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. Ce qui 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 | modifier le wikicode]

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 le jeu d’instruction change 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. 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, 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 densité de code 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. Rien d'étonnant vu que les micro-instructions d'un processeur normal sont plus longues que les instructions machines. 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.

L'implémentation des processeurs actionnés par déplacement

[modifier | modifier le wikicode]

Sur les processeurs actionnés par déplacement, on n’a besoin que d'une seule instruction MOV, qui copie 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 | modifier le wikicode]

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 numéro 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 | modifier le wikicode]

Dans le second cas, on intercale des registres intermédiaires spécialisés en entrée et sortie de l'ALU, pour stocker les opérandes et le résultat d'une instruction. L’exécution d'une opération par l'unité de calcul est déclenchée automatiquement par une écriture dans certains registres d'opérande. 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 « opérande.1 », « opérande.2/déclenchement » et « résultat ». Le premier registre stocke le premier opérande de l'addition, le second stocke le second opérande, le troisième stocke le résultat de l'opération. Pour déclencher une opération, il faut écrire le second opérande dans le registre « opérande.2/déclenchement ». Une fois l'instruction terminée, le résultat de l'addition sera disponible dans le registre « ajout.résultat ».

Architecture déclenchée par déplacement - micro-architecture avec des registres pour l'ALU.


La mémoire virtuelle et la protection mémoire

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/L'espace d'adressage du processeur Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle

Les entrées-sorties et périphériques

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques Fonctionnement d'un ordinateur/L'adressage des périphériques Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension

Les mémoires de masse

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les mémoires de masse : généralités Fonctionnement d'un ordinateur/Les disques durs Fonctionnement d'un ordinateur/Les solid-state drives Fonctionnement d'un ordinateur/Les disques optiques Fonctionnement d'un ordinateur/Les technologies RAID

La mémoire cache

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les mémoires cache Fonctionnement d'un ordinateur/Le préchargement Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer

Le parallélisme d’instructions

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Le pipeline

Les branchements et le front-end

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/La prédiction de branchement Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions

L’exécution dans le désordre

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les pipelines multicycles Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions Fonctionnement d'un ordinateur/Le contournement (data forwarding) Fonctionnement d'un ordinateur/L'exécution dans le désordre Fonctionnement d'un ordinateur/Le renommage de registres Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans le désordre Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache

L'émission multiple

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les processeurs superscalaires Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86 Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC Fonctionnement d'un ordinateur/Les architectures dataflow

Les architectures parallèles

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les architectures parallèles Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading Fonctionnement d'un ordinateur/Les architectures à parallélisme de données Fonctionnement d'un ordinateur/La cohérence des caches Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire

Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation Fonctionnement d'un ordinateur/Le matériel réseau Fonctionnement d'un ordinateur/La tolérance aux pannes Fonctionnement d'un ordinateur/Les architectures systoliques Fonctionnement d'un ordinateur/Les réseaux de neurones matériels Fonctionnement d'un ordinateur/Les ordinateurs de première génération : tubes à vide et mémoires Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires Fonctionnement d'un ordinateur/Les circuits réversibles