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, chaque instruction effectuant une action dans l'ordinateur. Sur les ordinateurs modernes, la majorité de ces instructions effectuent une opération arithmétique, comme 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.

Une instruction est représentée dans un ordinateur par une série de nombres : 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]

Au tout début de l'informatique, on utilisait des cartes perforées en plastique, sur lesquelles on inscrivait le programme à exécuter, les données à traiter, bref : tout ce qu'on donnait à manger à l'ordinateur. 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'interrupteur 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 premières générations : des ordinateurs qui prenaient des pièces de bâtiment entières

[modifier | modifier le wikicode]

La première génération d'ordinateur est celle des ordinateurs datant d'avant l'invention du transistor. Ils étaient conçus avec des tubes à vide, des composants électroniques assez rudimentaires.

Les ordinateurs de première génération étaient très simples, ils ne géraient que quelques opérations simples : l'addition, la soustraction, pas grand chose de plus. Ils ne géraient pas la multiplication et encore moins la division, ils n'avaient pas assez de tubes à vides à disposition pour cela.

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.

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.

Transistors comme tubes à vide prenaient beaucoup de place, 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 finit 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èce 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é au milliers de transistors, puis au millions 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 était 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 ordinateur où le byte faisait exactement 1 bit ! Mais sur presque tous les ordinateurs modernes, un byte fait effectivement 8 bits, ce qui fait que le terme byte est parfois utilisé en lieu et place d'octet. Mais c'est un abus de langage, attention aux confusions ! Dans ce cours, nous parlerons d'octet pour désigner un groupe de 8 bits, en réservant le terme byte pour sa véritable signification.

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

Bit de poids fort.
Bit de poids faible.

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

La traduction binaire→décimal

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

Value of digits in the Binary numeral system

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 : "em 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. A 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 quelque 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 hybrides entre décimal et binaire

[modifier | modifier le wikicode]

Dans cette section, 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.

Le code 2 parmi 5

[modifier | modifier le wikicode]

Les codes 2 parmi 5 permettent d'encoder 10 chiffres décimaux sur 5 bits. Le code garantit que sur les 5 bits, deux sont à 1, les trois restants sont à 0, les bits à 0/1 n'étant pas les mêmes selon le chiffre encodé et le code utilisé. Il existe plusieurs codes 2 parmi 5, la plupart sont utilisés sur les codes barres dans les magasins, mais ce ne sont pas ceux utilisés dans les ordinateurs d'antan.

L'ordinateur IBM 7070 et ses déclinaisons utilisait un code 2 parmi 5 qui codait les 10 chiffres décimaux, ainsi que les signes + et -, et un caractère . Il gérait des entiers décimaux codés sur 10 chiffres plus un caractère +/- pour le signe. Les caractères pour le texte étaient codés en utilisant un code à deux chiffres décimaux, c’est-à-dire sur 10 bits.

0 1 2 3 4 5 6 7 8 9 A - +
01100 11000 10100 10010 01010 00110 10001 01001 00101 00011 1––10 1––01 0––11

Les codes bi-quinaires

[modifier | modifier le wikicode]

Les codes bi-quinaires codent des chiffres sur 7 bits. Les 7 bits sont découpés en deux sections : 2 bits pour la première, 5 pour l'autre. Les 5 bits permettent de compter de 0 à 4 ou de 5 à 9, un seul bit est à 1, les quatre autres sont à 0. Le bit mis à 1 indique la valeur encodée. Les deux bits restants déterminent si la valeur est inférieure ou supérieure/égale à 5, un seul des deux bits est à 1. Voici les deux encodages les plus courants :

Code bi-quinaire basique
Code bi-quinaire réfléchi.

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. Et il a été utilisé sur d'anciens ordinateurs, dont l'IBM 650, l'UNIVAC Solid State et le UNIVAC LARC.

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

Les encodages alternatifs

[modifier | modifier le wikicode]

Outre le binaire et le BCD que nous venons de voir, il existe d'autres manières de coder des nombres en binaires. Et nous allons les aborder dans cette section. Parmi celle-ci, nous parlerons du code Gray, 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.


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. Les mémoires les plus simples sont des registres, qui mémorisent un nombre. D'autres mémoires plus complexes mémorisent plusieurs nombres, un grand nombre. Et ces mémoires ne sont pas des dispositifs parfaits, elles peuvent subir des corruptions. 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.

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 .

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. UDans 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à. A 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.

La porte à transmission

[modifier | modifier le wikicode]

La porte à transmission est une porte logique assez particulière, qui mérite d'être vue dans ce chapitre. Pour simplifier, il s'agit d'un interrupteur commandable. Le circuit peut soit connecter l'entrée et la sortie, soit les déconnecter. Pour rappel, un interrupteur fermé laisse passer le courant, alors qu'un interrupteur fermé ne le laisse pas passer. Pour choisir entre les deux, une porte à transmission possède une entrée de commande sur laquelle on envoie un bit de commande, qui ouvre ou ferme l'interrupteur. La porte est typiquement 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
Porte à transmission, symbole DIN

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

Un défaut de ces portes logique est qu'elles sont électriquement équivalentes à des interrupteurs. Les autres portes logiques peuvent générer un 1 ou un 0 distinct de ce qu'il y a sur leur entrée. Et ce n'est pas un prodige, c'est juste que les portes logiques sont toutes reliées à la tension d'alimentation et à la masse (le 0 volt). Elles sont alimentées en électricité, pour fournir un 1 en sortie si l'entrée est à 0. Pas les portes à transmission, qui ne sont pas reliées à l'alimentation. Ce détail ne pose pas de problèmes tant qu'on n'enchaine pas de portes à transmission les unes à la suite des autres.


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 sur chacun de ses bits. 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 qui prend en entrée un nombre et met sa sortie à la valeur maximale si une condition est respectée. Pour le dire autrement, le circuit va soit recopier l'entrée telle quelle sur sa sortie, soit la mettre à 11111...111. Le choix entre les deux situations est réalisé par une entrée Set de 1 bit : un 1 sur cette entrée met la sortie à la valeur maximale, un 0 signifie que l'entrée est recopiée en sortie.

La porte OU est toute indiquée pour cela. La mise à 1 d'un bit d'entrée demande de faire un OU de celui-ci avec un 1, alors que recopier un bit d'entrée demande de faire un OU de celui-ci avec un 0.

Circuit de mise à 1111111...11

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

Le circuit de mise à zéro

[modifier | modifier le wikicode]

Le circuit de Reset prend entrée le bit d'entrée, puis un bit de commande qui indique s'il faut mettre à zéro le bit d'entrée ou non. Le bit de commande en question est appelé le bit Reset. Si le signal Reset est à 1, alors on met à zéro le bit d'entrée, mais on le laisse intact sinon.

Le tableau ci-dessus nous dit que la porte ET est 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]

Dans cette section, nous allons voir un inverseur commandable, un circuit qui, comme son nom l'indique, inverse les bits d'un nombre passé en entrée. Ce circuit inverse un nombre quand le bit de commande, souvent 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.

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

Multiplexeur fabriqué avec des portes à transmission

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

Il est possible de remplacer les portes ET par des portes à transmission. Remplacer les portes ET par des portes à transmission permet de 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.

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 ou plusieurs bits. Ces circuits sont ce qu'on appelle des bascules, ou latches. Pour une question de simplicité, ce chapitre parlera des circuits capables de mémoriser un bit seulement, pas plusieurs. Nous verrons comment combiner ces bits pour former une mémoire ou des compteurs dans le chapitre suivant.

L'interface d'une bascule

[modifier | modifier le wikicode]

Avant de voir comment sont fabriquées les bascules, nous allons voir quelles sont leurs entrées et leurs sorties. La raison à cela est que des bascules aux entrées-sorties similaires peuvent être construites sur des principes suffisamment différents pour qu'on les voie à part. Si on ne regarde que les entrées-sorties, on peut grosso-modo classer les bascules en quelques grands types principaux : les bascules RS, les bascules JK et les bascules D.

Avant toute chose, faisons quelques précisions. Sur les schéma qui vont suivre, les entrées sont à gauche, les sorties sont à droite. Vous verrez que toutes les bascules qui vont suivre disposent de deux sorties, nommées Q et . La sortie Q fournit le bit qui est mémorisé dans la bascule. C'est sur cette sortie qu'on peut lire le bit mémorisé, le récupérer. La sortie est très similaire, la seule différence étant qu'elle fournit 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. Si les bascules ont toutes les mêmes sorties, elles se distinguent selon les entrées.

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, on ne sait pas ce qui arrivera sur ses sorties. Après tout, quelle idée de mettre la bascule à un en même temps qu'on la met à zéro !
Entrée Reset Entrée Set Sortie Q
0 0 Bit mémorisé par la bascule
0 1 1
1 0 0
1 1 Dépend de la bascule

Le comportement obtenu quand on met deux 1 en entrée dépend de la bascule. 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 Interdit
0 1 0
1 0 1
1 1 Bit mémorisé par la bascule

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

Les bascules JK

[modifier | 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.
Multiplexeur à deux entrées - circuit
Multiplexeur fabriqué avec des portes à transmission

Pour rappel, un multiplexeur peut s'implémenter de différentes manières, comme nous l'avons vu dans le chapitre sur les circuits de sélection. Il est possible de l'implémenter en utilisant seulement des portes logiques, ou alors en utilisant des portes à transmission. Les deux possibilités sont illustrées ci-contre.

Pour un multiplexeur fabriqué sans porte à transmission, boucler sa sortie sur son entrée ne pose aucun problème particulier. Mais en utilisant 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 des portes logiques dans la boucle pour régénérer le signal électrique. La solution la plus simple et la plus évidente, est de rajouter une porte OUI, la porte logique qui recopie son entrée sur sa sortie. Et la manière la plus simple de fabriquer une porte OUI est d'utiliser 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 conceptuelle d'une bascule D

L'avantage du circuit précédent est qu'il est plus économe en portes logiques que les autres bascules, même si ce n'est pas évident. Son implémentation utilise moins de transistors, bien qu'on devra en reparler dans un chapitre ultérieur. 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.

Circuit de mise à zéro d'un bit

Certaines bascules D ont une entrée R, qui met à zéro le bit mémorisé dans la bascule quand l'entrée R est à 1. Pour cela, elles ajoutent un circuit de mise à zéro, que nous avons déjà vu dans le chapitre sur les opérations bit à bit. Ce circuit de mise à zéro est placé après la seconde porte NON, et sa sortie est bouclée sur l'entrée du circuit. Le circuit obtenu est le suivant :

Bascule D avec entrée Reset

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

Bascule D avec entrée Reset, simplifiée

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

[modifier | 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. Et bien sachez qu'il est possible de faire la même chose pour des bascules. On peut par exemple fabriquer une bascule RS à partir d'une bascule D, et réciproquement. Ou encore, on peut fabriquer une bascule D à partir d'une bascule JK, et inversement. Les possibilités sont nombreuses. Et pour cela, il suffit juste d'ajouter un circuit combinatoire qui traduit les entrées de la bascule voulue vers les entrées de la bascule utilisée.

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

[modifier | 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. A 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.

Negative-edge triggered master slave D flip-flop

On peut faire exactement la même chose avec deux bascules asynchrones RS l'une à la suite de l'autre.

Bascule R synchrone basée sur des bascules RS non-synchrones.
Bascule D cadencée par une horloge.

Une autre méthode associe trois bascules RS normales, les deux premières formant une couche d'entrée qui commande la troisième bascule. Ces deux bascules d'entrée vont en quelque sorte traiter le signal à envoyer à la troisième bascule. Quand le signal d'horloge est à 0, les deux bascules d'entrée fournissent un 1 sur leur sortie : la troisième bascule reste donc dans son état précédent, sans aucune modification. Quand l'horloge passe à 1 (front montant), seule une des deux bascules va fournir un 1 en sortie, l'autre voyant sa sortie passer à 0. La bascule en question dépend de la valeur de D : un 0 sur l'entrée D force l'entrée R de la troisième bascule, un 1 forçant l'entrée S. Dit autrement, le contenu de la troisième bascule est mis à jour. Quand l'entrée d'horloge passe à 1, les bascules se figent toutes dans leur état précédent. Ainsi, la troisième bascule reste commandée par les deux bascules précédentes, qui maintiennent son contenu (les entrées R et S restent à leur valeur obtenue lors du front montant).

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 qu'ils mettent à jour régulièrement. 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. A chaque instant, le compteur contient le nombre de voitures dans le parking. C'est là un exemple d'un compteur, mais les exemples où on a besoin d'un circuit pour compter quelque chose sont nombreux.

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

L'incrémentation ou décrémentation d'un compteur augmente ou diminue le compteur d'une quantité fixe, appelée le pas du compteur. Suivant la valeur du pas, on fait la différence entre les compteurs d'un côté et les décompteurs de l'autre. Comme leur nom l'indique, les compteurs comptent alors que les décompteurs décomptent. Les compteurs augmentent le contenu du compteur à chaque mise à jour, alors que les décompteurs le diminuent. Dit autrement, le pas d'un compteur est positif, alors que le pas d'un décompteur est négatif. Les compteurs-décompteurs peuvent faire les deux, suivant ce qu'on leur demande.

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

[modifier | modifier le wikicode]

Suivant le compteur, la représentation du nombre mémorisé change : certains utilisent le binaire traditionnel, d'autres le BCD, d'autre le code Gray, etc. Mais tous les compteurs que nous allons voir seront des compteurs/décompteurs binaires, à savoir que les nombres qu'ils utilisent sont codés sur bits. Au passage, le nombre de bits du compteur est appelé la taille du compteur, par analogie avec les registres.

Vu que la taille d'un compteur est limitée, il cesse de compter au-delà d'une valeur maximale. La plupart des compteurs comptent de 0 à , avec la taille du compteur. D'autres compteurs ne comptent pas jusque-là : leur limite est plus basse que . Par exemple, certains compteurs ne comptent que jusqu'à 10, 150, etc. Ils sont appelés des compteurs modulo. Prenons un compteur modulo 6, par exemple : il compte de 0 à 5, et est remis immédiatement à zéro quand il atteint 6. Il compte donc comme suit : 0, 1, 2, 3, 4, 5, 0, 1, 2, ...

Outre la valeur de la limite du compteur, il est aussi intéressant de se pencher sur ce qui se passe quand le compteur atteint cette limite. Certains restent bloqués sur cette valeur maximale tant qu'on ne les remet pas à zéro "manuellement" : ce sont des compteurs à saturation. D'autres recommencent à compter naturellement à partir de zéro : ce sont des compteurs modulaires.

La plupart des compteurs utilisent un pas constant, qui est fixé à la création du compteur, ce qui simplifie la conception du circuit. Par exemple, les compteurs incrémenteurs/décrémenteurs ont un pas fixe de 1, à savoir que le contenu de leur registre est incrémenté de 1 à chaque cycle d'horloge ou demande d'incrémentation. D'autres ont un pas variable, à savoir qu'il change à chaque incrémentation/décrémentation. Cela peut aussi servir à compter quelque chose qui varie de manière non-régulière.

Par exemple, imaginez un circuit qui compte combien de voitures sont rentrées sur une autoroute par un péage bien précis. Plusieurs voitures peuvent rentrer sur le péage durant la même minute, presque en même temps. Pour cela, il suffit de prendre un compteur à pas variable, qui est incrémenté du nombre de voiture rentrées sur l'autoroute lors de la dernière période de temps. Évidemment, de tels compteurs à pas variables ont une entrée supplémentaire sur laquelle on peut envoyer le pas du compteur.

L'interface d'un compteur/décompteur

[modifier | modifier le wikicode]
Compteur 4 Bits.

Les compteurs et décompteurs sont des circuits synchrones, à savoir cadencés par une horloge, qui dit aux bascules du registre quand s'actualiser. Ils ont donc une entrée d'horloge. De plus, certains compteurs ont une entrée Enable qui active/désactive le comptage. Le compteur s'incrémente/décrémente seulement si l'entrée Enable est à 1, mais ne fait rien si elle est à 0. 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.

Les compteurs les plus simples n'ont pas d'entrée Enable, mais les compteurs plus complexes en ont une. Dans les schémas qui vont suivre, s'il y a une entrée Enable, le signal d'horloge ne sera pas représenté et il est sous-entendu qu'il y a une entrée d'horloge.

Les compteurs ont souvent une entrée Reset qui permet de les remettre à zéro. Un compteur/décompteur peut parfois être initialisé avec la valeur de notre choix. Pour cela, ils possèdent une entrée d'initialisation sur laquelle on peut placer le nombre initial, couplée à l'entrée Reset.

Sur les compteurs/décompteurs, il y a une entrée qui décide s'il faut compter ou décompter. Typiquement, elle est à 1 s'il faut compter et 0 s'il faut décompter.

Compteur 4 Bits avec entrée Reset.
Compteur 4 Bits avec entrée pour décider s'il faut compter ou décompter.

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

[modifier | modifier le wikicode]

Un compteur/décompteur peut être vu comme une sorte de registre (ils peuvent stocker un nombre), mais qu'on aurait amélioré de manière à le rendre capable de compter/décompter. Tous les compteurs/décompteurs utilisent un registre pour mémoriser le nombre, ainsi que des circuits combinatoires pour calculer la prochaine valeur du compteur. Ce circuit combinatoire est le plus souvent, mais pas toujours, un circuit capable de réaliser des additions (compteur), des soustractions (décompteurs), voire les deux (compteur-décompteur). Plus rarement, il s'agit de circuits conçus sur mesure, dans le cas où le pas du compteur est fié une bonne fois pour toutes.

Fonctionnement d'un compteur (décompteur), schématique

Comme dit plus haut, certains compteurs ont une valeur maximale qui est plus faible que la valeur maximale du registre. Par exemple, on peut imaginer un compteur qui compte de 0 à 9; mais construit à partir d'un compteur de 4 bits qui peut donc compter de 0 à 15 ! Ces compteurs sont construits à partir d'un compteur modulo, auquel on rajoute un circuit combinatoire, qui détecte le dépassement de la valeur maximale et remet à zéro le registre quand c'est nécessaire, via l'entrée de Remise à zéro (entrée Reset).

Compteur modulo N.

La valeur maximale peut même être configurable. Pour cela, le compteur est associé à un registre, dans lequel on place la valeur maximale souhaitée. Ce registre est appelé le registre de configuration. A chaque cycle d'horloge, la valeur dans le compteur est comparée à la valeur dans le registre de configuration. Si elles sont identiques, le compteur est remis à zéro. Le tout se fait avec un circuit comprenant le compteur, le registre, et un circuit comparateur qui vérifie que les deux sont égaux.

Compteur à valeur maximale programmable de 4 bits, le registre de configuration n'est pas représenté.

Pour le moment, nous ne savons pas faire de circuits comparateurs, mais cela n'a rien de compliqué. Le circuit comparateur est illustré ci-dessous. Il teste l'égalité bit par bit, avant de combiner ces résultats avec une porte ET. Le circuit qui compare si deux bits sont égaux est assez simple et vous pouvez le retrouver en écrivant sa table de vérité. Vous devriez tomber sur une porte NXOR.

4 Bit Counter Prog

L'incrémenteur/décrémenteur

[modifier | modifier le wikicode]

Certains compteurs, aussi appelés incrémenteurs comptent de un en un. Les décompteurs analogues sont appelés des décrementeurs. Nous allons voir comment créer ceux-ci dans ce qui va suivre. Il faut savoir qu'il existe deux méthodes pour créer des incrémenteurs/décrémenteurs. La première donne ce qu'on appelle des incrémenteurs asynchrones, et l'autre des incrémenteurs synchrones. Nous allons commencer par voir comment fabriquer un incrémenteur asynchrone, avant de passer aux incrémenteurs synchrones.

L'incrémenteur/décrémenteur asynchrone

[modifier | modifier le wikicode]

Pour fabriquer un incrémenteur asynchrone, la première méthode, il suffit de regarder la séquence des premiers entiers, puis de prendre des paires de colonnes adjacentes :

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

Pour la colonne la plus à droite (celle des bits de poids faible), on remarque que celle-ci inverse son contenu à chaque cycle d'horloge. Pour les colonnes suivantes, le bit sur une colonne change quand le bit de la colonne précédente passe de 1 à 0, en clair, lorsqu'on a un front descendant sur la colonne précédente. Maintenant que l'on sait cela, on peut facilement créer un compteur avec quelques bascules. On peut les créer avec des bascules T, D, JK, et bien d'autres. Nous allons d'abord voir ceux fabriqués avec des bascules T, plus simples, puis ceux fabriqués avec des bascules D.

Les incrémenteurs/décrémenteurs asynchrones à base de bascules T

[modifier | modifier le wikicode]

Pour rappel, les bascules T inversent leur contenu à chaque cycle d'horloge. Par simplicité, nous allons utiliser des bascules avec une sortie qui fournit l'inverse du bit stocké.

La première colonne inverse son contenu à chaque cycle, elle correspond donc à une bascule T simplifiée reliée directement à l'horloge. Les autres colonnes s'inversent quand survient un front descendant sur la colonne précédente. Le circuit qui correspond est illustré ci-dessous, avec des bascules T activées sur front descendant. Attention, cependant : la bascule la plus à gauche stocke le bit de poids FAIBLE, pas celui de poids fort. En fait, le nombre binaire est ici stocké de gauche à droite et non de droite à gauche ! Cela sera pareil dans tous les schémas qui suivront.

Compteur asynchrone de 3 bits, basé sur des bascules T simplifiées activées sur front descendant.

Il est aussi possible d'utiliser des bascules T actives sur front montant. En effet, notons qu'un front descendant sur la sortie Q correspond à un front montant sur la sortie /Q. En clair, il suffit de relier la sortie /Q d'une colonne sur l'entrée d'horloge de la suivante. Le circuit est donc le suivant :

Compteur asynchrone de 3 bits, basé sur des bascules T simplifiées activées sur front montant.

Un décrémenteur est strictement identique à un incrémenteur auquel on a inversé tous les bits. On peut donc réutiliser le compteur du dessus, à part que les sorties du compteur sont reliées aux sorties Q des bascules.

Décompteur asynchrone de 3 bits, basé sur des bascules T simplifiées activées sur front descendant.

Il est possible de fusionner un incrémenteur et un décrémenteur asynchrone, de manière à créer un circuit qui puisse soit incrémenter, soit décrémenter le compteur. Le choix de l'opération est réalisé par un bit d'entrée, qui vaut 1 pour une incrémentation et 0 pour une décrémentation. L'idée est que les entrées des bascules sont combinées avec ce bit, pour donner les entrées compatibles avec l'opération demandée.

AsyncCounter UpDown

L'incrémenteur asynchrone à base de bascules D

[modifier | modifier le wikicode]

Il est aussi possible d'utiliser des bascules D pour créer un compteur comme les deux précédents. En effet, une bascule T simplifiée est identique à une bascule D dont on boucle la sortie /Q sur l'entrée de données.

Compteur asynchrone, sans initialisation

Cette implémentation peut être modifiée pour facilement réinitialiser le compteur à une valeur non-nulle. Pour cela, il faut ajouter une entrée au compteur, sur laquelle on présente la valeur d’initialisation. Chaque bit de cette entrée est reliée à un multiplexeur, qui choisir quel bit mémoriser dans la bascule : celui fournit par la mise à jour du compteur, ou celui présenté sur l'entrée d'initialisation. On obtient le circuit décrit dans le schéma qui suit. Quand l'entrée Reset est activée, les multiplexeurs connectent les bascules aux bits sur l'entrée d'initialisation. Dans le cas contraire, le compteur fonctionne normalement, les multiplexeurs connectant l'entrée de chaque bascule à sa sortie.

Compteur asynchrone, avec initialisation.

L'incrémenteur/décrémenteur synchrone

[modifier | modifier le wikicode]

Passons maintenant à l'incrémenteur synchrone. Pour le fabriquer, on repart de la séquence des premiers entiers. Dans ce qui va suivre, nous allons créer un circuit qui compte de 1 en 1, sans utiliser d'additionneur. Pour comprendre comment créer un tel compteur, nous allons reprendre la séquence d'un compteur, déjà vue dans le premier extrait :

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

On peut remarquer quelque chose dans ce tableau : peu importe la colonne, un bit s'inversera au prochain cycle d’horloge quand tous les bits des colonnes précédentes valent 1. Et c'est vrai quelle que soit la taille du compteur ou sa valeur ! Ainsi, prenons le cas où le compteur vaut 110111 :

  • les deux premiers 1 sont respectivement précédés par la séquence 10111 et 0111 : vu qu'il y a un zéro dans ces séquences, ils ne s'inverseront pas au cycle suivant ;
  • le bit qui vaut zéro est précédé de la séquence de bit 111 : il s'inversera au cycle suivant ;
  • le troisième 1 en partant de la gauche est précédé de la séquence de bits 11 : il s'inversera aussi ;
  • même raisonnement pour le quatrième 1 en partant de la gauche ;
  • 1 le plus à droite correspond au bit de poids faible, qui s'inverse tous les cycles.

Pour résumer, un bit s'inverse (à la prochaine mise à jour) quand tous les bits des colonnes précédentes valent 1. Pour implanter cela en circuit, on a besoin d'ajouter un circuit qui détermine si les bits des colonnes précédentes sont à 1, qui n'est autre qu'un simple ET entre les bits en question. On pourrait croire que chaque bascule est précédée par une porte ET à plusieurs entrées qui fait un ET avec toutes les colonnes précédentes. Mais en réalité, il y a moyen d'utiliser des portes plus simples, avec une banale porte ET à deux entrées pour chaque bascule. Le résultat est indiqué ci-dessous.

Compteur synchrone à incrémenteur avec des bascules T.

L’implémentation de circuit avec des bascules D est légèrement plus complexe. Il faut ajouter un circuit qui prend en entrée le contenu de la bascule et un bit qui indique s'il faut inverser ou pas. En écrivant sa table de vérité, on s’aperçoit qu'il s'agit d'un simple XOR.

Compteur synchrone à incrémenteur avec des bascules D.

On peut appliquer la même logique pour un décrémenteur. Avec ce circuit, un bit s'inverse lorsque tous les bits précédents sont à zéro. En utilisant le même raisonnement que celui utilisé pour concevoir un incrémenteur, on obtient un circuit presque identique, si ce n'est que les sorties des bascules doivent être inversées avant d'être envoyée à la porte XOR qui suit.

La gestion des débordements d'entiers des incrémenteurs/décrémenteurs

[modifier | modifier le wikicode]

Pour rappel, tout compteur a une valeur maximale au-delà de laquelle il ne peut pas compter. Pour un compteur modulo N, le compteur compte de 0 à N-1. Pour les compteurs non-modulo, ils peuvent compter de 0 à , pour un compteur de n bits. Tout nombre en dehors de cet intervalle ne peut pas être représenté. Si le résultat d'un calcul sort de cet intervalle, il ne peut pas être représenté par l'ordinateur et il se produit ce qu'on appelle un débordement d'entier. Les débordements d'entiers surviennent quand on incrémente une valeur au-delà de la valeur maximale du compteur, ce qui est loin d'être rare.

La gestion des débordements peut se faire de deux manières différentes, mais celle qui est la plus utilisée sur les compteurs est tout simplement de réinitialiser le compteur quand on dépasse la valeur maximale. Pour les compteurs non-modulo, il n'y a pas besoin de faire quoique ce soit, car ils sont réinitialisés automatiquement. Prenez un compteur contenant la valeur maximale 111....1111, incrémentez-le, et vous obtiendrez 000...0000 automatiquement, sans rien faire. Par contre, pour les compteurs modulo, c'est une autre paire de manche. La réinitialisation ne se fait pas automatiquement, et on doit ajouter des circuits pour réinitialiser le compteur, et pour détecter quand le réinitialiser.

Les incrémenteurs/décrémenteurs précédents ne peuvent pas être réinitialisés. Pour que cela soit possible, il faut d'abord rajouter une entrée qui commande la réinitialisation du compteur : mise à 1 pour réinitialiser le compteur, à 0 sinon. Ensuite, il faut que les bascules du compteur aient une entrée de réinitialisation Reset, qui les force à se remettre à zéro. Il suffit alors de faire comme avec n'importe quel registre réinitialisable : connecter ensemble les entrées Reset des bascules et relier le tout à l'entrée de réinitialisation du compteur.

Compteur réinitialisable.

Maintenant que l'on a de quoi les réinitialiser, on ajoute un comparateur qui détecte quand la valeur maximale est atteinte, afin de commander l'entrée de réinitialisation. Prenons un compteur modulo 6, par exemple, ce qui veut dire qu'il compte de 0 à 5, et est remis immédiatement à zéro quand il atteint 6. Il compte donc comme suit : 0, 1, 2, 3, 4, 5, 0, 1, 2, ... Le circuit comparateur vérifie si la valeur maximale 6 est atteinte et qui met à 1 l'entrée Reset si c'est le cas. Un tel circuit est juste un comparateur avec une constante, que vous savez déjà fabriquer à cet endroit du cours. Un exemple est illustré ci-dessous.

Compteur modulo 10.

Il peut être utile de prévenir quand un compteur a débordé. Cela a des applications assez intéressantes, qu'on verra dans les chapitres suivants, notamment pour ce qui est des diviseurs de fréquence et des timers. Pour cela, on ajoute une sortie au compteur, qui est mise à 1 quand le compteur déborde. Pour les compteurs modulo, la sortie n'est autre que la sortie du comparateur. Et on peut généraliser pour n'importe quel N. Mais les choses sont plus compliquées pour les compteurs non-modulo, qui comptent de 0 au , pour lesquels on doit ajouter un circuit détecte quand un débordement d'entier a lieu. Pour cela, il y a plusieurs solutions. La plus simple est d'ajouter un comparateur qui vérifie si le compteur est à 0, signe qu'il vient d'être réinitialisé et a donc débordé. L'autre solution est d'ajouter une bascule à la toute fin de l'incrémenteur : cette bascule est mise à 1 en cas de débordement. Il suffit alors d'ajouter un circuit qui compare la valeur de cette bascule avec sa valeur du cycle précédent, et le tour est joué.

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

[modifier | modifier le wikicode]

Certains compteurs sont basés sur un registre à décalage. Pour simplifier les explications, nous allons les classer suivant le type de registre à décalage utilisé. Pour rappel, un registre à décalage dispose d'une entrée et d'une sortie. L'entrée peut être de deux types : soit une entrée série qui prend 1 bit, soit une entrée parallèle qui prend un nombre. Il en est de même pour la sortie, qui peut être série ou parallèle, à savoir qu'elle peut fournir en sortie soit un bit unique, soit un nombre complet. En combinant les deux, cela donne quatre possibilités qui portent les noms de registres à décalage PIPO, PISO, SIPO et SISO (P pour parallèle, S pour série, I pourInput, O pour Output).

Classification des registres à décalage
Entrée parallèle Entrée série
Sortie parallèle (registre simple) SIPO
Sortie série PISO SISO

Les registres PIPO sont en réalité des registres simples, pas des registres à décalage, donc on les omets dans de qui suit. Les compteurs déterministes peuvent se fabriquer avec les trois types restants de registres à décalage, ce qui donne trois types de compteurs déterministes. Malheureusement, ces types ne portent pas de noms proprement dit. À la rigueur, les compteurs déterministes basés sur un registre SISO sont appelés des compteurs en anneau, et encore cette dénomination est légèrement impropre.

À l'exception de certains compteurs one-hot qui seront vu juste après, ces compteurs sont des compteurs à rétroaction. Un terme bien barbare pour dire que l'on boucle leur sortie sur leur entrée, parfois en insérant un circuit combinatoire entre les deux. Les compteurs à rétroaction sont particuliers dans le sens où on n'a pas à changer leur valeur en cours de fonctionnement : on peut les réinitialiser, mais pas insérer une valeur dans le compteur. La raison est qu'ils sont utilisés pour une fonction bien précise : dérouler une suite de nombres bien précise, prédéterminée lors de la création du compteur. Par exemple, on peut créer un compteur qui sort la suite de nombres 4,7,6,1,0,3,2,5 en boucle, quel qu’en soit l'utilité. Cela peut servir pour fabriquer des suites de nombres pseudo-aléatoires, ce qui est de loin leur utilisation principale, comme on le verra dans un chapitre spécialement dédié aux circuits générateurs d'aléatoire. Mais certaines de leurs utilisations sont parfois surprenantes.

Un bon exemple de cela est leur utilisation dans l'Intel 8008, le tout premier microprocesseur 8 bits commercialisé, où ce genre de compteurs étaient utilisés pour le pointeur de pile, pour économiser quelques portes logiques. Ces compteurs implémentaient la séquence suivante : 000, 001, 010, 101, 011, 111, 110, 100. Pour les connaisseurs qui veulent en savoir plus, voici un article de blog sur le sujet : Analyzing the vintage 8008 processor from die photos: its unusual counters.

Les compteurs en anneau et de Johnson (SISO)

[modifier | 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, généralement, on boucle la sortie du registre sur son entrée, ce qui veut dire que le bit sortant du registre est renvoyé sur son entrée. Pour les compteurs fabriqués avec un registre SISO (entrée et sortie de 1 bit), le bit entrant dans le registre à décalage est calculé à partir du bit sortant. Et il n'y a pas 36 façons de faire ce calcul : soit le bit sortant est laissé tel quel, soit il est inversé, pas d'autre possibilité. La première possibilité, où le bit entrant est égal au bit sortant, donne un compteur en anneau, vu plus haut. La seconde possibilité, où le bit sortant est inversé avant d'être envoyé sur l'entrée du registre à décalage, donne un compteur de Johnson. Les deux sont très différents, et ne fonctionnent pas du tout pareil.

Les compteurs en anneau : 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. Dans une telle représentation, un bit est à 1 pendant que les autres sont à 0. es 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 n'encode aucune valeur, c'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.

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 ;
  • 0010 ;
  • 0100;
  • 1000.

Incrémenter ou décrémenter le compteur demande alors de faire un simple décalage, ce qui fait que le compteur est un simple registre à décalage et non pas un compteur combiné à un incrémenteur/décrémenteur compliqué. L'économie en circuits d'incrémentation n'est pas négligeable. De plus, faire des comparaisons avec ce type de compteur est très simple : le compteur contient la valeur N si le énième bit est à 1. Pas besoin d'utiliser de circuit comparateur, juste de lire un bit. Mais les économies en termes de circuits (incrémenteur et comparateur) sont cependant contrebalancée par le fait que le compteur demande plus de bascules. Imaginons que l'on veut un compteur qui compte jusqu'à une valeur N arbitraire : un compteur en binaire normal utilisera environ bascules, alors qu'un compteur one-hot demande N bascules. Mais si N est assez petit, l'économie de bascules est assez faible, alors que l'économie de circuits logiques l'est beaucoup plus. De plus, il n'y a pas qu'une économie de circuit : le compteur est plus rapide.

Si vous ne mettez que des 0 dans un compteur en anneau, il restera bloqué pour toujours. En effet, décaler une suite de 0 donnera la même suite de 0.

En théorie, un simple registre à décalage peut faire office de compteur one-hot, mais son comportement est alors invalide en cas de débordement. Un débordement met la valeur du registre à décalage à zéro, ce qui n'est pas l'effet recherché. On peut en théorie rajouter des circuits combinatoires annexes pour gérer le débordement, mais il y a encore plus simple : boucler la sortie du registre sur son entrée. Le résultat donne donc un type particulier de compteurs one-hot, appelé les compteurs en anneau sont des registres à décalage dont le bit sortant est renvoyé sur l'entrée. En faisant cela, on garantit que le registre revient à zéro, zéro étant codé avec un 1 dans le bit de poids faible. En cas de débordement, le registre est mis à zéro par le débordement, mais le bit sortant vaut 1 et ce 1 sortant est envoyé sur l'entrée pour y être inséré dans le bit de poids faible.

Compteur en anneau de 4 bits

Il y a peu d'applications qui utilisent des compteurs en anneau. Ils étaient autrefois utilisés dans les tous premiers ordinateurs, notamment ceux qui géraient une représentation des nombres spécifique appelée la Bi-quinary coded decimal. Ils étaient aussi utilisés comme diviseurs de fréquence, comme on le verra dans le chapitre suivant. De nos jours, de tels compteurs sont utilisés dans les séquenceurs de processeurs, mais aussi dans les séquenceurs de certains périphériques, ou dans les circuits séquentiels simples qui se résument à des machines à états. Ils sont alors utilisés car très rapides, 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.

Les registres à décalage à rétroaction linéaire

[modifier | modifier le wikicode]

Étudions en premier lieu le cas des registres à décalage à rétroaction linéaire. Le terme anglais pour de tels registres est Linear Feedback Shift Register, ce qui s’abrège en LFSR. Nous utiliserons cette abréviation dans ce qui suit pour simplifier grandement l'écriture. Les LFSR sont appelés ainsi pour plusieurs raisons. Déjà, registre à décalage implique qu'ils sont fabriqués avec un registre à décalage, et plus précisément des registres à décalage SIPO. A rétroaction indique que l'on boucle la sortie sur l'entrée. Linéaire indique que l'entrée du registre à décalage s'obtient par une combinaison linéaire de la sortie. Le terme combinaison linéaire signifie que l'on multiplie les bits de l'entrée par 0 ou 1, avant d'additionner le résultat. Vu que nous sommes en binaire, les constantes en question valent 0 ou 1. Voici un exemple de formule qui colle avec ce cahier des charges :

Une première simplification est possible : supprimer les multiplications par 0. Ce faisant, les bits associés ne sont tout simplement pas pris en compte dans le calcul d'addition. Tout se passe comme suit l'on ne tenait compte que de certains bits du LFSR, pas des autres. On a alors des opérations du genre :

Dans ce calcul, on ne garde qu'un seul bit du résultat, vu que l'entrée du registre à décalage ne fait qu'un bit. Par simplicité, on ne garde que le bit de poids faible. Or, il s'avère que cela simplifie grandement les calculs, car cela nous dispense de gérer les retenues. Et nous verrons dans quelques chapitres qu'additionner deux bits en binaire, sans tenir compte des retenues, revient à faire une simple opération XOR. On peut donc remplacer les additions par des XOR.

Le résultat est ce que l'on appelle un LFSR de Fibonacci, ou encore un LFSR classique, qui celui qui colle le mieux avec la définition.

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

Les registres à décalage à rétroaction de Gallois sont un peu l'inverse des LFSR vus juste avant. Au lieu d'utiliser un registre à décalage SIPO, on utilise un registre à décalage PISO. Pour faire la différence, nous appellerons ces derniers les LFSR PISO, et les premiers LFSR SIPO. Avec les LFSR PISO, on prend le bit sortant et on en déduit plusieurs bits à partir d'un circuit combinatoire, qui sont chacun insérés dans le registre à décalage à un endroit bien précis. Bien sûr, la fonction qui calcule des différents bits à partir du bit d'entrée conserve les mêmes propriétés que celle utilisée pour les LFSR : elle se calcule avec uniquement des portes XOR. Leur avantage est qu'ils sont plus rapides, sans avoir les inconvénients des autres LFSR. Ils sont plus rapides car il n'y a qu'une seule porte logique entre la sortie et une entrée du registre à décalage, contre potentiellement plusieurs avec les LFSR SIPO.

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

Les variantes des registres à décalage à rétroaction linéaire

[modifier | modifier le wikicode]

Il existe une variante des LFSR, qui modifie légèrement son fonctionnement. Il s'agit des registres à décalages à rétroaction affine.

Pour les LFSR SIPO, la fonction qui calcule le bit de résultat n'est pas linéaire, mais se calcule par une formule comme la suivante. Notez le +1 à la fin de la formule : c'est la seule différence.

Le résultat obtenu est l'inverse de celui obtenu avec le LFSR précédent. Un tel circuit est donc composé de portes NXOR, comparé à son comparse linéaire, composé à partir de portes XOR. Petite remarque : si je prends un registre à rétroaction linéaire et un registre à rétroaction affine avec les mêmes coefficients sur les mêmes bits, le résultat du premier sera égal à l'inverse de l'autre. Notons que tout comme les LFSR qui ne peuvent pas mémoriser un 0, de tels registres à décalage à rétroaction ne peuvent pas avoir la valeur maximale stockable dans le registre. Cette valeur gèle le registre à cette valeur, dans le sens où le résultat au cycle suivant sera identique. Mais cela ne pose pas de problèmes pour l'initialisation du compteur.

Pour les LFSR PISO, il existe aussi une variante affine, où les portes XOR sont remplacées par des portes NXOR.

Il existe enfin des compteurs de ce type qui ne sont pas des LFSR, même en incluant les compteurs de Gallois et autres. Ce sont des compteurs basés sur des registres à décalage où le circuit combinatoire inséré entre l'entrée et la sortie n'est pas basé sur des portes XOR ou NXOR. Ils sont cependant plus compliqués à concevoir, mais ils ont beaucoup d'avantages.

La période d'un compteur à rétroaction

[modifier | 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, ce registre ne peut contenir qu'un nombre fini de valeurs, ce qui fait qu'il finira donc par repasser par une valeur qu'il aura déjà parcourue. Une fois qu'il repassera par cette valeur, son fonctionnement se reproduira à l'identique comparé à son passage antérieur. Lors de son fonctionnement, le compteur finira par repasser par une valeur parcourue auparavant et il bouclera. Il parcourt un nombre N de valeurs à chaque cycle, ce nombre étant appelé la période du compteur.

Le cas le plus simple est celui des compteurs en anneau, suivi par les compteurs Johnson. Les deux compteurs ont des périodes très différentes. Un compteur en anneau de N bits peut prendre N valeurs différentes, qui ont toutes un seul bit à 1. À l'opposé, un compteur Johnson peut prendre deux fois plus de valeurs. Pour nous en rendre compte, comparons la séquence de nombre déroulé par chaque compteur. Pour 5 bits, les séquences sont illustrées ci-dessous, dans les deux animations.

Compteur en anneau de 5 bits.
Compteur de Johnson de 5 bits.

La période des registres à décalage à rétroaction linéaire dépend fortement de la fonction utilisée pour calculer le bit de sortie, des bits choisis, etc. Dans le meilleur des cas, le registre à décalage à rétroaction passera par presque toutes les valeurs que le registre peut prendre. Si je dis presque toutes, c'est simplement qu'une valeur n'est pas possible : suivant le registre, le zéro ou sa valeur maximale sont interdits. Si un registre à rétroaction linéaire passe par zéro, il y reste bloqué définitivement. La raison à cela est simple : un XOR sur des zéros donnera toujours 0. Le même raisonnement peut être tenu pour les registres à rétroaction affine, sauf que cette fois-ci, c'est la valeur maximale stockable dans le registre qui est fautive. Tout le chalenge consiste donc à trouver quels sont les registres à rétroaction dont la période est maximale : ceux dont la période vaut . Qu'on se rassure, quelle que soit la longueur du registre, il en existe au moins un : cela se prouve mathématiquement, même si nous ne vous donnerons pas la démonstration.


L'initialisation d'un compteur à rétroaction

[modifier | modifier le wikicode]

Sur la quasi-totalité des compteurs et registres vu dans ce chapitre et les précédents, le compteur peut être initialisé à une valeur arbitraire. De plus, les débordements d'entiers sont possibles. Mais sur une bonne partie des compteurs à rétroaction, rien de tout cela n'est possible. Rappelons que les compteurs à rétroaction déroulent une suite de nombres bien précise, déterminée lors de la création du compteur. Donc, ça ne servirait à rien de charger une valeur arbitraire dans ces compteurs, du fait de leur fonctionnement. De plus, leur fonctionnement est périodique, ce qui fait que de tels compteurs ne peuvent pas déborder. En conséquence, un compteur à rétroaction ne peut pas être initialisé à une valeur arbitraire, mais seulement réinitialisé à une valeur de base qui est toujours la même.

Et ce qui est de l'initialisation, tous les compteurs basés sur un registre à décalage ne sont pas égaux. Pour résumer, deux cas sont possibles : soit le compteur peut être initialisé avec zéro sans que cela pose problème, soit ce n'est pas le cas. Les deux cas donnent des résultats différents. Autant le premier cas fait que l'on fait comme avec tous les autres registres, autant le second cas est inédit et demande des solutions particulières, qui sont les mêmes que le compteur oit en anneau, un LFSR, ou autre.

Le premier cas est celui où le compteur peut être initialisé avec zéro sans que cela ne pose problème. C'est le cas sur les compteurs de Johnson, mais aussi sur les registres à décalage à rétroaction non-linéaire. Sur de tels compteurs, la réinitialisation se fait comme pour n'importe quel registre/compteur. A savoir que les entrées de reset des bascules sont toutes connectées ensemble, au même signal de reset.

Compteur de Johnson de 4 bits

Dans le second cas, on ne peut pas l'initialiser avec uniquement des 0, comme les autres registres, et la méthode précédente ne fonctionne pas. C'est le cas sur les compteurs en anneau, et sur les LFSR. Sur les compteurs en anneau, lors de la réinitialisation, il faut faire en sorte que toutes les bascules soient réinitialisées, sauf une qui est mise à 1. Pour cela, il faut utiliser des bascules, avec une entrée pour les reset et une autre pour les mettre à 1. Le signal de reset est envoyée normalement sur toutes les bascules, sauf pour la première. La première bascule est configurée de manière à ce que le signal de reset la mette à 1, en envoyant le signal de reset directement sur l'entrée S (Set) qui met la bascule à 1 quand elle est activée. Cela garantit que le registre est réinitialisé avec un zéro codé en one-hot.

Compteur en anneau de 4 bits

Une autre solution est de mettre un multiplexeur juste avant l'entrée du registre à décalage. Cette solution marché bien dans le sens où elle permet d'initialiser le registre avec une valeur arbitraire, qui est insérée dans le registre en plusieurs cycles. Elle fonctionne sur les registres en anneau, mais aussi sur les LFSR. Pour les LFSR, le multiplexeur est connecté soit au bit calculé par les portes XOR, soit par une entrée servant uniquement de l'initialisation.

Initialisation d'un LFSR

Les compteurs 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. L'idée derrière ces circuits est tout simplement de compter les cycles d'horloge. Vu qu'un compteur/décompteur est cadencé par le signal d'horloge, on peut l'incrémenter ou le décrémenter à chaque cycle d'horloge, ce qui lui fait compter les cycles d'horloge. Compter les cycles d'horloge a plusieurs utilités. On peut s'en servir pour mesurer des durées, ou pour diviser une fréquence. Dans ce qui va suivre, nous allosn voire deux types de circuits : les diviseurs de fréquence, et les timers.

Les diviseurs de fréquence

[modifier | 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 plus faible. Plus précisément, la fréquence de sortie est 2, 3, 4, ou 18 fois plus faible que la fréquence d'entrée. La fréquence est donc divisée par un nombre N, qui dépend du diviseur de fréquence. Il existe des diviseurs de fréquence qui divisent la fréquence par 2, d'autres par 4, d'autres par 13, etc.

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

[modifier | modifier le wikicode]

Leur implémentation est simple : il suffit d'un compteur auquel on rajoute une sortie. Pour être plus précis, il faut utiliser un compteur modulo. Pour rappel, le compteur modulo est un compteur qui est remis à zéro quand il atteint une valeur limite. Pour un diviseur de fréquence par N, il faut plus précisément un compteur modulo par N. Tous les N cycles, le compteur déborde, à savoir qu'il dépasse sa valeur maximale et est remis à zéro. Une sortie du compteur indique si le compteur déborde : elle est mise à 1 lors d'un débordement et reste à 0 sinon. L'idée est de compter le nombre de cycles d'horloges, et de mettre à 1 la sortie quand le compteur déborde.

Par exemple, pour diviser une fréquence par 8, on prend un compteur 3 bits. A chaque fois que le compteur déborde et est réinitialisé, on envoie un 1 en sortie. Le résultat est un signal qui est à 1 tous les 8 cycles d'horloge, à savoir un signal de fréquence 8 fois inférieure. La même idée marche avec un diviseur de fréquence par 6, sauf que l'on doit alors utiliser un compteur modulo par 6, ce qui veut dire qu'il compte de 0 à 5 comme suit : 0, 1, 2, 3, 4, 5, 0, 1, 2, ... Le compteur déborde tous les 6 cycles d’horloge, ce qui fait que sa sortie de débordement est à 1 tous les 6 cycles, ce qui est demandé.

Si n'importe quel compteur fait l'affaire, il est cependant utile d'utiliser les compteurs les plus adaptés à la tâche. Pour faire un diviseur de fréquence, on utilise rarement un compteur complet, mais souvent des compteurs plus simples, comme un circuit incrémenteur ou des compteurs en anneau.

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

[modifier | modifier le wikicode]

Il est rare que l'on doive diviser une fréquence par 50 ou par 100, par exemple. Un diviseur de fréquence divise une fréquence par N, avec N très petit. Or, les compteurs one-hot, aussi appelés compteurs en anneau, sont particulièrement adaptés pour compter jusqu'à des valeurs assez faibles : 6, 10, 12, 25, etc. Il est donc naturel d'utiliser un compteur en anneau dans un diviseur de fréquence.

Pour diviser une fréquence par N, il suffit de prendre un compteur en anneau qui compte de 0 à N-1 (inclut). Le signal de sortie est mis à 1 et/ou inversé quand le compteur est remis à zéro, c'est à dire quand son bit de poids faible est à 1. Une méthode alternative consiste à regarder au contraire le bit de poids fort : le compteur atteint N quand ce bit est à 1, ce qui fait qu'il a compté jusqu'à N. En clair, la sortie est obtenue en regardant la valeur du bit de poids faible/fort, sans même utiliser de comparateur. Pas besoin de comparateur, donc.

Et au-delà de ça, le circuit obtenu est beaucoup plus simple qu’avec un compteur normal. Et c'est la raison pour laquelle les diviseurs de fréquence sont souvent conçus en utilisant des compteurs one-hot. Plus on divise une fréquence par un N très petit, plus les compteurs auront d'avantages : très simples, demandent peu de 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 incrémenteurs à bascule T

[modifier | modifier le wikicode]

Il est aussi possible de concevoir des diviseurs de fréquence en utilisant un banal incrémenteur. Si l'incrémenteur est un incrémenteur non-modulo, on se retrouve avec un diviseur de fréquence qui divise la fréquence d'entrée par une puissance de deux. Pour comprendre comment les fabriquer, nous allons étudier le cas le plus simple : celui qui divise par 2 la fréquence d'entrée. Et bien sachez qu'il s'agit d'une simple bascule T. En effet, regardons ce qui se passe quand on envoie un signal constamment à 1 sur son entrée T. Dans ce cas, la bascule s'inversera une fois par chaque cycle d'horloge. Un cycle d'horloge sur la sortie correspond au temps passé entre deux inversions.

Diviseur de fréquence par 2.

Pour créer un diviseur de fréquence par 4, il suffit d'enchainer deux fois le circuit précédent. La sortie de la première bascule T doit être envoyée sur l'entrée T de la seconde bascule. Et pour créer un diviseur de fréquence par 8, il suffit d'enchainer trois fois le circuit précédent. Et ainsi de suite. Au final, un diviseur de fréquence qui divise la fréquence d'entrée par 2^N est un enchainement de N bascules T, qui n'est autre qu'un circuit incrémenteur. La sortie d'un tel diviseur de fréquence se situe en sortie de la dernière bascule.

Diviseur de fréquence par 8.

On peut en profiter pour créer un circuit à plusieurs sorties, en mettant une sortie par bascule. Le circuit, illustré ci-dessous, fournit donc plusieurs fréquences de sortie : une à la moitié de la fréquence initiale, une autre au quart de la fréquence d'entrée, une autre au huitième, etc.

Diviseur de fréquence multiple.

Les timers, aussi appelés Programmable interval timer, sont des circuits capables de compter des durées. Leur fonctionnement est assez simple : on leur envoie un certain nombre de cycles d'horloge en entrée, et ils émettent un signal quand ce nombre de cycles est écoulé. Le signal en question est disponible sur une sortie de 1 bit, et correspond tout simplement au fait que cette sortie est mise à 1, pendant un cycle d'horloge. Ils permettent de compter des durées, exprimées en cycles d'horloge. On peut aussi générer un signal qui surviendra après 50 cycles d'horloge, ou après 100 cycles d'horloge, etc.

Les timers sont composés d'un compteur/décompteur cadencé par un signal d'horloge. Le compteur initialisé à 0, puis est incrémenté à chaque signal d'horloge, jusqu’à atteinte d'une valeur limite où il génère un signal. Pour un décompteur, c'est la même chose, sauf que le décompteur est initialisé à sa valeur limite et est décrémenté à chaque cycle, et envoie un signal quand il atteint 0. Les timers basés sur des décompteurs sont nettement plus simples que les autres, ce qui fait qu'ils sont plus utilisés. Pour que les timers soient configurables, on doit pouvoir préciser combien de cycles il faut (dé-)compter avant d'émettre un signal. On peut ainsi préciser s'il faut émettre le signal après 32 cycles d'horloge, après les 50 cycles, tous les 129 cycles, etc. Pour cela, il suffit de préciser le nombre de cycles à compter/décompter en entrée et d'initialiser le compteur/décompteur avec.

Les timers matériels peuvent compter de deux manières différentes, appelées mode une fois et mode périodique. Concrètement, le mode périodique divise la fréquence d'entrée, alors que le mode une fois compte durant une durée fixe avant de s'arrêter.

  • En mode une fois, le timer s'arrête une fois qu'il a atteint la limite configurée. On doit le réinitialiser manuellement, par l'intermédiaire du logiciel, pour l'utiliser une nouvelle fois. Cela permet de compter une certaine durée, exprimée en nombre de cycles d'horloge.
  • En mode périodique, le timer se réinitialise automatiquement avec la valeur de départ, ce qui fait qu'il reboucle à l'infini. En clair, le timer se comporte comme un diviseur de fréquence. Si le compteur est réglé de manière à émettre un signal tous les 9 cycles d'horloge, la fréquence de sortie sera de 9 fois moins celle de la fréquence d'entrée du compteur.

Un ordinateur est rempli de timers divers. Dans ce qui va suivre, nous allons voir les principaux timers, qui sont actuellement intégrés dans les PC modernes. Ils se trouvent sur la carte mère ou dans le processeur, tout dépend du timer.

Le watchdog timer

[modifier | modifier le wikicode]

Le watchdog timer est un timer spécifique dont le but est d'éteindre ou de redémarrer automatiquement l'ordinateur si jamais celui-ci ne répond plus ou plante. Tous les ordinateurs n'ont pas ce genre de timer, et beaucoup de PC s'en passent. Mais ce timer est très fréquent dans les architectures embarquées.

Le watchdog timer est un compteur/décompteur qui doit être réinitialisé régulièrement. S'il n'est pas réinitialisé, le watchdog timer déborde (revient à 0 ou atteint 0) et envoie un signal qui redémarre le système. Le système est conçu pour réinitialiser le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer. Si jamais le système dysfonctionne gravement, le système ne pourra pas réinitialiser le watchdog timer et le système est redémarré automatiquement ou mis en arrêt.

Le Watchdog Timer et l'ordinateur.

Le Time Stamp Counter des processeurs x86

[modifier | 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 ont une fréquence variable. Nous expliquerons cela plus en détail dans quelques chapitres, mais les processeurs actuels font varier leur fréquence suivant les besoins. Ils augmentent leur fréquence quand on leur demande de faire beaucoup de calculs, et se mettent en mode basse(fréquence pour économiser de l'énergie si on ne leur demande pas grand chose. Avec une fréquence variable, le Time Stamp Counter perd complétement en fiabilité. Intel a tenté de corriger ce défaut en incrémentant ce registre à une fréquence constante, différente de celle du processeur, ce qui est encore le cas sur les processeurs Intel actuels. Le comportement est un peu différent sur les processeurs AMD, mais il compte par cycle d'horloge, avec des mécanismes de synchronisation assez complexes pour corriger l'effet de la fréquence variable.

L'horloge temps réel

[modifier | 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, les systèmes d'exploitation modernes ne l'utilisent pas pour ça. L'horloge temps réel est trop imprécise et sa fréquence n'aide pas. En effet, 1024 Hz est proche de 1000, mais pas assez pour faire des mesures à la missliseconde près, chose qui est nécessaire pour mesurer le ping ou d'autres choses utiles.

A la place, l'ordinateur l'utiliser pour compter les secondes, afin que l'ordinateur soit toujours à l'heure. Vous savez déjà que l'ordinateur sait quelle heure il est (vous pouvez regarder le bureau de Windows dans le coin inférieur droite de votre écran pour vous en convaincre) et il peut le faire avec une précision de l'ordre de la seconde. Mais pour savoir quel jour, heure, minute et seconde il est, l'ordinateur doit faire deux choses : mémoriser la date exacte à la seconde près, et avoir la capacité de compter le temps qui s'écoule, seconde par seconde. Pour cela, un ordinateur contient une CMOS RAM qui mémorise la date, et la RTC.

Le Programmable Interval Timer : l'Intel 8253

[modifier | modifier le wikicode]
Intel 8253 and 8254

L'Intel 8253 est un timer programmable qui était autrefois intégré dans les cartes mères des ordinateurs personnels de type PC. Les premiers processeurs x86 étaient souvent secondés avec un Intel 8253 soudé à la carte mère. Il fût suivi par l'Intel 8254, qui en était une légère amélioration. S'il n'est plus présent dans un boitier de la carte mère, on trouve toujours un circuit semblable au 8253 à l'intérieur du chipset de la carte mère, voire à l'intérieur du processeur, pour des raisons de compatibilité. Sur les PC, il est cadencé par une horloge maitre, générée par un oscillateur à Quartz, dont la fréquence est de 32 768 Hertz, soit 2^15 cycles d'horloge par seconde. La fréquence générée par un compteur va donc de 18,2 Hz à environ 500 KHz. Il était utilisé pour dériver un grand nombre de fréquences utilisées dans l'ordinateur. Par exemple, le second compteur était utilisé par défaut pour le rafraichissement de la mémoire (D)RAM, mais il était souvent reprogrammé pour servir à générer des fréquences spécifiques par le BIOS ou la carte graphique.

L'intérieur de l'Intel 8253 est illustré ci-dessous. Nous allons expliquer l'ensemble de ce schéma, rassurez-vous, mais les explications seront plus simples à comprendre si vous survolez ce schéma en premier lieu.

Intel 8253, intérieur.

L'Intel 8253 contient trois compteurs de 16 bits, numérotés de 0 à 2. Chaque compteur possède deux entrées et une sortie : l'entrée CLOCK est celle de l'horloge de 32 MHz, l'entrée GATE active ou désactive le compteur, la sortie fournit le signal voulu et/ou la fréquence de sortie.

L'Intel 8253 lui-même possède plusieurs entrées et sorties. En premier lieu, on voit un port de 8 bits connecté aux trois compteurs, qui permet à l'Intel 8253 de communiquer avec le reste de l'ordinateur. La communication se fait dans les deux sens : soit de l'ordinateur vers les compteurs, soit des compteurs vers l'ordinateur. Dans le sens ordinateur -> compteurs, cela permet à l'ordinateur de programmer les compteurs, de les initialiser. Dans l'autre sens, cela permet de récupérer le contenu des compteurs, même si ce n'est pas très utilisé.

Ensuite, on trouve un registre de 8 bits, le Control Word register qui mémorise la configuration de l'Intel 8253. Le contenu de ce registre détermine le mode de fonctionnement du compteur, de combien doit compter le compteur et bien d'autres choses. Pour programmer les trois compteurs, il faut écrire un mot de 8 bits dans le Control Word register. La configuration de l'Intel 8253 fournie en sur le port de 8 bits pendant un cycle d'horloge, puis est mémorisée dans ce registre et reste pour les cycles suivants.

Mais l'écriture a lieu à condition que les 5 entrées de configuration soit bien réglées. Les 5 entrées de configuration sont les suivantes :

  • Deux bits A0 et A1 pour sélectionner le compteur voulu avec son numéro, ou le control word register.
  • Un bit RD à mettre à 0 pour que l'ordinateur récupère le compteur sélectionné ou le control word register sur le port de 8 bits.
  • Un bit WR à mettre à 0 pour que l'ordinateur modifie le compteur sélectionné ou le control word register, en envoyant le nombre pour l'initialisation sur le port de 8 bits.
  • Un bit CS qui active ou désactive l'Intel 8253 et permet de l'allumer ou de l’éteindre.

Pour écrire dans le Control Word register, il faut mettre le bit CS à 0 (on active l'Intel 8253), mettre à 1 le bit RD et à 0 le bit WR (on indique qu'on fait une écriture), et sélectionner le Control Word register en mettant les deux bits A0 et A1 à 1. Pour écrire dans un compteur, il faut faire la même chose, sauf que les bits A0 et A1 doivent être configurés de manière à donner le numéro du compteur voulu. LA lecture s'effectue elle aussi de la même manière, mais il faut inverser les bits RD et WR.

Le High Precision Event Timer (HPET)

[modifier | 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 est couplé à plusieurs comparateurs, qui vérifient chacun une valeur limite, une valeur à laquelle générer un signal. La valeur limite peut être programmée, ce qui fait que chaque comparateur est associé à un registre pour mémoriser la valeur limite. Il doit y avoir au moins trois comparateurs, mais le nombre peut monter jusqu’à 256. Chaque comparateur doit pouvoir fonctionner en mode une fois, et au moins un comparateur doit pouvoir fonctionner en mode périodique.

High Precision Event Timer

Il faut noter que les systèmes d'exploitation conçus avant le HPET ne peuvent pas l'utiliser, pour des raisons techniques de compatibilité matérielle. C'est le cas de Windows XP avant le Service Pack 3. C'est la raison pour laquelle les cartes mères possèdent encore un PIT et une RTC, ou au moins qu'elles émulent RTC et PIT dans leurs circuits. D'ailleurs, pour économiser des circuits, les cartes mères modernes émulent le PIT et la RTC avec le HPET. Le HPET est configuré de manière à ce que le premier comparateur fournisse une fréquence de 1024 Hz, comme la RTC, et les 3 comparateurs suivants remplacent l'Intel 8253.

Le HPET gère de nombreux modes de fonctionnement : ses comparateurs peuvent être configuré en mode une fois ou périodique, on peut lui demander d'émuler la RTC et le PIT, etc. Aussi, il contient aussi de nombreux registres de configuration. En tout, on trouve 3 registres de configuration. à Cela, il faut ajouter trois registres pour configurer chaque comparateur indépendamment les uns des autres. Notons qu'il est aussi possible de lire ou écrire dans le compteur de 64 bits, mais ce n'est pas recommandé.

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éatoires. C'est très utile dans des applications cryptographiques, statistiques, mathématiques, dans les jeux vidéos, et j'en passe. L'aléatoire dans les jeux vidéos est un bon exemple : pas besoin d'un aléatoire de qualité, un simple algorithme pseudo-aléatoire suffit.

Dans certaines situations, il est nécessaire de générer des nombres aléatoires de manière matérielle. Cela peut servir pour sélectionner une ligne de cache à remplacer lors d'un défaut de cache, pour implémenter des circuits cryptographiques, pour calculer la durée d'émission sur un bus Ethernet à la suite d'une collision, et j'en passe.

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, aussi appelés Feedback Shift Registers, abréviés LSFR. Un LSFR seul ne fournit pas un aléatoire digne de ce nom, car il boucle, il a une période comme tout compteur. Par contre, 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.

  • Dans le générateur alternative step, on utilise trois LSFR. Le premier commande un multiplexeur qui choisit la sortie parmi les deux restants.
  • Dans le générateur stop-and-go, on 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.
  • Dans le shrinking generator, deux LSFR sont cadencés à des vitesses différentes. Si le bit de sortie du premier vaut 1, alors le bit de sortie du second est utilisé comme résultat. Par contre, si le bit de sortie du premier vaut 0, aucun bit n'est fourni en sortie, 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]

Les LSFR ne permettent pas d'obtenir du vrai aléatoire, compte tenu de leur comportement totalement déterministe. Pour obtenir un aléatoire un peu plus crédible, il est possible d'utiliser des moyens non-déterministes. Et certains d'entre eux utilisent le signal d'horloge.

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 les décalages et les rotations. Nous allons voir ce que sont ces opérations, avant de voir les circuits associés. Précisons que dans les ordinateurs modernes, décalages et rotations sont prises en charge par un circuit, le barrel shifter, qui est capable d'effectuer aussi bien des rotations que des décalages. Il en existe de nombreux types, mais nous allons voir les barrel shifters basés sur des multiplexeurs. Mais expliquons d'abord les différentes opérations de décalage et de rotation.

Les opérations de décalage

[modifier | modifier le wikicode]

Les décalages décalent un nombre de un ou plusieurs rangs vers la gauche, ou la droite. Le nombre à décaler est envoyé sur une entrée du circuit, de même que le nombre de rangs l'est sur une autre. Le circuit fournit le nombre décalé sur sa sortie. Il existe plusieurs opérations de décalage différentes et on peut les classer en plusieurs types. Dans les grandes lignes, on distingue les rotations, les décalages logiques et les décalages arithmétiques. Elles se distinguent sur plusieurs points, les principaux étant les suivants :

  • ce qu'on fait des bits qui sortent du nombre lors du décalage ;
  • comment on remplit les vides qui apparaissent lors du décalage ;
  • la manière dont est géré le signe du nombre décalé.
Décalages, gestion des bits entrants et sortants

Pour comprendre les deux premiers points, prenons l'exemple d'un nombre de 8 bits, comme ci-contre. L'exemple montre le décalage de 01011101 de deux rangs. On obtient 010111 : les deux bits de poids forts sont vides et les deux bits de fin (01) sortent du nombre. Et cela vaut pour tout décalage : d'un côté le décalage fait sortir des bits du nombre, de l'autre certains bits sont inconnus ce qui laisse des vides dans le nombre. Le nombre de bits sortants et de vides est strictement égal au nombre de rangs de décalage : si on décale de n rangs, alors cela laissera n vides et fera sortir n bits. Pour un décalage de n rangs, les vides sont dans les n bits de poids fort pour un décalage à droite et dans les n bits de poids faibles pour un décalage à gauche. Et les n bits sortant sont à l'opposé : bits de poids faible pour un décalage à droite et bits de poids fort pour un décalage à gauche. Ces deux points, la gestion des vides et des bits sortants, sont assez liés.

Le différents types de décalages

[modifier | modifier le wikicode]

En premier lieu, parlons de ce qu'on fait des bits qui sortent du nombre lors du décalage. Que fait-on de ces bits ?

La première solution est de les faire rentrer de l'autre côté, de les remettre au début du nombre décalé. L'opération en question est alors appelée une rotation. Il existe des rotations à droite et à gauche.

MSB : bit de poids fort

(Most Significant Bit)


LSB : bit de poids faible

(Least Significant Bit)

Rotation à gauche.
Rotation à droite.

L'autre solution est d'oublier les bits sortants. L’opération est alors appelée un décalage, qui peut être soit un décalage logique, soit un décalage arithmétique. Le fait que l'on oublie les bits sortants fait que les vides ne sont pas remplis et qu'il faut trouver de quoi les combler. Et c'est là qu'on peut faire la distinction entre décalages logiques et arithmétiques.

Avec un décalage logique, les vides sont remplis par des zéros, aussi bien pour un décalage à gauche et un décalage à droite.

Décalage logique à gauche.
Décalage logique à droite.
Décalage arithmétique à droite.

Avec un décalage arithmétique, la situation est différente pour un décalage à gauche et à droite. Le principe des décalages arithmétique est qu'ils conservent le bit de signe du nombre décalé (qui est supposé être signé), contrairement aux autres décalages. La situation est cependant quelque peu compliquée et tout dépend de l'implémentation exacte du décalage, tous les ordinateurs ne faisant pas la même chose.

Il n'y a pas d’ambigüité pour les décalages à droite, qui sont tous réalisés de la même manière sur toutes les architectures. Pour un décalage à droite, les vides dans les vides de poids forts sont remplis par le bit de signe. Ce remplissage est une sorte d'extension de signe, ce qui fait que la conservation du signe est automatique.

Décalage arithmétique à gauche qui ne conserve pas le bit de signe.

Pour un décalage à gauche, les choses sont plus compliquées. Les bits de poids faible vides sont remplis par des zéros, comme pour un décalage logique. Mais pour ce qui est de la conservation du bit de signe, c'est plus compliqué. On a deux écoles : la première ne conserve pas le bit de signe, la seconde le fait. Dans le premier cas, le décalage est identique à un décalage logique à gauche. Dans le second cas, le bit de signe n'est pas concerné par le décalage.

L'interprétation mathématique des décalages

[modifier | modifier le wikicode]

L'utilité principale des opérations de décalage est qu'elles permettent de faire simplement des multiplications ou divisions par une puissance de 2. Un décalage logique/arithmétique correspond à une multiplication ou division entière par 2^n : multiplication pour les décalages à gauche, division pour les décalages à droite. Les décalages logiques fonctionnent pour les entiers non signés, alors que les décalages arithmétiques fonctionnent sur les entiers signés. Le fait est qu'un décalage logique ne donne pas le bon résultat avec un entier signé, la raison étant qu'il ne préserve pas le bit de signe. À l'inverse, le décalage arithmétique conserve le bit de signe, du moins pour les décalages à droite, ce qui le rend adapté pour les entiers signés. Les décalages arithmétiques à droite permettent donc de faire des divisions par 2^n sur des nombres signés.

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

Les arrondis lors des décalages

[modifier | modifier le wikicode]

Les décalages à droite entraînent l'apparition d'arrondis. Lorsqu'on effectue un décalage à droite, certains bits vont sortir du résultat et être perdus. L’équivalent en décimal est que les chiffres après la virgule sont perdus, ce qui arrondit le résultat. Mais cet arrondi dépend de la représentation des nombres utilisé. Pour comprendre pourquoi, il faut faire un rapide rappel sur les types d'arrondis en décimal.

En décimal, on peut arrondir de deux manières : soit on arrondit à l'entier au-dessus, soit on arrondi à l'entier au-dessous. Par exemple, prenons la division 29/4, qui a pour résultat 7.25. Cela donne 7 dans le premier cas et 8 dans le second. Pour un résultat négatif, c'est la même chose, mais le fait que le signe soit inversé change la donne. Par exemple, prenons le résultat de -29 / 4, soit -7.25. On peut l'arrondir soit à -7, soit à -8. En combinant les deux cas négatifs avec les deux cas positifs, on se trouve face à quatre possibilités :

  • l'arrondi vers la plus basse valeur absolue (vers zéro), qui donne respectivement 7 et -7 dans l'exemple précédent.
  • l'arrondi vers la plus basse valeur (vers moins l'infini), qui donne -8 et 7 dans l'exemple précédent ;
  • l'arrondi vers la plus haute valeur (vers plus l'infini), qui donne -7 et 8 dans l'exemple précédent ;
  • l'arrondi vers la plus haute valeur absolue (vers l'infini), qui donne 8 et -8 dans l'exemple précédent.

En binaire, c'est la même chose. Par exemple, 11100,1010 peut s'arrondir en 11100 ou en 11101, suivant qu'on arrondisse vers le bas ou vers le haut, et la même chose est possible pour les nombres négatifs. Précisons que ces arrondis n'ont lieu que si le résultat du décalage n'est pas exact. Pour un décalage d'un rang, à savoir une division par deux, seuls les nombres impairs donnent un arrondi, pas les nombres pairs. De manière générale, pour un décalage de n rangs, les nombres divisibles par 2^n ne donnent pas d'arrondi, alors que les autres si.

Lors d'un décalage, les bits sortants sont simplement éliminés. On pourrait croire que cela signifie que l'arrondi se fait vers zéro (vers la valeur inférieure). C'est bien le cas pour les nombres positifs, mais pas pour les nombres négatifs pour lesquels le résultat dépend de la représentation. Pour les décalages logiques, peu importe la représentation, l'arrondi se fait vers zéro (vu que tous les nombres sont traités comme positifs). Mais pour les décalages arithmétiques, c'est autre chose. En complément à 1, l'arrondi se fait bien vers zéro : les nombres positifs sont arrondis à la valeur inférieure et les nombres négatifs à la valeur supérieure. Par contre, en complément à deux, les nombres positifs et nombres négatifs sont arrondis à la valeur inférieure. En clair, l'arrondi se fait vers moins l'infini. Ce qui peut causer quelques problèmes si l'on ne fait pas attention, le résultat du décalage et d'une division pouvant varier à cause des règles d'arrondis.

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

[modifier | modifier le wikicode]

Outre les arrondis, les décalages peuvent causer ce qu'on appelle des débordements d'entier. Ce terme barbare recouvre toutes les situations où le résultat d'un calcul devient trop gros pour être codé. Pour donner un exemple, prenons une situation équivalente mais en décimal. On suppose que l'on manipule des données codées sur 5 chiffres décimaux, pas plus. Si on prend le nombre 4512, le décalage à gauche d'un cran donne 45120, qui tient sur 5 chiffres : on n'a pas de débordement. Mais si je prends le nombre 97426, un décalage à gauche d'un cran donne 974260, ce qui ne tient pas dans 5 chiffres : on a un débordement d’entier. Celui-ci se traduit par le fait qu'un chiffre non-nul sorte du nombre. La même chose a lieu en binaire, avec les décalages à gauche. Un débordement d'entier en binaire se traduit par le fait qu'au moins un bit non-nul sorte à gauche.

La manière habituelle de gérer les débordements d'entiers est simplement de ne rien faire, mais de prévenir qu'un débordement a eu lieu. Pour cela, le circuit qui effectue le décalage a une sortie qui indique qu'un débordement a eu lieu lors du décalage. Cette sortie fournit un simple bit qui vaut 1 en cas de débordements et 0 sinon (ou l'inverse). Une autre solution est de corriger le débordement, mais si cela est fait pour les opérations arithmétiques, cela n'est pas fait pour les décalages.

Toujours est-il que déterminer l’occurrence d'un débordement n'est pas compliqué. Pour les décalages logiques, il suffit de prendre les bits sortants et de vérifier qu'un au moins d'entre eux vaut 1. Une simple porte OU sur les bits sortants fait l'affaire. Pour les décalages arithmétiques, il faut aussi tenir compte de la présence du bit de signe. La valeur des bits sortants dépend du signe positif ou négatif du nombre. Si le nombre décalé est positif, seuls des zéros doivent sortir, la présence d'un 1 indiquant un débordement d'entier. Pour un nombre négatif, c'est l'inverse : seuls des 1 doivent sortir (du fait des règles d'extension de signe), alors que l’occurrence d'un zéro trahit un débordement d'entier. Pour résumer le tout, les bits sortants sont censés être égaux au bit de signe, un débordement a eu lieu dans le cas contraire. L’occurrence d'un débordement se détermine en décomposant le décalage en une succession de décalages de 1 bit. Si un seul de ces décalages de 1 rang altère le bit de signe (change sa valeur), alors on a un débordement.

Il est possible de déterminer l’occurrence d'un débordement en analysant l'opérande, sans même avoir à faire le décalage. Pour un décalage vers la gauche de rangs, on sait que les bits sortants sont les bits de poids fort de l'opérande. En clair, on peut déterminer si un débordement a lieu en sélectionnant seulement les bits de poids fort de l'opérande. Pour cela, on peut simplement prendre l'opérande et lui appliquer un masque adéquat. Par exemple, prenons le cas d'un débordement pour un décalage logique, qui a lieu si au moins un bit sortant est à 1. Il suffit de prendre l'opérande, conserver les rangs bits de poids fort et mettre les autres à zéro, puis faire un ET entre les bits du résultat. La même logique prévaut pour les décalages arithmétiques, même s'il faut faire quelques adaptations.

Calcul du bit de débordement pour un décalage à gauche de trois rangs.

Toujours est-il que le calcul des débordements peut se faire en parallèle du décalage, ce qui est utile. Précisons que le masque se calcule dans un circuit à part, qui ressemble beaucoup à un encodeur. Le masque calculé peut être utilisé sur certains circuits de décalages, pour transformer des rotations en décalage logiques, par exemple. Mais nous verrons cela plus tard.

Les décaleurs et rotateurs élémentaires

[modifier | 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 circuit capable de décaler un nombre (vers la droite ou la gauche, peu importe) d'un nombre de rangs variable : on pourra décaler notre nombre de 2 rangs, de 3 rangs, de 4 rangs, etc. Il faudra préciser le nombre de rangs sur une entrée. On peut faire une remarque simple : décaler de 6 rangs, c'est équivalent à décaler de 4 rangs et redécaler le tout de 2 rangs. Même chose pour 7 rangs : cela consiste à décaler de 4 rangs, redécaler de 2 rangs et enfin redécaler d'un rang. En suivant notre idée jusqu'au bout, on se rend compte qu'on peut créer un décaleur à partir de décaleurs plus simples, reliés en cascade, qu'on active ou désactive suivant le nombre de rangs. L'idée est de prendre des décaleurs élémentaires qui décalent par 1, 2, 4, 8, etc ; bref : par une puissance de 2. La raison à cela est que le nombre de rangs par lequel on va devoir décaler est un nombre codé en binaire, qui s'écrit donc sous la forme d'une somme de puissances de deux. Chaque bit du nombre de rang servira à actionner le décaleur qui déplace d'un nombre égal à sa valeur (la puissance de deux qui correspond en binaire).

Décaleur logique - principe

La même logique s'applique pour les rotateurs, la seule différence étant qu'il faut remplacer les décaleurs par 1, 2, 4, 8, etc ; par des rotateurs par 1, 2, 4, 8, etc.

Reste à savoir comment créer ces décaleurs qu'on peut activer ou désactiver à la demande. Surtout que le circuit n'est pas le même selon que l'on parle d'un décalage logique, d'un décalage arithmétique ou d'une rotation. Néanmoins, tous les circuits de décalage/rotation sont fabriqués avec des multiplexeurs à deux entrées et une sortie.

Le circuit décaleur logique

[modifier | modifier le wikicode]

Commençons par étudier le cas du décalage logique. On va prendre comme exemple un décaleur par 4 à droite, mais ce que je vais dire peut être adapté pour créer des décaleurs par 1, par 2, par 8, etc. La sortie vaudra soit le nombre tel qu'il est passé en entrée (le décaleur est inactif), soit le nombre décalé de 4 rangs. Ainsi, si je prends un nombre A, composé des bits a7, a6, a5, a4, a3, a2, a1, a0 ; (cités dans l'ordre), le résultat sera :

  • soit le nombre composé des chiffres a7, a6, a5, a4, a3, a2, a1, a0 (on n'effectue pas de décalage) ;
  • soit le nombre composé des chiffres 0, 0, 0, 0, a7, a6, a5, a4 (on effectue un décalage par 4).

Chaque bit de sortie peut prendre deux valeurs, qui valent soit zéro, soit un bit du nombre d'entrée. On peut donc utiliser un multiplexeur pour choisir quel bit envoyer sur la sortie. Par exemple, pour le choix du bit de poids fort du résultat, celui-ci vaut soit a7, soit 0 : il suffit d’utiliser un multiplexeur prenant le bit a7 sur son entrée 1, et un 0 sur son entrée 0. Il suffit de faire la même chose pour tous les autres bits, et le tour est joué.

Exemple d'un décaleur par 4.

En utilisant des décaleurs basiques par 4, 2 et 1 bit, on obtient le circuit suivant :

Décaleur logique 8 bits.

Le circuit décaleur arithmétique

[modifier | 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ù il y avait un zéro avec le décaleur logique. Par exemple, reprenons un nombre A, composé des bits a7, a6, a5, a4, a3, a2, a1, a0 ; (cités dans l'ordre). La sortie d'un décaleur arithmétique par 4 sera :

  • soit le nombre composé des chiffres a7, a6, a5, a4, a3, a2, a1, a0 (on n'effectue pas de décalage) ;
  • soit le nombre composé des chiffres a7, a7, a7, a7, a7, a6, a5, a4 (on effectue un décalage arithmétique par 4).
Exemple d'un décaleur arithmétique par 4

En combinant des décaleurs basiques par 4, 2 et 1 bits, on obtient le circuit suivant :

Décaleur arithmétique 8 bits

Le circuit rotateur

[modifier | 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 ce qui va suivre, nous allons nous concentrer sur les barrel shifters qui font des décalages/rotations vers la droite. Les explications seront valides aussi pour des décalages/rotations à gauche, avec quelques petites modifications triviales. Mais nous ne verrons pas comment fabriquer des barrel shifters bidirectionnels. En effet, de tels barrel shifters sont plus compliqués à fabriquer et sont de plus basés sur un barrel shifter unidirectionnel.

Il existe trois grandes méthodes pour fabriquer un décaleur-rotateur.

  • La manière la plus naïve est de prendre un décaleur logique, un décaleur arithmétique et un rotateur, et de prendre le résultat adéquat suivant l’opération voulue. Le choix du bon résultat est effectué par une couche de multiplexeur adaptée. Mais cette solution est inutilement gourmande en multiplexeurs. Après tout, les trois circuits se ressemblent et partagent une même structure.
  • Une autre solution, bien plus économe en multiplexeurs, élimine ces redondances en fusionnant les trois circuits en un seul. Elle part d'un circuit qui effectue des décalages logiques, auquel on ajoute des multiplexeurs pour le rendre capable de faire aussi les décalages arithmétiques et les rotations.
  • La dernière méthode part d'un rotateur et on lui ajoute de quoi faire des décalages logiques.

Le décaleur-rotateur à base de multiplexeurs

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

La prise en charge des rotations

[modifier | modifier le wikicode]

Nous allons d'abord ajouter des multiplexeurs pour prendre en charge les rotations, un peu de la même manière qu'on modifie un décaleur logique pour lui faire faire aussi des décalages arithmétiques. Pour cela, prenons un décaleur par 4 et étudions les 4 bits de poids fort. Suivant le type de décalage, on doit envoyer soit un zéro, soit le bit de poids faible adéquat sur certaines entrées. Ce choix peut être réalisé par un multiplexeur, tant qu'il est commandé correctement. En clair, il suffit d'ajouter un ou plusieurs multiplexeurs pour chaque décaleur élémentaire par 1, 2, 4, etc. Ces multiplexeurs choisissent quoi envoyer sur l'entrée de l'ancienne couche : soit un 0 (décalage logique), soit le bit de poids faible (rotation). Notons qu'on doit utiliser un multiplexeur par entrée, contrairement au décaleur complet. La raison est qu'un décalage arithmétique envoie toujours le même bit dans les entrées de poids fort, alors qu'une rotation envoie un bit différent sur chaque entrée de poids fort, ce qui demande un multiplexeur par entrée.

Décaleur-rotateur par 4.

La prise en charge des décalages arithmétiques

[modifier | modifier le wikicode]

Il est possible d'étendre le décaleur logique pour lui permettre de faire des décalages arithmétiques. Pour cela, même recette que dans le cas précédent. Encore une fois, suivant le type de décalage, on doit envoyer soit un zéro, soit le bit de poids fort sur certaines entrées. Il est possible d'utiliser un seul multiplexeur dans ce cas précis, car on envoie le même bit sur les entrées de poids fort.

Exemple avec un décaleur par 4.

En combinant des décaleurs basiques par 4, 2 et 1 bits, on obtient un circuit qui fait tous les types de décalages. Pas étonnant que ce circuit soit nommé un décaleur complet. Notons qu'on peut se contenter d'un seul mutiplexeur pour tout le barrel shifter, en utilisant le câblage astucieusement. Après tout, le choix entre 0 ou bit de poids fort est le même pour toutes les entrées concernées. Autant ne le faire qu'une seule fois et connecter toutes les entrées concernées au multiplexeur.

Décaleur complet 8 bits

Le barrel shifter complet

[modifier | modifier le wikicode]

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]

Il est temps de voir la dernière manière possible pour fabriquer un décaleur-rotateur. Celle-ci se base sur les masques, vus au chapitre précédent. L'idée est de faire une rotation et de corriger le résultat si c'est un décalage qui est demandé. La correction à effectuer dépend du type de décalage demandé, suivant qu'il soit logique ou arithmétique. Le circuit complet est organisé comme illustré ci-dessous.

Pour un décalage logique, il suffit de mettre les n bits de poids fort à zéro pour un décalage de n bits vers la droite (inversement, les n bits de poids faible pour un décalage vers la gauche). Et pour mettre des bits de poids fort à zéro sous une certaine condition, on doit utiliser un masque, comme vu précédemment. Le masque en question est le même que celui calculé pour le bit de débordement d'entier. Le masque est calculé par un circuit dédié, avant d'être appliqué au résultat du rotateur. Le circuit de calcul du masque est un encodeur modifié, qu'on peut concevoir avec les techniques des chapitres précédents.

Le circuit d'application du masque est composé d'une couche de portes ET et d'une couche de multiplexeurs. La couche de portes ET applique le masque sur le résultat du rotateur. Les multiplexeurs choisissent entre le résultat du rotateur et le résultat avec masque appliqué. Les multiplexeurs sont commandés par un bit de commande qui indique s'il faut faire un décalage ou une rotation.

Décaleur-rotateur basé sur un masque.

Les barrel shifters bidirectionnels (à double sens de décalage/rotation)

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

Le rotateur bidirectionnel

[modifier | modifier le wikicode]

Pour créer le rotateur bidirectionnel, nous allons devoir étudier ce qui se passe quand on enchaine deux rotations successives. N'allons pas par quatre chemins : l'enchainement de deux rotations successives donne un résultat qui aurait pu être obtenu en ne faisant qu'une seule rotation. Le résultat issu de la succession de deux rotations est identique à celui d'une rotation équivalente. Et on peut calculer le nombre de rangs de la rotation équivalente à partir des rangs des deux rotations initiales. Pour cela, il suffit d'additionner les rangs en question. Par exemple, faire une rotation à droite par 5 rangs suivie d'une rotation à droite de 8 rangs est équivalent à faire une rotation à droite de 5+8 rangs, soit 13 rangs.

La logique est la même quand on enchaine des rotations à droite et à gauche. Il suffit de compter les rangs d'une rotation en les comptant positifs pour une rotation à droite et négatifs pour une rotation à gauche. Par exemple, une rotation de -5 rangs sera une rotation à gauche de 5 rangs, alors qu'une rotation de 10 rangs sera une rotation à droite de 10 rangs. On pourrait faire l'inverse, mais prenons cette convention pour l'explication qui suit. Toujours est-il qu'avec cette convention, l'addition des rangs donne le bon résultat pour la rotation équivalente. Par exemple, si je fais une rotation à droite de 15 rangs et une rotation à gauche de 6 rangs, le résultat sera une rotation de 15-6 rangs : c'est équivalent à une rotation à droite de 9 rangs.

Faisons dès maintenant remarquer quelque chose d'important. Prenons un nombre de n bits. Avec un peu de logique et quelques expériences, on remarque facilement qu'une rotation par ne fait rien, dans le sens où les bits reviennent à leur place initiale. Une rotation par est donc égale à pas de rotation du tout, ce qui est équivalent à faire une rotation par zéro rangs. Ce détail sera utilisé par la suite. Pour le moment, il nous permet de gérer le cas où l'addition de deux rangs donne un résultat supérieur à . Par exemple, prenons une rotation par 56 rangs pour un nombre de 9 bits. La division nous dit que 56 = 9*6 + 2. En clair, faire un décalage par 56 rangs est équivalent à faire 6 rotations totales par 9, suivie d'une rotation par 2 rangs. Les rotations par 9 ne comptant pas, cela revient en fait à faire une rotation par 2 rangs. Le même raisonnement fonctionne dans le cas général, et revient à faire ce qu'on appelle l'addition modulo n. C'est à dire qu'une fois le résultat de l'addition connu, on le divise par et l'on garde le reste de la division. Avec cette méthode, le nombre de rangs de la rotation équivalente est compris entre 0 et .

Les additions modulo n seront notées comme suit : .

Armé de ces explications, on peut maintenant expliquer comment fonctionne le rotateur bidirectionnel. L'idée derrière ce circuit est de remplacer une rotation à droite par une rotation équivalente. Dans ce qui suit, nous utiliserons la notation suivante : est le nombre de rangs de la rotation équivalente, la taille du nombre à décaler et le nombre de rangs du décalage initial. En soi, ce n'est pas compliqué de trouver une rotation équivalente : une rotation à droite de rangs est équivalente à une rotation de rangs, à une rotation de rangs, et de manière générale à toute rotation de rangs. La raison est que les rotations par n ne comptent pas, elles sont éliminées par la division par . Mais les propriétés des calculs modulo n font que cela marche aussi quand on retranche n. Les bizarreries de l'arithmétique modulaire font que, quand on fait les additions modulo n, on peut remplacer tout nombre positif r par sans changer les résultats. Pour résumer, on a :

L'équation précédente dit qu'il suffit d'ajouter ou de retrancher n autant de fois qu'on veut au nombre de rangs initial, pour obtenir le nombre de rangs équivalent. Mais tous les cas possibles ne nous intéressent pas. En effet, on sait que le nombre de rangs de la rotation équivalente est compris entre 0 et . Le résultat que l'on recherche doit donc être compris entre 0 et . Et seul un cas respecte cette contrainte : celui où l'on retranche n une seule fois. On a alors :

L'équation nous dit qu'il est possible de remplacer une rotation à droite par une rotation à gauche équivalente. Par exemple, sur 8 bits et pour une rotation à droite de 6 bits, on a . En clair, la rotation équivalente est ici une rotation à gauche de 2 crans. Vous pouvez essayer avec d'autres exemples, vous trouverez la même chose. Par exemple, sur 16 bits, une rotation à gauche de 3 rangs est équivalente à une rotation à droite de 13 rangs.

Le calcul ci-dessus peut être simplifié en utilisant quelques astuces. Sur la plupart des ordinateurs, n est égal à 8, 16, 32, 64, ou toute autre nombre de la forme . Les cas où n vaut 3, 7, 14 ou autres sont tellement rares que l'on peut les considérer comme anecdotiques. De plus, est compris entre 0 et . On peut donc coder le rang sur un nombre bien précis de bits, tel que n est la valeur haute de débordement (en clair, n-1 est la plus grande valeur codable, n entraine un débordement d'entier). Grâce à cela, on peut coder le nombre de rangs en complément à un ou en complément à deux. Rappelons que ces deux représentations des nombres utilisent l'arithmétique modulaire, c'est à dire que l'addition et la soustraction se font modulo n, et que leur principe est de représenter tout n négatif par un n positif équivalent. Ainsi, tout négatif est codé par un positif équivalent. Et dans ces représentations, on a obligatoirement . En appliquant cette formule dans l'équation précédente, on a :

Reprenons l'exemple d'une rotation à gauche de 2 crans pour un nombre de 8 bits, ce qui est équivalent à une rotation de 6 crans à droite: on a bien 6 = -2 en complément à deux. Reste à faire le calcul ci-dessus par le circuit de rotation.

En complément à un, le calcul de l'opposé d'un nombre consiste simplement à inverser les bits de . En conséquence, le circuit est plus simple en complément à un. Le calcul du nombre de rangs demande juste un inverseur commandable, qu'on sait fabriquer depuis quelques chapitres.

Rotateur bidirectionnel en complément à un.

En complément à deux, le calcul est le suivant :

On pourrait utiliser un circuit pour faire l'addition, mais il y a une autre manière plus simple de faire. L'idée est simplement de prendre le circuit en complément à un et d'y ajouter de quoi corriger le résultat final. En clair, on fait le calcul comme en complément à un, mais la rotation effectuée ne sera pas équivalente, du fait du +1 dans le calcul. Ce +1 indique simplement qu'il faut décaler le résultat obtenu d'un cran supplémentaire. Pour cela, on ajoute un rotateur d'un cran à la fin du circuit.

Rotateur bidirectionnel en complément à deux.

Le circuit final

[modifier | modifier le wikicode]

On peut transformer ce circuit en décaleur-rotateur en appliquant la méthode vue plus haut, à savoir en appliquant un masque en sortie du rotateur. Le circuit obtenu est le suivant :

Décaleur rotateur bidirectionnel basé sur un masque.


Tout circuit de calcul peut être conçu par les méthodes vues dans les chapitres précédents. Mais les circuits de calcul actuels manipulent des nombres de 32 ou 64 bits, ce qui demanderait des tables de vérité démesurément grandes : plus de 4 milliards de lignes en 32 bits ! Il faut donc ruser, pour créer des circuits économes en portes logiques.

Dans ce chapitre, nous allons voir les circuits capables de faire une addition ou une soustraction, ainsi que quelques circuits spécialisés, comme les additionneurs multi-opérande. Précisons cependant que les constructeurs de processeurs, ainsi que des chercheurs en arithmétique des ordinateurs, travaillent d'arrache-pied pour trouver des moyens de rendre ces circuits de calcul plus rapides et plus économes en énergie. Autant vous dire que les circuits que vous allez voir sont vraiment des circuit qui font pâle figure comparé à ce que l'on peut trouver dans un vrai processeur commercial !

Les circuits pour additionner 2 ou 3 bits

[modifier | modifier le wikicode]

Pour rappel, l'addition se fait en binaire de la même manière qu'en décimal. On additionne les chiffres/bits colonne par colonne, une éventuelle retenue est propagée à la colonne d'à côté. La soustraction fonctionne sur le même principe, sur le même modèle qu'en décimal.

Exemple d'addition en binaire.

En clair, additionner deux nombres demande de savoir additionner 2 bits et une retenue sur chaque colonne, et de propager les retenues d'une colonne à l'autre. La propagation des retenues est quelque chose de simple en apparence, mais qui est sujet à des optimisations extraordinairement nombreuses. Aussi, pour simplifier l'exposition, nous allons voir comment gérer une colonne avant de voir comment sont propagées les retenues. Le fait que les additionneurs soient organisés de manière à séparer les deux nous aidera grandement. En effet, tout additionneur est composé d'additionneurs plus simples, capables d'additionner deux ou trois bits suivant la situation. Ceux-ci gèrent ce qui se passe sur une colonne.

Le demi-additionneur

[modifier | 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. Le voici :

Demi-addtionneur.
Demi-addtionneur.
Circuit d'un demi-addtionneur.
Circuit d'un demi-addtionneur.

L'additionneur complet

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

L'additionneur complet conçu à partir de la table de vérité

[modifier | modifier le wikicode]

En étudiant la table de vérité, on remarque une chose assez intéressante. Si la retenue entrante est nulle, le bit de la somme est égal au XOR des bits d'entrée. Et inversement, il est égal au NXOR des entrées si la retenue est à 1. Un moyen pour calculer ce bit est donc d'utiliser une porte et NXOR, et de choisir la bonne valeur selon la retenue entrante. Pour faire ce choix, un simple multiplexeur fait l'affaire. Et il y a la même chose pour la retenue sortante : c'est soit un ET, soit un OU entre les bits d'entrée, selon que la retenue entrante vaut 0 ou 1. Le circuit résultant est donc celui-ci :

Additionneur complet construit à partir de MUX commandés par la retenue entrante.

Si les multiplexeurs sont implémentés avec des portes à transmission, le circuit complet fait 7 portes logiques. Intuitivement, on se dit qu'il est peut-être possible de simplifier le circuit. Par exemple, la porte XOR et la NXOR sont redondantes vu que l'une est l'inverse de l'autre. Il est possible de ne conserver que la porte XOR et d'inverser son résultat selon la valeur de la retenue. Or, nous avons déjà vu comment créer un inverseur commandable, un circuit qui inverse ou non son entrée selon ce qu'on envoie sur une entrée de commande. Un tel inverseur commandable est une simple porte XOR. En somme, le calcul de la somme se fait en enchainant deux portes XOR : une qui calcule la somme des entrées, l'autre qui inverse ou non le résultat selon la retenue entrante.

Addittionneur complet fabriqué avec des MUX, après simplification du premier MUX

Le circuit pour calculer le bit de somme ne peut pas être plus simplifié, car on ne peut pas vraiment simplifier deux portes XOR qui se suivent. Par contre, le circuit pour calculer la retenue est améliorable. Pour comprendre pourquoi, regardez le circuit obtenu en utilisant un MUX construit avec des portes logiques :

Additionneur complet basé sur un MUX

Le circuit de calcul de la retenue est assez complexe et il ressemble à un circuit simplifiable. Le calcul de la retenue sortante est un cas particulier d'un calcul qui revient souvent en électronique, comme nous allons le voir dans la section suivante. Dans ce qui va suivre, nous allons voir plusieurs implémentations alternatives pour simplifier le calcul de la retenue.

Une première solution calcule la retenue sortante avec un circuit déjà connu, appelé la porte à majorité. Pour comprendre comment, il faut remarquer quelque chose : la retenue sortante vaut 1 seulement si au moins 2 des 3 bits d'entrée sont à 1. Dit autrement, plus de la moitié des bits d'entrées doivent être à 1. Or,n il existe une porte logique qui fonctionne comme cela : elle met sa sortie à 1 si plus de moitié des entrées vaut 1, et sort un 0 sinon. Cette porte logique complexe s'appelle une porte à majorité. On obtient donc un additionneur en combinant deux portes XOR pour calculer la somme, et une porte à majorité pour la retenue sortante.

Additionneur crée avec une porte à majorité


Le circuit de calcul de la retenue peut aussi être remplacé par un multiplexeur, qui n'est pas commandé par la retenue, mais par la sortie de la première porte XOR. Et il peut être remplacé par n'importe quel multiplexeur, qu'il soit construit avec des portes logiques, des portes à transmission, ou autre.

Additionneur crée avec un multiplexeur

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

[modifier | modifier le wikicode]

Une 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.
Additionneur basé sur des MUX.

La retenue sortante est calculée avec un circuit à trois portes logiques. Et pour information, ce circuit peut se dériver à partir de l'additionneur basé sur des MUX vu plus haut.

Pour comprendre pourquoi regardons les équations de ce circuit :

Pour simplifier le second terme, l'idéal serait de trouver un terme de la forme . Et il se trouve que ce terme se trouve de manière implicite dans le premier terme. En effet, on a :

En combinant les deux équations précédentes, on a :

Les deux derniers termes se simplifient en factorisant (A . B), ce qui donne :

Le résultat est bien le circuit vu plus haut.

Les autres versions de l'additionneur complet que nous allons voir sont des dérivés de ce circuit, auquel on a appliqué quelques simplifications. Les simplifications portent surtout sur le circuit de calcul de la retenue.

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

[modifier | modifier le wikicode]

Une autre solution calcule la retenue finale d'une autre manière, en combinant le résultat de deux circuits séparés. Le premier vérifie si l'addition génère une retenue, l'autre si la retenue en entrée est propagée en sortie.

  • Une retenue est dite générée si l'addition donne une retenue, quelle que soit la retenue envoyée en entrée (sous-entendu, même si celle-ci vaut 0). Cela arrive quand les bits additionnés valent tous deux 1 : la retenue sera alors de 1, seul le bit du résultat changera. On peut donc calculer si une retenue est générée en faisant un ET entre les deux bits d'entrée.
  • Une retenue est propagée si la retenue en sortie est égale à la retenue en entrée. En clair, la retenue n'existe que si on envoie une retenue en entrée. Dans ce cas, la retenue finale vaut 1 quand un seul des deux bits d'entrée vaut 1, et vaut 0 sinon. En clair, on peut déterminer si une retenue est propagée en faisant un XOR entre les deux bits d'entrée.

Vous remarquerez que les signaux P et G sont calculés par le premier demi-additionneur.

Ces deux circuits fournissent deux signaux : un signal G qui indique si une retenue est générée, et un signal P qui indique si une retenue est propagée. En combinant les deux, on peut calculer la retenue finale. Elle vaut 1 soit quand la retenue est générée, soit quand la retenue d'entrée vaut 1 et qu'elle est propagée. Dans les autres cas, elle vaut zéro. La traduction en équation logique dit qu'il suffit de faire un ET entre la retenue d'entrée et le bit P, puis de faire un OU avec le bit G. Le circuit obtenu est strictement identique au circuit précédent.

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

L'additionneur en Manchester carry chain est une modification de l'additionneur précédent, où les portes logiques en orange dans le schéma précédent sont remplacées par un circuit à base de portes à transmission. L'idée est que la retenue de sortie vaut : soit la retenue générée, soit la retenue à propager. Il suffit donc d'ajouter deux portes à transmission : une pour connecter la retenue générée sur la sortie, une autre pour propager/connecter la retenue d'entrée sur la sortie. L'activation de chaque porte à transmission est assez simple : il dépend du signal de propagation généré par la porte XOR en vert. La première porte à transmission est activée quand il est à 1, désactivée pour un 0, l'autre porte fait l'exact inverse. Une simple porte NON suffit.

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

Manchester carry chain

Il existe des additionneurs qui fournissent les signaux P et G en plus du résultat et de la retenue sortante. Mais il en existe qui ne calculent pas la retenue finale. Par contre, ils calculent les signaux P et G qui disent si l'addition de deux bits génère une retenue, ou si elle propage une retenue provenant d'une colonne précédente. Ils fournissent ces deux signaux sur deux sorties P et G pour indiquer s'il y a propagation et génération de retenue. Un tel additionneur est appelé un additionneur P/G (P/G pour propagation/génération). Ils seront très utiles pour créer des circuits additionneurs comme on le verra plus bas.

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

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

[modifier | 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. Voyons maintenant une autre méthode, utilisée dans l'unité de calcul de l'Intel 4004 et de l'Intel 8008, fonctionnait autrement. Avec elle, la retenue sortante et calculée en premier, puis on détermine le bit du résultat à partir de la retenue sortante. En effet, le bit du résultat est l'inverse de la retenue sortante, sauf dans deux cas : les trois bits d'entrée sont à 0, où ils sont tous à 1. Dans ces deux cas, le bit du résultat vaut 0, quelque soit la retenue sortante.

L'implémentation de cette idée en circuit est assez simple. Au circuit de calcul de la retenue sortante, il faut ajouter un circuit qui vérifie si tous les bits opérande valent 0, un autre s'ils valent tous 1. Leur sortie vaut 1 si c'est le cas. Le premier est une simple porte ET, l'autre une porte NOR. Ensuite, on combine le résultat des trois circuits précédents pour obtenir le résultat final. Si un seul des trois circuits a sa sortie à 1, alors la sortie finale doit être à 0. Elle est à 1 sinon. C'est donc une porte NOR qu'il faut utiliser. 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

A 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 des techniques de fabrication différentes anciennes. Les processeurs de l'époque utilisaient la technologie TTL, et non la technologie CMOS des processeurs 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.

Les autres implémentations

[modifier | modifier le wikicode]
Additionneur complet fabriqué avec 24 transistors.

Les implémentations précédentes utilisent des portes logiques, ce qui est simple et facile à comprendre, mais pas forcément le top du top en termes de performances. Mais les additionneurs des processeurs modernes sont nettement plus optimisés que ça. Et pour cela, ils sont optimisés directement au niveau des transistors. Cela permet de les rendre plus rapides, notamment au niveau du calcul de la retenue. Là où l'addition des deux bits d'opérande se doit d'être rapide, le calcul de la retenue doit absolument être le plus rapide possible, c'est crucial pour les circuits qui vont suivre.

Outre les optimisations en termes de rapidité, une implémentation à base de transistors peut économiser des transistors. Tout dépend de l'objectif visé, certains circuit optimisant à fond pour la vitesse, d'autres pour le nombre de transistors, d'autres font un compromis entre les deux. Les circuits de ce genre sont très nombreux, trop pour qu'on puisse les citer.

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 (et donc les nombres codés en complètement à deux, ou en complément à un).

L'additionneur série

[modifier | modifier le wikicode]

Avec un additionneur complet, il est possible d'additionner deux nombres bit par bit. Dit autrement, on peut effectuer l'addition colonne par colonne. Cela demande de coupler l'additionneur avec plusieurs registres à décalages. Les opérandes vont être placées chacune dans un registre à décalage, afin de passer à chaque cycle d'un bit au suivant, d'une colonne à la suivante. Même chose pour le résultat. La retenue de l'addition est stockée dans une bascule de 1 bit, en attente du prochain cycle d'horloge. Un tel additionneur est appelé un additionneur série.

Additionneur série.

Bien que très simple et économe en transistors, cet additionneur est cependant peu performant. Le temps de calcul est proportionnel à la taille des opérandes. Par exemple, additionner deux nombres de 32 bits prendra deux fois plus de temps que l'addition de deux nombres de 16 bits. L'addition étant une opération fréquente, il vaut mieux utiliser d'autres méthodes d'addition, plus rapides. C'est pour cela que la totalité des autres additionneurs préfère utiliser plus de circuits, quitte à gagner quelque peu en rapidité.

L'additionneur à propagation de retenue

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

Additionneur à propagation de retenue.

Notons la présence de la retenue sortante, qui est utilisée pour détecter les débordements d'entier, ainsi que pour d'autres opérations. Le bit de retenue final est souvent stocké dans un registre spécial du processeur (généralement appelé carry flag).

Notez aussi, sur le schéma précédent, la présence de l’entrée de retenue sur l'additionneur. L'additionneur le plus à droite est bien un additionneur complet, et non un demi-additionneur,c e qui fait qui l'additionneur a une entrée de retenue. Tous les additionneurs ont une entrée de retenue de ce type. Elle est très utile pour l'implémentation de certaines opérations comme l'inversion de signe, la soustraction, l'incrémentation, etc. Certains processeurs sont capables de faire une opération appelée ADC, ADDC ou autre nom signifiant Addition with Carry, qui permet de faire le calcul A + B + Retenue (la retenue en question est la retenue sortante de l'addition précédente, stockée dans le registre carry flag). Son utilité principale est de permettre des additions d'entiers plus grands que ceux supportés par le processeur. Par exemple, cela permet de faire des additions d'entiers 32 bits sur un processeur 16 bits.

Propagation de retenue dans l'additionneur.

Ce circuit utilise plus de portes logiques que l'additionneur série, mais est un peu plus rapide. Il utilise très peu de portes logiques et est assez économe en transistors, ce qui fait qu'il était utilisé sur les tous premiers processeurs 8 et 16 bits. Un défaut de ce circuit est que le calcul des retenues s'effectue en série, l'une après l'autre. En effet, chaque additionneur doit attendre que la retenue de l'addition précédente soit disponible pour donner son résultat. Les retenues doivent se propager à travers le circuit, du premier additionneur jusqu'au dernier. On garde donc un défaut de l'additionneur série, à savoir : le fait que le temps de calcul est proportionnel à la taille des opérandes. Pour éviter cela, les autres additionneurs utilisent diverses solutions : soit calculer les retenues en parallèle, soit éliminer certaines opérations inutiles quand c'est possible.

Une solution est d'utiliser des additionneurs complets de type Manchester carry chain, qui propagent la retenue très rapidement. Néanmoins, l'additionneur de type Manchester carry chain connecte directement l'entrée de retenue sur la sortie de retenue, via un interrupteur, sans passage par une porte logique. Donc, la tension d'entrée est envoyée directement sur la sortie, sans amplification ou régénération. Si on enchaine plusieurs additionneurs complets de ce type, la tension diminue à chaque passage dans un additionneur complet. En conséquence, on peut difficilement enchainer plus de 4 à 8 additionneurs de type Manchester carry chain à la suite.

Il reste alors à trouver d'autres solutions pour résoudre ce problème de la propagation de retenue. Pour cela, il existe globalement plusieurs solutions, qui donnent quatre types d'additionneurs. Pour résumer ces solutions, en voici une liste rapide :

  • Détecter quand le résultat est disponible, plutôt que d'attendre suffisamment pour couvrir le pire des cas.
  • Limiter la propagation des retenues sur un petit nombre de bits et concevoir l'additionneur avec cette contrainte.
  • Accélérer le calcul de la retenue avec des techniques d'anticipation de retenue.
  • Utiliser des représentations binaires qui permettent de se passer de la propagation de retenue.

Les trois premières méthodes donnent, respectivement, les additionneurs à saut de retenue, à sélection de retenue, et à anticipation de retenue. Nous allons les voir dans les sections suivantes. La quatrième méthode sera vue dans le chapitre suivant, qui abordera l'addition multiopérande.

Les accélérations de la propagation de retenue

[modifier | modifier le wikicode]

La propagation de la retenue est lente, mais il existe de nombreux moyens de la rendre plus rapide. Dans cette section, nous allons voir quelques additionneurs qui visent à accélérer la propagation de la retenue, mais en gardant la base de l'additionneur de propagation de retenue.

Additionneur 4 bits, un bloc.

Avant de poursuivre, partons du principe que l'additionneur est conçu en assemblant des additionneurs à plus simples, qui additionnent environ 4 à 5 bits, parfois plus, parfois moins. Ces additionneurs simples seront nommés blocs dans ce qui suit, et l'un d'entre eux est illustré ci-contre. Chaque bloc prend en entrée les deux opérandes à additionner, mais aussi une retenue d'entrée. Il fournit en sortie non seulement le résultat de l'addition, codé sur 4/5 bits, mais aussi une retenue sortante.

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

Les blocs sont tous identiques dans le cas le plus simple, mais il est possible d'utiliser des blocs de taille variable. Par exemple, le premier bloc peut avoir des opérandes de 6 bits, le second des opérandes de 7 bits, etc. Faire ainsi permet de gagner un petit peu en performances, si la taille de chaque bloc est bien choisie. La raison est une question de temps de propagation des retenues. La retenue met plus de temps à se propager à travers 8 blocs qu'à travers 4, ce qui prend plus de temps qu'à travers 2 blocs, etc. En tenir compte fait que la taille des blocs tend à augmenter ou diminuer quand on se rapproche des bits de poids fort.

Le calcul parallèle de la retenue

[modifier | 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é de l'additionneur à propagation de retenue, on trouve un circuit qui calcule la retenue sortante. Il existe de nombreuses manières de calculer la retenue sortante. La plus simple consiste à établir la table de vérité de l'entrée de retenue, puis d'utiliser les techniques du chapitre sur les circuits combinatoire. Cela marche si les blocs sont de petite taille, mais elle devient difficile si le bloc a des opérandes de 2/3 bits ou plus. Mais des techniques alternatives existent.

Un exemple est celui de l'additionneur CMOS 4008, un additionneur de 4 bit. Il est intéressant de voir comment fonctionne ce circuit. Aussi, voici son implémentation. Le circuit est décomposé en trois sections. Une première couche de demi-additionneurs, le circuit de calcul de la retenue sortante, le reste du circuit qui finit l'addition. Le reste du circuit fait que le calcul de l'addition se fait en propageant la retenue, ce qui en fait un additionneur à propagation de retenue. Le circuit de calcul de la retenue sortante prend les résultats des demi-additionneurs, et les utilise pour calculer la retenue sortante. C'est là une constante de tous les circuits qui vont suivre.

CMOS 4008, circuit découpé en sections

Le point important à comprendre est que les demi-additionneurs génèrent les signaux P et G, qui disent si l'additionneur propage ou génère une retenue. Ces signaux sont alors combinés pour déterminer la retenue sortante. La méthode de combinaison des signaux P et G dépend fortement de l'additionneur utilisé. La méthode utilisée sur le 4008 utilise à la fois les signaux P et G, ce qui fait que c'est un hybride entre un additionneur à propagation de retenue, et un additionneur à anticipation de retenue qui sera vu dans la suite du chapitre. Mais il existe des techniques alternatives pour calculer la retenue sortante. La plus simple d'entre elle n'utilise que les signaux de propagation P, sans les signaux de génération. Et nous allons les voir immédiatement.

L'additionneur à saut de retenue

[modifier | modifier le wikicode]

L'additionneur à saut de retenue (carry-skip adder) est un additionneur dont le temps de calcul est variable, qui dépend des nombres à additionner. Le calcul prendra quelques cycles d'horloges avec certains opérandes, tandis qu'il sera aussi long qu'avec un additionneur à propagation de retenue avec d'autres. Il n'améliore pas le pire des cas, dans lequel la retenue doit être propagée du début à la fin, du bit de poids faible au bit de poids fort. Dans tous les autres cas, le circuit détecte quand le résultat de l'addition est disponible, quand la retenue a fini de se propager. Il permet d'avoir le résultat en avance, plutôt que d'attendre suffisamment pour couvrir le pire des cas.

L'additionneur à saut de retenue peut, sous certaines conditions, sauter complètement la propagation de la retenue dans le bloc. L'idée est de savoir si, dans le bloc, une retenue est générée par l'addition, ou simplement propagée. Dans le second cas, le bloc ne fait que propager la retenue entrante, sans en générer. La retenue entrante est simplement recopiée sur la retenue sortante. La propagation de retenue dans le bloc est alors skippée (mais elle a quand même lieu). Si une retenue est générée dans le bloc, on envoie cette retenue sur la retenue sortante. Le choix entre les deux est le fait s'un multiplexeur.

Carry skip adder : principe de base

Toute la difficulté est de savoir comment commander le multiplexeur. Pour cela, on doit savoir si le circuit propage une retenue ou non. Le bloc propage une retenue si chaque additionneur complet propage la retenue. Les additionneurs complets doivent donc fournir le résultat, mais aussi indiquer s'ils propagent la retenue d'entrée ou non. Le signal de commande du multiplexeur est généré assez simplement : il vaut 1 si tous les additionneurs complets du bloc propagent la retenue précédente. C'est donc un vulgaire ET entre tous ces signaux.

Calcul de la commande du MUX.

L'additionneur à saut de retenue est construit en assemblant plusieurs blocs de ce type.

Additionneur à saut de retenue.

L'additionneur à sélection de retenue

[modifier | modifier le wikicode]

L'additionneur à sélection de retenue découper les opérandes en blocs, qui sont additionnés indépendamment. L'addition se fait en deux versions : une avec la retenue du bloc précédent valant zéro, et une autre version avec la retenue du bloc précédent valant 1. Il suffira alors de choisir le bon résultat avec un multiplexeur, une fois cette retenue connue. On gagne ainsi du temps en calculant à l'avance les valeurs de certains bits du résultat, sans connaître la valeur de la retenue. Petit détail : sur certains additionneurs à sélection de retenue, les blocs de base n'ont pas la même taille. Cela permet de tenir compte des temps de propagation des retenues entre les blocs.

Additionneur à sélection de retenue avec seulement deux blocs.

Dans les exemples du dessus, chaque sous-additionneur étaient des additionneurs à propagation de retenue. Mais ce n'est pas une obligation, et tout autre type d’additionneur peut être utilisé. Par exemple, on peut faire en sorte que les sous-additionneurs soient eux-mêmes des additionneurs à sélection de retenue, et poursuivre ainsi de suite, récursivement. On obtient alors un additionneur à somme conditionnelle, plus rapide que l'additionneur à sélection de retenue, mais qui utilise beaucoup plus de portes logiques.

Les additionneurs à anticipation de retenue

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

Demi-soustracteur.

Celui-ci peut être complété afin de prendre en compte une éventuelle retenue, ce qui donne un soustracteur complet. On remarque que le soustracteur complet et composé de deux demi-soustracteurs placés en série. Le calcul de la retenue se fait en combinant les deux retenues des demi-soustracteurs avec une porte OU.

Soustracteur complet.

Celui-ci permet de créer des soustracteurs sur le même patron que pour les additionneurs. On peut ainsi créer un soustracteur série, un soustracteur à propagation de retenue, et ainsi de suite.

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

[modifier | modifier le wikicode]

Étudions maintenant le cas de la soustraction en complément à deux, dans l'objectif de créer un circuit soustracteur. Vous savez sûrement que a−b et a+(−b) sont deux expressions équivalentes. Et en complément à deux, − b = not(b) + 1. Dit autrement, a − b = a + not(b) + 1. On pourrait se dire qu'il faut deux additionneurs pour faire le calcul, mais la majorité des additionneurs possède une entrée de retenue pour incrémenter le résultat de l'addition. Un soustracteur en complément à deux est donc simplement composé d'un additionneur et d'un inverseur.

Soustracteur en complément à deux.

Il est possible de créer un circuit capable d'effectuer soit une addition, soit une soustraction : il suffit de remplacer l'inverseur par un inverseur commandable, qui peut être désactivé. On a vu comment créer un tel inverseur commandable dans le chapitre sur les circuits combinatoires. On peut remarquer que l'entrée de retenue et l'entrée de commande de l'inverseur sont activées en même temps : on peut fusionner les deux signaux en un seul.

Additionneur-soustracteur en complément à deux.

Une implémentation alternative est la suivante. Elle remplace l'inverseur commandable par un multiplexeur.

Additionneur-soustracteur en complément à deux, version alternative.

L'additionneur-soustracteur pour opérandes codées en signe-magnitude

[modifier | modifier le wikicode]

Passons maintenant aux nombres codés en signe-valeur absolue.

Étudions tout d'abord un circuit d'addition de deux opérandes en signe-magnitude, les deux opérandes étant notée A et B. Suivant les signes des deux opérandes, on a quatre cas possibles : A + B, A − B (B négatif), −A + B (A négatif) et −A − B (A et B négatifs). On remarque que B − A est égal à − (A − B), et − A − B vaut − (A + B). Ainsi, le circuit n'a besoin que de calculer A + B et A − B : il peut les inverser pour obtenir − A − B ou B − A. A + B et A − B peuvent se calculer avec un additionneur-soustracteur. Il suffit de lui ajouter un inverseur commandable pour obtenir le circuit d'addition finale. On peut transformer ce circuit en additionneur-soustracteur en signe-valeur absolue, mais le circuit combinatoire devient plus complexe.

Additionneur en signe-valeur absolue.

Toute la difficulté tient dans le calcul du bit de signe du résultat, quand interviennent des soustractions. Autant l'addition de deux nombres de même signe ne pose aucun problème, autant l'addition de nombres de signes différents ou les soustractions posent problème. Suivant que ou que , le signe du résultat ne sera pas le même. Intuitivement, on se dit qu'il faut ajouter des comparateurs pour déterminer le signe du résultat. Diverses optimisations permettent cependant de limiter la casse et d'utiliser moins de circuits que prévu. Mais rien d'extraordinaire.

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

[modifier | 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'incrémenteur

[modifier | modifier le wikicode]

Maintenant, nous allons voir un circuit capable d'incrémenter un nombre, appelé l'incrémenteur. Les circuits incrémenteurs étaient très utilisés sur les premiers processeurs 8 bits, comme le Z-80, le 6502, les premiers processeurs x86 comme le 8008, le 8086, le 8085, et bien d'autres. Il s'agit d'un circuit assez simple, mais qu'il peut être intéressant d'étudier.

Le circuit incrémenteur se construit sur la même base qu'un additionneur, qu'on simplifie. En effet, incrémenter un nombre A revient à calculer A + 1. En clair, l'opération effectuée est la suivante :

           
+  0  0  0  0  0  0  0  1
------------------------------

Le calcul alors très simple : il suffit d'additionner 1 au bit de poids faible, sur la colonne la plus à droite, et propager les retenues pour les autres colonnes. En clair, on n'additionne que deux bits à chaque colonne : un 1 sur celle tout à droite, la retenue de la colonne précédente pour les autres. En clair : un incrémenteur est juste un additionneur normal, dont on a remplacé les additionneurs complets par des demi-additionneurs. Le 1 le plus à droite est injecté sur l'entrée de retenue entrante de l'additionneur. Et cela marche avec tous les types d'additionneurs, que ce soit des additionneurs à propagation de retenue, à anticipation de retenue, etc.

L'incrémenteur à propagation de retenue

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

L'incrémenteur à propagation de retenue était utilisé sur le processeur Intel 8085, avec cependant une optimisation très intéressante. Pour la comprendre, rappelons que les portes logiques sont construites à partir de transistors. Les portes les plus simples à implémenter avec des transistors CMOS et TTL sont les portes NON, NAND et NOR. Le demi-additionneur est donc construit comme ci-dessous

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

Les ingénieurs ont tenté de se débarrasser de la porte NON, et ont réussi à s'en débarrasser pour une colonne sur deux. L'idée est de prendre les demi-additionneurs deux par deux, par paires. On peut alors regrouper les portes logiques comme ceci :

Brique de base de l'incrémenteur du 8085

Les trois portes sont fusionnées, de manière à donner une porte NOR couplée à une porte NON.

Brique de base de l'incrémenteur du 8085 - les portes en jaune sont faites avec un seul transistor
On peut optimiser le tout en fusionnant la porte XOR avec la porte NON pour le calcul de la somme, la porte XOR étant une porte composite. Mais nous n'en parlerons pas plus que ça ici.

Le résultat est que la propagation de la retenue est plus rapide. Au lieu de passer par une porte NAND et une porte NON, il traverse une seule porte : une porte NAND pour les colonnes paires, une porte NOR pour les colonnes impaires. Avec cette optimisation, la retenue se propage presque deux fois plus vite. Mine de rien, cette optimisation économisait des portes logiques et rendait le circuit deux fois plus rapide.

Les incrémenteurs plus complexes sont rares

[modifier | modifier le wikicode]

Pour résumer, ce circuit ne paye pas de mine, mais il était largement suffisant sur les premiers microprocesseurs. Ils utilisaient généralement un incrémenteur capable de traiter des nombres de 8 bits, guère plus. Ces processeurs étaient très peu puissants, et fonctionnaient à une fréquence très faible. Ainsi, ils n'avaient pas besoin d'utiliser de circuits plus complexes pour incrémenter un nombre, et se contentaient d'un incrémenteur à propagation de retenues.

Il existe cependant des processeurs qui utilisaient des incrémenteurs complexes, avec anticipation de retenues, voir du carry skip. Par exemple, le processeur Z-80 de Zilog utilisait un incrémenteur pour des nombres de 16 bits, ce qui demandait des performances assez élevées. Et cet incrémenteur utilisait à la fois anticipation de retenues et carry skip. Pour ceux qui veulent en savoir plus sur cet incrémenteur, voici un lien sur le sujet :

L'additionneur BCD

[modifier | 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. Il fournit un résultat sur 4 bits et une retenue qui est mise à 1 si le résultat dépasse 10 (la limite d'un chiffre BCD). L'additionneur BCD se base sur un additionneur normal, pour des entiers codés en binaire, auquel on ajoute des circuits pour gérer le format BCD. Les deux chiffres sont codés sur 4 bits et sont additionnés en binaire par un additionneur des plus normal, similaire à ceux vus plus haut. Le résultat est alors un entier codé en binaire, sur 5 bits, qu'on cherche à corriger/convertir pour obtenir un chiffre BCD.

Pour corriger le résultat, une idée intuitive serait de prendre le résultat et de faire une division par 10. Le quotient donne la retenue, alors que le reste est le résultat, le chiffre BCD . Mais faire ainsi prendrait beaucoup de circuits, ce qui ne vaut pas le coup. Il existe une autre méthode beaucoup plus simple.

Une autre méthode détecte si le résultat est égal ou supérieur à 10, ce qui correspond à un "débordement" (on dépasse les limites d'un chiffre BCD). Si le résultat est plus petit que 10, il n'y a rien à faire : le résultat est bon et la retenue est de zéro. Par contre, si le résultat vaut 10 ou plus, il faut corriger le résultat et générer une retenue à 1.

Il faut donc ajouter un circuit qui détecte si le résultat est supérieur à 9. La retenue s'obtient facilement : le circuit qui détecte si le résultat est supérieur à 9 donne directement la retenue. Ce circuit peut se fabriquer simplement à partir de sa table de vérité, ou en utilisant les techniques que nous verrons dans un chapitre ultérieur sur les comparateurs. La solution la plus simple est clairement d'utiliser la table de vérité, ce qui est très simple, assez pour être laissé en exercice au lecteur.

Pour comprendre comment corriger le résultat, établissons une table de vérité qui associe le résultat et le résultat corrigé. L'entrée vaut au minimum 10 et au maximum 9 + 9 = 18. On considère la sortie comme un tout, la retenue étant un 5ème bit, le bit de poids fort.

Entrée Retenue Résultat corrigé (sans retenue) interprétation de la sortie en binaire (retenue inclue)
0 1 0 1 0 (10) 1 0000 (16)
0 1 0 1 1 (11) 1 0001 (17)
0 1 1 0 0 (12) 1 0010 (18)
0 1 1 0 1 (13) 1 0011 (19)
0 1 1 1 0 (14) 1 0100 (20)
0 1 1 1 1 (15) 1 0101 (21)
1 0 0 0 0 (16) 1 0110 (22)
1 0 0 0 1 (17) 1 0111 (23)
1 0 0 1 0 (18) 1 1000 (24)

En analysant le tableau, on voit que pour corriger le résultat, il suffit d'ajouter 6. La raison est que le résultat déborde d'un nibble à 16 en binaire, mais à 10 en décimal : il suffit d'ajouter la différence entre les deux, à savoir 6, et le débordement binaire fait son travail. Donc, la correction après une addition est très simple : si le résultat dépasse 9, on ajoute 6.

On peut maintenant implémenter l'additionneur BCD au complet, en combinant le comparateur de débordement, le circuit de correction, et l'additionneur. La première solution calcule deux versions du résultat : la version corrigée, la version normale. Le choxi entre les deux est réalisée par un multiplexeur, commandé par le comparateur.

Additionneur BCD

L'autre solution utilise un circuit commandable qui soit additionne 6, soit ne fait rien. Le choix entre les deux est commandé par un bit de commande dédié, calculé par le comparateur.

Additionneur BCD, seconde version.

Une version assez compliquée du circuit final est illustrée ci-dessous. Le circuit commandable est un additionneur, précédé d'un circuit comparateur qui détecte si le résultat est supérieur à 9. Si c'est le cas, ce circuit génère l'opérande 6, et l'envoie en entrée de l'additionneur. Le circuit est simple à concevoir, mais gaspille beaucoup de circuit. Idéalement, il vaudrait mieux utiliser un circuit combinatoire d'addition avec une constante conçu pour.

Additionneur BCD, circuit complet.

Pour obtenir un additionneur BCD complet, il suffit d’enchaîner les additionneurs précédents, comme on le ferait avec les additionneurs complets dans un additionneur à propagation de retenue. La seule chose importante est que le circuit précédent est altéré de manière à ce qu'on puisse prendre en compte la retenue. Pour cela, rien de plus simple : il suffit d'utiliser l'entrée de retenue de l'additionneur binaire.

Au final, l'additionneur BCD est beaucoup plus compliqué qu'un additionneur normal. Il rajoute des circuits à un additionneur normal, à savoir un circuit pour détecter si chaque chiffre binaire est >9, un petit additionneur pour ajouter 6 et un multiplexeur. De plus, il est difficile d'appliquer les optimisations disponibles sur les additionneurs non-BCD. Notamment, les circuits d'anticipation de retenue sont totalement à refaire et le résultat est relativement compliqué. c'est ce qui explique pourquoi le BCD a progressivement été abandonné au profit du binaire simple.

L'additionneur BCD par ajustement décimal

[modifier | modifier le wikicode]

L'additionneur BCD précédent effectuait son travail chiffre BCD par chiffre BCD. Il additionne et corrige le résultat un chiffre BCD après l'autre, en commençant par le chiffre BCD de poids faible. Mais il existe des additionneurs BCD qui font autrement. L'idée est d’additionner les deux opérandes avec une addition binaire normale, puis de corriger le résultat. Le tout se fait donc en deux étapes : l'addition, puis la correction du résultat. Les deux étapes traitent des opérandes complètes, de 8, 16, voire 32 bits, et non des chiffres BCD seuls.

Une telle technique était utilisée dans le processeur Intel 8085, et de manière générale sur les premiers processeurs x86. Sur ce processeur, les deux étapes d'addition et de correction du résultat étaient séparées dans deux opérations distinctes. Il n'y avait pas d'opération d'addition BCD proprement dit, seulement une addition binaire normale. Par contre, l'addition était secondée par une opération dite d'ajustement décimal qui transformait un nombre binaire en nombre codé en BCD. Effectuer une addition BCD demandait donc de faire deux opérations à la suite : une addition binaire simple, suivie par l'opération d'ajustement décimal. Cela permettait de gérer des nombres entiers en binaire usuel et des entiers BCD sans avoir deux instructions d'addition séparées pour les deux, sans compter que cela simplifiait aussi les circuits d'addition. L'opération d'ajustement décimal lisait l'opérande à manipuler dans un registre (l’accumulateur), et mémorisait le résultat dans ce même registre. Elle prenait une opérande de 8 bits, soit deux chiffres BCD, et fournissait un résultat de la même taille. Elle avait son propre circuit, assez simple, que nous allons voir dans ce qui suit.

L'ajustement décimal s'effectue en ajoutant une constante bien précise à l'opérande à convertir en BCD. L'idée est que la constante est découpée en morceaux de 4 bits, correspondant chacun à un chiffre BCD de l'opérande, chaque morceau contenant soit un 0, soit 6. Cela permet d'ajouter soit 0, soit, à chaque chiffre BCD, et donc de le corriger. La propagation des retenues d'un chiffre à l'autre est effectuée automatiquement par l'addition binaire de la constante. La constante est calculée en deux étapes, sur un principe similaire à celui vu dans l'additionneur précédent. D'abord, on découpe l'opérande en nibbles et on vérifie si chaque nibble est supérieur ou égal à 10. Ensuite, la seconde étape rend les résultats de ces comparaisons et détermine la valeur de chaque nibble de la constante finale. Par exemple, si je prends l'opérande 1001 1110, le nibble de poids faible déborde, alors que celui de poids fort non. La constante sera donc 0000 0110 : 0x06. Inversement, si le nibble de poids fort déborde et pas celui de poids faible, la constante sera alors 0x60. Et la constante est de 0x66 si les deux nibbles débordent, de 0x00 si aucun ne déborde.

Le circuit d’ajustement décimal est donc composé de trois étapes : deux étapes pour calculer la constante, et un circuit d'addition pour additionner cette constante au nombre de départ. La première étape découpe l'opérande en morceaux de 4 bits, en chiffres BCD, et vérifie si chacun d'entre eux vaut 10 ou plus. La seconde étape prend les résultats de la première étape, et les combine pour calculer la constante. Enfin, on trouve l'addition finale, qui était réalisée par un circuit d'addition utilisé à la fois pour l'ajustement décimal et l'addition binaire. La différence entre une addition normale et une opération d'ajustement décimal tient dans le fait que les deux premières étapes sont désactivées dans une addition normale.

Additionneur BCD parallèle

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

[modifier | modifier le wikicode]

Les instructions arithmétiques et quelques autres manipulent des entiers de taille fixe, qui ne peuvent prendre leurs valeurs que dans un intervalle. Pour les nombres positifs, un ordinateur qui code ses entiers sur n bits pourra coder tous les entiers allant de 0 à . Tout nombre en dehors de cet intervalle ne peut pas être représenté. Dans le cas où l'ordinateur gère les nombres négatifs, l'intervalle est différent. Dans le cas général, l'ordinateur peut coder les valeurs comprises de à . Si le résultat d'un calcul sort de cet intervalle, il ne peut pas être représenté par l'ordinateur et il se produit ce qu'on appelle un débordement d'entier.

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

La correction des débordements d'entier : l'arithmétique saturée

[modifier | 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 par le circuit, celui-ci ne conserve que les bits de poids faibles du résultat. Les bits en trop sont simplement ignorés. On dit qu'on utilise l'arithmétique modulaire. Le problème avec ce genre d'arithmétique, c'est qu'une opération entre deux grands nombres peut donner un résultat très petit. Par exemple, si je dispose de registres 4 bits et que je souhaite faire l'addition 1111 + 0010 (ce qui donne 15 + 2), le résultat est censé être 10001 (17), ce qui est un résultat plus grand que la taille d'un registre. En conservant les 4 bits de poids faible, j’obtiens 0001 (1). En clair, un résultat très grand est transformé en un résultat très petit. Cela peut poser problèmes si on travaille uniquement avec des nombres positifs, mais c'est aussi utilisé pour coder des nombres en complément à deux. En clair, faire ainsi est parfois une bonne idée, parfois non.

D'autres circuits utilisent ce qu'on appelle l'arithmétique saturée : si un résultat est trop grand au point de générer un débordement, on arrondi le résultat au plus grand entier supporté par le circuit. Les circuits capables de calculer en arithmétique saturée sont un peu tout petit peu plus complexes que leurs collègues qui ne travaillent pas en arithmétique saturée, vu qu'il faut rajouter des circuits pour corriger le résultat en cas de débordement. Il suffit généralement de rajouter un circuit de saturation, qui prend en entrée le résultat en fournit en sortie une version saturée en cas de débordement. Ce circuit de saturation met la valeur maximale en sortie si un débordement survient, mais se contente de recopier le résultat du calcul sur sa sortie s'il n'y a pas de débordement. Typiquement, il est composé d'une ou de deux couches de multiplexeurs, qui sélectionnent quelle valeur mettre sur la sortie : soit le résultat du calcul, soit le plus grand nombre entier géré par le processeur, soit le plus petit (pour les nombres négatifs/soustractions).

L'arithmétique saturée est surtout utilisée pour les additions et soustractions, mais c'est plus rare pour les multiplications/divisions. Une des raisons est que le résultat d'une addition/soustraction prend un bit de plus que le résultat, là où les multiplications doublent le nombre de bits. Cette large différence se traduit par une grande différence pour les résultats qui débordent. Quand une addition déborde, le résultat réel est proche de la valeur maximale codable. mais quand une multiplication déborde, le résultat peut parfois valoir 200 à 60000 fois plus que la valeur maximale codable. Les calculs avec une valeur saturée/corrigée sont donc crédibles pour une suite d'additions, mais pas pour une suite de multiplications. Raison pour laquelle l'arithmétique saturée est utilisée pour les additions/soustractions, là où on préfère corriger les multiplications/divisions par des méthodes logicielles.

La détection des débordements avec des entiers non-signés

[modifier | modifier le wikicode]

Et quand un débordement d'entier a eu lieu, il vaut mieux que le circuit prévienne ! Pour cela, les circuits de calculs ont une sortie nommée Overflow, dont la valeur indique si le calcul a donné un débordement d'entier ou non. Reste que détecter un débordement ne se fait pas de la même manière selon que l'on parle d'un additionneur non-signé, d'un additionneur signé, d'un multiplieur non-signé, etc.

Pour le cas des nombres positifs, la détection des débordements dépend assez peu de l'opération. L'idée générale est que le circuit de calcul calcule tous les bits du résultat, quitte à dépasser ce qui est supporté par l'ordinateur. Par exemple, un additionneur 32 bits fournit un résultat sur 33 bits, un multiplieur 32 bits fournit des résultats sur 64 bits, etc. Le circuit de calcul a donc des bits qui sont en trop et doivent être oubliés. Un débordement a lieu quand ces bits oubliés sont pertinents, à savoir quand au moins l'un d'eux est à 1. Par exemple, une addition sur 32 bits déborde quand le 33ème bit est à 1, une multiplication sur 32 bits déborde quand un des 32 bits de poids fort est à 1, etc.

Pour les additionneurs non-signés, la sortie Overflow n'est autre que la retenue finale, celle fournie par le dernier additionneur complet. De plus, le seul type de débordement possible est un débordement par le haut, où le résultat dépasse la valeur maximale. Le circuit de saturation est alors très simple. Il consiste au pire en une seule couche de multiplexeurs. Une solution encore plus simple consiste à utiliser le circuit de mise à la valeur maximale vu dans le chapitre sur les opérations bits à bits.

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

La détection des débordements avec des entiers signés

[modifier | modifier le wikicode]

Pour les additionneurs non-signés, la gestion des débordements d'entiers dépend fortement de la représentation signée. Dans les grandes lignes, rien ne change avec les représentations en signe-magnitude et par excès, dont les débordements sont gérés de la même manière que pour les nombres positifs. Par contre, il n'en est pas de même pour le complément à deux. Si vous vous rappelez le chapitre 1, j'ai clairement dit que les calculs sur des nombres en complètement à deux utilisent les règles de l'arithmétique modulaire : les calculs sont faits avec un nombre de bits fixé une fois pour toute. Si un résultat dépasse ce nombre de bits fixé, on ne conserve pas les bits en trop. C'est une condition nécessaire pour pouvoir faire nos calculs. À priori, on peut donc penser que dans ces conditions, les débordements d'entiers sont une chose parfaitement normale, qui nous permet d'avoir des résultats corrects. Néanmoins, certains débordements d'entiers peuvent survenir malgré tout et produire des bugs assez ennuyeux.

Si l'on tient en compte les règles du complément à deux, on sait que le bit de poids fort (le plus à gauche) permet de déterminer si le nombre est positif ou négatif : il indique le signe du nombre. Tout se passe comme si les entiers en complément à deux étaient codés sur un bit de moins, et avaient leur longueur amputé du bit de poids fort. Si le résultat d'un calcul a besoin d'un bit de plus que cette longueur, amputée du bit de poids fort, le bit de poids fort sera écrasé; donnant un débordements d'entiers. Il existe une règle simple qui permet de détecter ces débordements d'entiers. L'addition (ou la multiplication) de deux nombres positifs ne peut pas être un nombre négatif : on additionne deux nombres dont le bit de signe est à 0 et que le bit de signe du résultat est à 1, on est certain d'être en face d'un débordements d'entiers. Même chose pour deux nombres négatif : le résultat de l'addition ne peut pas être positif. On peut résumer cela en une phrase : si deux nombres de même signe sont ajoutés, un débordement a lieu quand le bit du signe du résultat a le signe opposé. On peut préciser que cette règle s'applique aussi pour les nombres codés en complément à 1, pour les mêmes raisons que pour le codage en complément à deux. Cette règle est aussi valable pour d'autres opérations, comme les multiplications.

Modifier les circuits d'au-dessus pour qu'ils détectent les débordements en complément à deux est simple comme bonjour : il suffit créer un petit circuit combinatoire qui prenne en entrée les bits de signe des opérandes et du résultat, et qui fasse le calcul de l'indicateur de débordements. Si l'on rédige sa table de vérité, on doit se retrouver avec la table suivante :

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

L'équation de ce circuit est la suivante, avec et les signes des deux opérandes, et la retenue de la colonne précédente :

En simplifiant, on obtient alors :

Or, il se trouve que est tout simplement la retenue en sortie du dernier additionneur, que nous noterons . On trouve donc :

Il suffit donc de faire un XOR entre la dernière retenue et la précédente pour obtenir le bit de débordement.


Pour le moment, nous savons faire des additions, des soustractions, des décalages et rotations, ainsi que des opérations bit à bit. Chaque opération est réalisée par un circuit séparé. Cependant, il est possible de les fusionner en un seul circuit appelé une unité de calcul arithmétique et logique, abrévié ALU (Arithmetic and Logical Unit). Comme son nom l'indique, elle effectue des opérations arithmétiques et des opérations logiques (bit à bit). Tous les processeurs contiennent une ALU très similaire à celle qu'on va voit dans ce qui suit. La plupart des ALUs ne gèrent donc pas les opérations compliquées, comme les multiplications ou les divisions, de même que les décalages et rotation, et vous comprendrez pourquoi dans ce qui suit.

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

[modifier | 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. La suite de bit est très variable d'une ALU à l'autre. Elle peut être très structuré, chaque bit configurant une portion de l'ALU, ou être totalement arbitraire. La suite de bit peut être vu est aussi appelée l'opcode, ce qui est un diminution de code opération.

De plus, l'ALU a des sorties pour la retenue de sortie, les bits qui indiquent que le calcul a entrainé un débordement d'entier, etc. Ces bits sont appelés des flags, ou indicateurs. Les plus fréquents sont la retenue de sortie, un bit qui est à 1 si un débordement d'entier a eu lieu, un bit qui est à 1 si un débordement d'entier a eu lieu pour une addition signée (débordement en complètement à deux), un bit qui indique si le résultat est zéro, et quelques autres. Les flags sont calculés avec les circuits vus dans le chapitre précédent, dans la section sur la détection des débordements d'entiers.

Interface d'une ALU

Le bit-slicing

[modifier | 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 2, 4, 8 bits, rarement 16 bits. Mais il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes. Par exemple, on pouvait combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul plus grosses à partir d’unités de calcul plus élémentaires s'appelle en jargon technique du bit slicing.

Le bit slicing est utilisé pour des ALU capables de gérer les opérations bit à bit, l'addition, la soustraction, mais guère plus. Les ALU en bit-slice qui gère les multiplications existent, mais sont rares. La raison est qu'il n'est pas facile d'implémenter une multiplication entre deux nombres de 16 bits avec deux multiplieurs de 4 bits (idem pour la division). Alors que c'est plus simple pour l'addition et la soustraction : il suffit de transmettre la retenue d'une ALU à la suivante. Bien sûr, les performances seront alors nettement moindres qu'avec des additionneurs modernes, à anticipation de retenue, mais ce n'était pas un problème pour l'époque.

L'implémentation des opérations bit à bit avec une ALU bit-slice est très simple, la seule complexité est l'addition. Si on combine deux ALU de 4 bits, la première calcule l'addition des 4 bits de poids faible, alors que le second calcule l'addition des 4 bits de poids fort. Mais il faut propager la retenue de l'addition des 4 bits de poids faible à la seconde ALU. Pour cela, l'ALU doit transmettre un bit de retenue sortant à l'ALU suivante, qui doit elle accepter celui-ci sur une entrée. Rappelons qu'une addition en binaire s'effectue comme en décimal : on additionne les bits colonne par colonne pour obtenir le bit de résultat, et il arrive qu'une retenue soit propagée à la colonne suivante.

Pour cela, l'ALU doit avoir une interface compatible : il faut qu'elle ait une entrée de retenue, et une sortie pour la retenue sortante. La retenue passée en entrée est automatiquement prise en compte lors d'une addition par l'ALU. Comme nous l'avons vu dans le chapitre dédié aux circuits de calculs, ajouter une entrée de retenue ne coute rien et est très simple à implémenter en à peine quelques portes logiques.

L'intérieur d'une unité de calcul

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

D'autres envoient les opérandes à tous les circuits en même temps, et activent ou désactivent chaque sous-circuit suivant les besoins. Chaque circuit possède ainsi une entrée de commande, dont la valeur est déduite par un circuit combinatoire à partir de l'entrée de sélection d'instruction de l'ALU (généralement un décodeur). Nous allons voir plusieurs exemples d'unités de calcul configurable dans ce chapitre. Pour le moment, sachez qu'un simple additionneur-soustracteur est un circuit configurable de ce genre.

ALU composée de sous-ALU configurables.

Les ALU sérielles

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

ALU sérielle

Les ALU sérielles ne payent pas de mine, mais elles étaient très utilisées autrefois, sur les tout premiers processeurs. Les ordinateurs antérieurs aux années 50 utilisaient des ALU de ce genre. L'avantage de ces ALU est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes. Il suffit de prévoir des registres à décalage suffisamment longs, ce qui est tout sauf un problème. Par contre, elles sont assez lentes pour faire leur calcul, vu que les calculs se font bit par bit. Elles sont d'autant plus lentes que les opérandes sont longs.

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

[modifier | modifier le wikicode]

Il est possible d'obtenir une ALU entière simple en modifiant un additionneur-soustracteur simple. Pour rappel, un additionneur soustracteur est fait en combinant un additionneur avec un inverseur commandable.

Additionneur soustracteur

Il est possible de modifier l'additionneur, mais aussi les circuits situés juste avant. L'idée est d'ajouter un circuit commandable de mise à zéro la seconde entrée d'opérande. Il est aussi possible de commander l'entrée de retenue entrante de l'additionneur séparément, via sa propre entrée.

ALU basée sur un additionneur soustracteur modifié

Les opérations que peut faire cette ALU sont assez nombreuses. Déjà, elle supporte l'addition et la soustraction, quand la seconde opérande n'est pas inversée. En inversant la seconde opérande, on peut gérer deux opérations. La première est l'identité (on recopie l'opérande d'entrée sur la sortie), qui se fait en désactivant l'inverseur. En activant l'inverseur, on a l'opération d'inversion NOT, à savoir que les bits de l'opérande sont inversés. En jouant sur l'entrée de retenue, on peut aussi émuler d'autres opérations, comme l'incrémentation. Les 8 opérations possibles sont les suivantes :

Reset Invert Retenue entrante Sortie de l'ALU
0 0 0 A + B
0 0 1 A + B + 1
0 1 0 A + = A - B - 1
0 1 1 A + + 1 = A - B
1 0 0 B
1 0 1 B + 1
1 1 0
1 1 1 + 1

Les ALU basées sur un additionneur, avec manipulation des retenues

[modifier | modifier le wikicode]

Maintenant que nous avons vu ce qu'il est possible de faire en modifiant ce qu'il y a avant l'additionneur, nous allons modifier l'additionneur lui-même.

L'implémentation du XOR/NXOR

[modifier | modifier le wikicode]

Dans cette section, nous allons nous intéresser à un circuit qui effectue un XOR en plus de l'addition. Le choix d'utiliser à la fois une addition et un XOR peut sembler bizarre, mais s'explique par le fait qu'une opération XOR est identique à une addition binaire dont on ne tiendrait pas compte des retenues.

Pour rappel, un additionneur complet additionne trois bits, en faisant deux XOR :

Si on met la retenue entrante à zéro, on a :

En clair, en manipulant les entrées de retenue des additionneurs complets, on peut avoir un XOR à partir de l'addition. Pour cela, on ajoute un circuit de masquage, comme vu dans le chapitre sur les opérations bit à bit, pour mettre les entrées à 0, on a le circuit ci-dessous. Le choix de l'opération est le fait d'une entrée de commande, mise à 0 pour un XOR et à 1 pour l'addition. Cette méthode marche avec tous les additionneurs, mais elle est plus simple à implémenter avec les additionneurs à anticipation de retenue.

Circuit qui fait ADD et XOR.

Si un XOR est équivalent à une addition où les retenues sont mises à 0, on peut se demander ce qu'il se passe quand on les met à 1. Dans ce cas, pour chaque additionneur complet, le bit du résultat est :

Sachant que , on se rend compte que le circuit calcule le NXOR des deux entrées.

En clair, en masquant les retenues entrantes, on peut transformer une addition en XOR ou en NXOR. Il suffit d'insérer des circuits de masquage avant les entrées de retenue. Le circuit de masquage soit recopie le bit d'entrée (pour l'addition), soit force les entrées de retenue à 0, soit les force à 1. Et on a déjà vu le circuit adéquat dans le chapitre sur les opérations bit à bit, à savoir la porte 1 bit universelle. Pour rappel, c'est un circuit avec deux bits de commandes, qui prend un bit en entrée et fournit sur la sortie : soit 0, soit 1, soit le bit d'entrée, soit son inverse. Il suffit donc d'ajouter une porte universelle 1 bit juste avant l'entrée de retenue entrante, et le tour est joué !

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

L'implémentation du ET/OU avec une addition, en utilisant les retenues sortantes

[modifier | modifier le wikicode]

Maintenant, faisons la même chose, mais regardons les retenues de sortie.

Retenue entrante Opérande 1 Opérande 2 Retenue sortante
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 1
1 0 0 0
1 0 1 1
1 1 0 1
1 1 1 1

On remarque deux choses :

  • si la retenue d'entrée est à 0, la retenue de sortie est un ET entre les deux bits d'opérandes.
  • si on met la retenue entrante à 1, alors la retenue sortante sera un OU entre les deux bits d'opérandes

Pour implémenter le circuit, il faut connecter la sortie soit aux bits de résultat, soit aux entrées de retenue. Dans le circuit précédent, si la couche d'additionneurs finale est une couche d'additionneurs complets, on peut extraire le ET et le OU des deux opérandes. Un simple MUX placé à la suite permet de choisir si l'on regarde les bits du résultat ou la "retenue sortante" de ces additionneurs complets.

Implémentation d'une ALU entière simple

L'implémentation du NOT avec une addition, en manipulant retenues et opérandes

[modifier | modifier le wikicode]

Il est enfin possible d'implémenter l'opération NOT avec seulement un additionneur, pas besoin qu'il soit un additionneur-soustracteur. En effet, on obtient le NOT d'une opérande, via deux manières légèrement différentes.

Un additionneur complet peut se comporter comme une porte NOT à condition que l'une des entrées soit à 0 et l'autre à 1. La raison à cela est que l'additionneur complet fait un double XOR : le bit à inverser est XORé avec 0, ce qui le recopie, puis avec 1, ce qui l'inverse. Peu importe l'ordre, vu que le XOR est commutatif et associatif.

Cela donne deux possibilités : soit on met le second opérande à 0 et les retenues à 1, soit on fait l'inverse. Le résultat est disponible sur les bits de résultat. Pour cela, la solution consiste à ajouter un circuit qui met à 0 la seconde opérande, on a déjà le circuit pour manipuler les retenues. Mais cette solution est rarement utilisée, vu que la majorité des ALU utilise un additionneur-soustracteur, qui permet d'implémenter l'opération NOT facilement, tout en permettant d'implémenter aussi la soustraction, pour un cout en porte logique mineur et des performances quasiment identiques.

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 les additionneurs complets eux-même n'ont pas été modifiés. Les ALU que l'on va voir dans ce qui suit fonctionnent sur un principe différent. Au lieu d'ajouter des circuits autour des additionneurs complets, elles modifient les additionneurs complets de manière à ce qu'il puisse faire des opérations logiques ET/OU/XOR/NXOR. Ils deviennent alors de véritables ALU de 1 bit, qui sont assemblées pour donner des ALU entières.

En plus d'assembler des ALU 1 bit, il faut aussi gérer les retenues, et les différentes manières de faire ressemblent beaucoup à ce qui se fait avec les additionneurs. Par exemple, on peut relier chaque ALU 1 bit comme dans un additionneur à propagation de retenue, où chaque ALU 1 bit envoie sa retenue sortante sur l'entrée de retenue de l'ALU 1 bit suivante. Une autre possibilité est d'utiliser un circuit de calcul des retenues, comme pour un additionneur à anticipation de retenue.

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

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

[modifier | modifier le wikicode]

Voyons maintenant l'exemple du processeur 8086 d'Intel, un des tout premier de la marque. L'additionneur de cette ALU est un additionneur à propagation de retenue, avec une Manchester Carry Chain pour accélérer la propagation des retenues. Pour rappel, un additionneur Manchester carry chain génère en interne deux signaux : un signal de propagation de retenue et un signal de génération de retenue. Les deux sont combinés avec la retenue entrante pour calculer le résultat et la retenue de sortie, la combinaison se faisant avec un circuit basé sur des portes à transmission. Les deux signaux sont déterminés par une unique porte logique, qui prend en entrée les deux bits d'opérande : une porte logique détermine si l'addition génère une retenue, un autre si elle propage la retenue d'entrée sur la colonne suivante. Un tel additionneur est illustré ci-dessous, pour rappel.

Manchester carry chain

Sur le 8086, ces deux portes sont remplacées par une porte logique universelle commandable 2 bit, à savoir un circuit qui peut remplacer toutes les portes logiques 2 bit existantes. Comme vu dans le chapitre sur les opérations bit à bit, cette porte universelle est un simple multiplexeur configuré convenablement. En conséquence, le signal de propagation et de génération de retenue sont configurables et on peut les utiliser pour calculer autre chose. Par exemple, la première porte XOR peut être remplacée par une porte ET, OU, NOR, FALSE (elle donne toujours zéro en sortie), OUI (recopie un bit d'entrée sur la sortie), etc.

ALU du 8086 (bloc de 1 bit)

La gestion des additions et soustractions est alors triviale. Il suffit de configurer les deux portes universelles de manière à obtenir le circuit d'un additionneur complet ou d'un soustracteur complet.

Lors des opérations bit à bit et des décalages, les deux signaux sont configurés de manière à ce qu'au moins l'un d'entre elle ait sa sortie mise à 0. C'est-à-dire que l'une des deux portes universelles sera configurée de manière à devenir une porte FALSE. En faisant cela, la porte XOR aura une entrée à 0, ce qui fait qu'elle recopiera l'autre entrée sur sa sortie. Elle se comportera comme une porte OUi pour l'autre entrée, celle pas mise à 0.

Pour les opérations logiques, l'une des portes universelle est configurée de manière à avoir la porte logique voulue, l'autre est mise à 0, la porte XOR recopie l'entrée de la première. La porte logique mise à 0 est celle qui génère les retenues. La porte qui calcule le signal de propagation de la retenue, celle qui additionne les deux bits d'opérande, est alors configurée pour donner la porte voulue : soit un ET, soit un OU, soit un XOR, soit...

ALU du 8086 lors d'une opération logique

L'ALU du 8086 supporte aussi les décalages d'un rang vers la gauche, qui sont équivalents à une multiplication par deux. L'opérande décalée est envoyé sur les entrées A de chaque additionneur complet. Pour effectuer, ils utilisent une solution très simple : chaque additionneur envoie le bit de l'opérande sur la sortie de retenue. De plus, les entrées d'opérandes ne sont pas additionnées. Pour résumer, il faut que le signal de propagation de retenue soit mis à zéro, alors que le signal de génération de retenue soit égal au bit d'entrée de l'opérande. Les deux portes logiques universelles sont alors configurées pour : la porte de propagation se comporte comme une porte FALSE, l'autre comme une porte OUI qui recopie l'entrée A.

ALU du 8086 lors d'un décalage à gauche d'un rang

En somme, l'ALU fait son travail en configurant les deux portes universelles. Pour configurer les portes, l'ALU contient un petit circuit combinatoire qui traduit l'opcode en signaux envoyés aux portes universelles.

Pour ceux qui veulent en savoir plus sur les circuits de calcul de l'Intel 8086, voici un lien :

L'exemple de l'ALU du processeur Intel x86 8008

[modifier | 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. Il n'était pas possible de placer beaucoup de transistors sur les puces de l'époque, ce qui fait que les concepteurs de processeurs tournaient à l'économie et privilégiaient des additionneurs à propagation de retenue.

Comme beaucoup d'ALU des processeurs assez anciens, elle est construite en assemblant plusieurs ALU de 1 bits, chacune étant un additionneur complet amélioré. L'ALU de 1 bit utilise des additionneurs complets implémentés avec le circuit suivant :

Full adder basé sur une modification de la retenue

L'additionneur précédent est modifié pour gérer les trois opérations XOR, ET, OU. Pour gérer le XOR, il suffit de mettre la retenue d'entrée à 0, ce qui est réalisé avec une vulgaire porte ET pour chaque additionneur complet, placée en aval de l'entrée de retenue. Pour gérer les deux autres opérations logiques, le circuit ne suit pas la logique précédente et n'utilise pas de multiplexeur. Le résultat du ET/OU est bien disponible sur la sortie de résultat, non sur la sortie de retenue. A la place, le circuit utilise la porte ET et la porte OU de l'additionneur complet, et désactive la porte inutile. Pour un ET/OU, le circuit met à zéro la retenue entrante. De plus, elle met aussi à zéro la retenue sortante, sans quoi le circuit donne des résultats invalides.

Dans les faits, l'implémentation exacte était légèrement plus complexe, vu que ce circuit était conçu à partir de portes TTL AND-OR-NAND, qui regroupe une porte ET, une porte OU et une porte NAND en une seule. Pour ceux qui veulent en savoir plus sur les circuits de calcul de l'Intel 8008, voici un lien qui pourrait vous intéresser :

L'exemple de l'unité de calcul 74181

[modifier | modifier le wikicode]

Afin d'illustrer ce qui a été dit plus haut, nous allons étudier un exemple d'unité de calcul : l'unité de calcul 74181, très souvent utilisée dans les autres cours d'architecture des ordinateurs pour son aspect pédagogique indéniable. Il s'agit d'une unité de calcul commercialisée dans les années 60, à une époque où le microprocesseur n'existait pas. Les processeurs de l'époque étaient conçus à partir de pièces détachées assemblées et connectées les unes aux autres. Les pièces détachées en question étaient des boitiers qui contenaient des registres, l'unité de calcul, des compteurs, des PLA, qu'on assemblait sur une carte électronique pour faire le processeur. L'unité 74181 était une des toutes premières unités de calcul fournie dans un boitier, là où il fallait auparavant fabriquer une ALU à partir de circuits plus simples comme des additionneurs ou des circuits d’opérations bit à bit.

Le 74181 était une unité de calcul de 4 bits, ce qui veut dire qu'il était capable de faire des opérations arithmétiques sur des nombres entiers codés sur 4 bits. Il prenait en entrée deux nombres de 4 bits, et fournissait un résultat de 4 bits. Il était possible de faire du bit-slicing, à savoir de combiner plusieurs 74181 afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Le 74181 était spécifiquement conçu pour, car il gérait un bit de retenue en entrée et fournissait une sortie pour la retenue du résultat.

Les opérations gérées par l'ALU 74181

[modifier | modifier le wikicode]

Les opérations de base du 74181 comprennent l'addition et 16 opérations dites bit à bit. Il peut fonctionner selon deux modes. Dans le premier mode, il effectue une opération bit à bit seule. Dans le second mode, il effectue une opération bit à bit entre les deux nombres d'entrée A et B, additionne le nombre A au résultat, et additionne la retenue d'entrée. Pour résumer, il effectue une opération bit à bit et une addition facultative. En tout, le 74181 était capable de réaliser 32 opérations différentes : les 16 opérations bit à bit seules (le maximum d'opérations de ce type possibles entre deux bits), et 16 autres opérations obtenues en combinant les 16 opérations bit à bit avec une addition.

Le fait de faire une opération bit à bit avant l'addition permet d'émuler une soustraction. Rappelons qu'une soustraction entre deux nombres A et B s'obtient en inversant les bits de B, en additionnant et en ajoutant 1. Or, il existe une opération bit à bit qui inverse tous les bits d'un nombre et celle-ci est supportée par le 74181. Ajouter 1 se fait en envoyant une retenue égale à 1 sur l'entrée de retenue.

Schéma fonctionnel du 74181.

L'entrée de sélection de l'instruction fait 5 bits, ce qui colle parfaitement avec les 32 instructions possibles. Les 5 bits en question sont séparés en deux : un groupe de 4 bits qui précise l'opération bit à bit, et un bit isolé qui indique s'il faut faire l'addition ou non. L'opération bit à bit à effectuer, est précisée par 4 bits d'entrée notés s0, s1, s2 et s3. L'activation de l'addition se fait par un bit d'entrée, le bit M, qui précise s'il faut faire ou non l'addition.

L'implémentation de l'ALU 74181

[modifier | modifier le wikicode]

Le 74181 comprend 75 portes logiques, du moins en théorie. 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, qui implémente les 16 opérations logiques. Le circuit est cependant très optimisé, dans le sens où l'additionneur complet est fusionné avec la porte logique universelle. Comme sur le 8086, on modifie la manière dont les signaux de propagation et de génération de retenue sont calculés. Sauf qu'ici, on n'utilise qu'une seule porte logique universelle, très modifiée.

Le 74181 est composé de circuits assez semblables à une porte logique universelle de 2 bits, sauf qu'elle fournit deux sorties : un signal de propagation de retenue, un signal de génération de retenue. Pour comprendre comment il fonctionne, le mieux est d'établir sa table de vérité. On part du principe que le circuit a deux entrées A et B, et calcule A + f(A,B), avec f(A,B) une opération bit à bit.

A B A PLUS f(a,b) P G
0 0 0+f(0,0) f(0,0) 0
0 1 0+f(0,1) f(0,0) 0
1 0 1+f(1,0) 1 f(1,0)
1 1 1+f(1,1) 1 f(1,1)

Sur le 74181, il faut imaginer que le circuit qui calcule f(A,B) est une porte universelle commandable 2 bits, réalisée avec un multiplexeur. Les bits du résultat sont envoyés sur les 4 entrées du multiplexeur, et le multiplexeur choisit le bon bit à partir des entrées A et B (qui sont envoyés sur son entrée de commande. Les 4 entrées du multiplexeur sont notées S0, S1, S2 et S3. On a alors :

A B A PLUS f(a,b) P G
0 0 0+f(0,0) S1 0
0 1 0+f(0,1) S0 0
1 0 1+f(1,0) 1 S2
1 1 1+f(1,1) 1 S3

Le circuit pour faire cela est le suivant :

Circuit de base du 74181, avant l'additionneur

Le schéma du circuit est reproduit ci-dessous. Un œ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 :

L'implémentation naïve, avec des multiplexeurs

[modifier | modifier le wikicode]

Pour obtenir une ALU, vous avez peut-être pensé à la solution ci-dessous. L'idée est d'utiliser un circuit par opération et de choisir le résultat adéquat avec des multiplexeurs. Le multiplexeur est évidemment commandé par l'entrée de commande, il reçoit l'opcode. Un exemple avec une ALU de 2 bits est conné ci-dessous.

2-bit ALU

Mais cette solution peut être mélangée avec les solutions précédentes. Par exemple, il est possible de mélanger cette idée avec une ALU basée sur un additionner-soustracteur. Pour obtenir un additionneur-soustracteur, on doit placer un inverseur commandable avant l'additionneur, en aval d'un opérande. L'idée est de relier l'inverseur commandable non seulement à l'additionneur, mais aussi aux portes ET/OU/XOR. Cela permet de faire les opérations NOR/NAND/NXOR, gratuitement, juste en changeant le câblage du circuit. Il est aussi possible d'ajouter un circuit pour mettre à zéro l'opérande non inversée, comme vu plus haut. Le résultat est celui vu plus haut.

ALU simplifiée.


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

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

Les opérations logiques manipulent un opérande, et précisément sa représentation binaire. Elles ne font pas de calcul avec cet opérande, mais manipulent ses bits. La différence avec les opérations bit à bit est que l'opération ne se fait pas bit par bit, il n'y a pas d'opération appliquée à chaque bit indépendamment des autres. Les opérations logiques combinent les bits entre eux pour fournir leur résultat, elles peuvent les faire changer de place, en supprimer, en ajouter, etc.

L'opération de population count

[modifier | modifier le wikicode]

Nous allons maintenant aborder un cas très particulier d'addition multi-opérande : le calcul de la population count d'un nombre, aussi appelée poids de Hamming. Ce calcul prend en entrée un opérande entier tout ce qu'il y a de plus normal, codé sur plusieurs bits. La population count compte le nombre de bits de l'opérande qui sont à 1.

Elle est très utilisée quand il faut manipuler la représentation binaire d'un nombre ou quand on manipule des tableaux de bits. Les deux cas sont peu fréquents en dehors des codes très bas niveau, mais tout programmeur a déjà eu l'occasion d'en manipuler. Elle est aussi utilisée pour le codage/décodage vidéo/audio, quand il faut crypter/décrypter des données, etc. Les réseaux de neurones artificiels, notamment ceux utilisés dans l'intelligence artificielle, font aussi usage de cette opération. Elle 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 ne parlent pas des circuits de population count, sauf pour quelques exceptions assez rares. Ils parlent de l'opération elle-même, pas des circuits pour la calculer. Si nous faisons un chapitre complet sur ce circuit, c'est parce que les concepts que nous allons aborder dans ce chapitre seront une bonne préparation au chapitre portant sur l'addition multiopérande. En effet, la population count est une forme d'addition multiopérande.

Le calcul de la population count : les compteurs parallèles

[modifier | modifier le wikicode]

La population count peut se calculer à partir du raisonnement suivant : si on découpe un nombre en deux parties, sa population count est la somme des population count de chaque partie. L'idée est donc de découper un opérande en groupes de 2, 3, 4, 5, 6, voire 7 bits ; dont on calcule la population count, avant d'additionner le tout. Pour cela, il faut des circuits qui calculent la population count pour des opérandes de 2, 3, 4, 5, 6, voire 7 bits et fournissent un résultat codé en binaire. Ils sont appelés des parallel counters, terme que nous traduirons par compteurs parallèles.

Pour additionner les résultats des compteurs parallèles, il est possible d'utiliser des additionneurs dit multiopérande, qui sont capables d'additionner 3, 4, 5 opérandes, voire plus. Par exemple, pour 5 nombres notés a, b, c, d et e, il existe un additionneur 5-opérandes capable de calculer a + b + c + d + e. De tels additionneurs multi-opérandes nous rendraient la vie plus facile. Par exemple, pour calculer la population count d'un entier 32 bits, on pourrait utiliser des compteurs parallèles prenant en entrée un octet, et additionner les 4 résultats.

Circuit de calcul de population count.

Les compteurs parallèles eux-mêmes peuvent être construits en combinant des compteurs parallèles plus petits avec un additionneur. Par exemple, si je prends un entier de 32, sa population count peut se calculer en additionnant la sortie de deux compteurs parallèles de 16 bits. Eux-mêmes peuvent se construire avec chacun deux compteurs parallèles de 8 bits et un additionneur, etc.

Population count avec des compteurs parallèles

Mais pour le moment, nous ne disposons que d'additionneurs capables d'additionner deux opérandes, des additionneurs simples. Nous verrons dans le prochain chapitre comment fabriquer des additionneurs multiopérande. D'ailleurs, ce chapitre sert d'introduction propédeutique à l'addition multiopérande. En effet, la population count est une forme d'addition multiopérande, dont les opérandes font toutes 1 bit. Mais pour le moment, nous n'avons que des additionneurs normaux, ce qui nous limite à la possibilité d'utiliser deux compteurs parallèles et d'additionner leur résultat avec un additionneur normal. Le tout est illustré ci-dessous.

POPCOUNT avec un adder multiopérande et des adders compressors

Si on poursuit le processus jusqu'au bout, on se retrouve avec des compteurs parallèles de 2 bits, qui ne sont ni plus ni moins que des demi-additionneurs. Le résultat est une série d'additionneurs enchainés en arbre, comme illustré ci-dessous. Mais ce n'est évidemment pas la solution optimale, juste un cas extrême où le processus récursif est poussé jusqu'au bout. L'idéal serait sans doute d'utiliser d'utiliser des compteurs parallèles plus larges, par exemple de 4, 8, 16 bits, du moins en première approche.

Circuit de calcul de population count.
Mine de rien, les schémas précédents nous donnent un moyen pour créer un additionneur multiopérande, en combinant plusieurs additionneurs normaux. Il faut les enchainer comme indiqué, en arbre, avec chaque additionneur qui additionne la somme de deux précédents. Mais nous verrons cela dans le chapitre suivant, surtout que nous verrons que ce n'est pas la meilleure manière de faire.

Les opérandes de l'opération population count sont des entiers codés sur 8, 16, 32, 64 bits, qu'on aimerait découper en groupes de 4, 8, 16 bits. Mais ce n'est pas le plus pratique. Un compteur parallèle fournit un résultat codé sur bits, à partir de bits d'entrée (le -1 est là, car il faut encoder le zéro). Par exemple, pour un résultat codé sur 2 bits, les population count possibles sont 0, 1, 2 ou 3, des valeurs ce qui demande 3 bits en entrée. 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. À chaque fois, il nous manque un bit pour avoir un groupe bien rond de 4, 8, 16 bits. Ceci dit, même si ce n'est pas parfait, additionner 4 ou 5 bits à la fois au lieu de deux est d'une aide précieuse. Aussi, il est intéressant de regarder ce que donne un compteur parallèle de 2, 3, 4, 5 bits d'entrée.

Il est maintenant temps de voir comment fabriquer un compteur parallèle. Nous avions vu qu'une solution est de le fabriquer avec des compteurs parallèles eux-mêmes fabriqués par des compteurs parallèles plus petits et ainsi de suite. Cependant, il existe une manière alternative, qui donne un circuit différent. Le circuit que nous allons voir est composé d'un paquet d'additionneurs complets ou de demi-additionneurs. Pour rappel, le premier additionne deux bits et une retenue, alors que le second additionne deux bits d'opérandes. Dans ce qui suit, nous allons utiliser les abréviations suivantes : FA (Full-Adder) pour additionneur complet,, FA (Half-Adder) pour demi-additionneur.

Un exemple : un compteur parallèle de 4 bits

[modifier | modifier le wikicode]

Intuitivement, il est intéressant de commencer par des compteurs parallèles qui prennent 2, 3 4 bits d'opérandes, pas plus. Et il se trouve que le cas d'un compteur parallèle 2 bits est très simple : il s'agit d'un simple demi-additionneur, un simple HA, qui additionne deux bits, par définition. Il en est de même avec un compteur parallèle 3 bits : c'est un additionneur complet, un FA. Certes, un FA est construit pour additionner deux bits d'opérandes et un bit de retenue. Mais le bit de retenue n'a rien de spécial, c'est un bit comme un autre. Il est parfaitement possible de le remplacer par un bit provenant d'un autre opérande, pour obtenir la somme de trois bits.

Le premier exemple intéressant est donc un compteur parallèle de 4 bits, c’est-à-dire 4 bits d'opérande. Le résultat est un entier codé sur 3 bits. Le seul moyen de le concevoir est de combiner plusieurs HA et FA. Une manière de faire serait d'additionner les bits d'opérande deux par deux, avec deux HA. Les deux HA fournissent deux résultats de deux bits : un bit de somme et un bit de retenue chacun. Les 2 bits de somme sont additionnés par un autre HA, qui fournit le bit de poids faible du résultat et un troisième bit de retenue.

Compteur parallèle de 4 bits, partiel

Maintenant, que faire des trois bits de retenue ? La réponse se comprend quand on écrit la table de vérité du circuit, que voici :

Opérande Retenue 1 Retenue 2 Retenue 3 Résultat
0000 0 0 0 0 0 0
0001 0 0 0 0 0 1
0010 0 0 0 0 0 1
0011 1 0 0 0 1 1
0100 0 0 0 0 0 1
0101 0 0 1 0 1 0
0110 0 0 1 0 1 0
0111 1 0 0 0 1 1
1000 0 0 0 0 0 1
1001 0 0 1 0 1 0
1010 0 0 1 0 1 0
1011 1 0 0 0 1 1
1100 0 1 0 0 1 0
1101 0 1 0 0 1 1
1110 0 1 0 0 1 1
1111 1 1 0 1 0 0

Vous remarquerez que si on additionne les retenues, la somme est identique aux bits de poids fort du résultat. En clair, les retenues doivent être additionnées. Pour cela, on peut utiliser un FA, ce qui donne le circuit suivant :

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

Les compteurs parallèles à base d'additionneurs complets

[modifier | modifier le wikicode]

Le circuit que nous avons vu précédemment a l'air simple : on additionne les bits entre eux, ce qui donne un bit de somme et des retenues, retenues qui sont additionnées elles aussi. Mais que se passe-t-il pour les compteurs parallèles de plus de 4 bits ? Pour cela, nous allons voir le cas général.

Un compteur parallèle peut-être composé uniquement de FA ou de HA, voire combiner les deux. Nous allons commencer par parler de compteurs parallèles fabriqués uniquement avec des FA, afin de pouvoir comparer avec le même circuit fabriqué avec des HA. Pour commencer, les bits sont regroupés par groupes de 3. Les bits d'un triplet sont additionnés avec un FA, ce qui donne un résultat sur deux bits : un bit de somme, un bit de retenue.

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

Les bits de somme sont gérés à part des bits de retenue. Les bits de somme sont tous additionnés entre eux, par une couche de FA, suivie par une autre couche de FA, et ainsi de suite jusqu’à ce que tous les bits aient été additionnés. Le résultat est un arbre d'additionneurs qui calcule le bit de poids faible du résultat. L'addition des bits de somme donne le bit de poids faible du résultat, de la population count. Mais il y a aussi des bits de retenue, un par FA (en comptant toutes les couches). Ils sont utilisés pour calculer les bits de poids fort de la population count.

Première étape du calcul de la PCOUNT

Gérer les bits de retenue est assez simple : ils doivent tous être additionnés entre eux ! Sauf que cette fois-ci, on ne peut les additionner avec un simple FA. Il faut à la place utiliser un circuit qui additionne plus de 3 bits, c’est-à-dire un compteur parallèle ! Pour le dire autrement, on doit calculer la population count des bits de retenue ! En clair, calculer la population count d'un opérande de n bits, il faut utiliser un arbre d'additionneurs, couplé à un circuit qui calcule une population count plus courte !

Circuit complet de calcul de la population count avec des additionneurs complets

Le circuit ne paye pas de mine, mais c'est ce circuit qui a été utilisé sur un des tout premier processeur ARM, l'ARM1 prévu pour les premiers iPhones.

Pour ceux qui veulent en savoir plus, voici un lien vers le blog de Ken Shiriff, qui a rétroingénieuré ce processeur et qui décrit le circuit exact utilisé dans l'ARM1. Le circuit été cependant utilisé dans un contexte un peu particulier, lié à la gestion des registres du processeur, chose qui sera vu dans la cinquième partie de ce cours : Counting bits in hardware: reverse engineering the silicon in the ARM1 processor

Maintenant, faisons quelques calculs, pour savoir combien de retenues doivent être additionnées. Avec des additionneurs complets, la première couche fournit 32/3 = 10 opérandes de 2 bits + un bit isolé. Mais qu'en est-il pour les autres couches ? Je vais donner le nombre de FA/retenues dans le cas général avec n bits d'opérande, mais essayez de le calculer, de trouver une formule mathématique assez simple.

Dans le cas général, pour un opérande de n bits, la première couche produit n/3 avec des FA. Et chaque couche fournit un nombre de retenues qui est divisé par 3, comparé à la couche précédente. On a donc :

Le résultat est que l'on a moitié moins de FA/retenues. Le circuit contient un arbre de n/2 FA, et un circuit qui calcule la population count de n/2 retenues. En comptant le nombre de portes de ce dernier, et ainsi de suite, on trouve :

Maintenant, faisons une comparaison avec un circuit composé uniquement de demi-additionneurs (HA). Pour un opérande de n bits, le nombre de bits de retenue est de n-1. Le circuit pour n bits utilise donc n-1 HA et un compteur parallèle de n-1 bits. En faisant la somme totale, on trouve : HA. Cela fait exactement n(n+1)/2 HA. Et vu que chaque HA contient deux portes logiques, cela fait en tout portes logiques.

Circuit complet de calcul de la population count avec des demi-additionneurs

L'usage de FA diminue drastiquement le nombre de portes logiques utilisées : de portes logiques, on passe à seulement N. Certes, un FA prend deux-trois fois plus de portes logiques qu'un HA, mais le gain est substantiel pour de longs opérandes. Peut-être est-il possible de pousser cette logique un peu plus loin, ce qui nous amène à la section suivante.

Les compteurs parallèles à base d'adders compressors

[modifier | modifier le wikicode]

Passer d'un HA à un FA a donc fortement réduit le nombre de retenue et grandement simplifié le circuit. Maintenant, essayons de voir l'étape suivante, à savoir ce qui se passe avec des circuits qui additionnent plus de 3 bits. Par exemple, on pourrait imaginer des circuits qui additionnent 4/5/6/7 bits. Il faut noter que nous recherchons des circuits qui ne donnent pas la population count directement, seulement le bit de somme final et les retenues intermédiaires. De tels circuits sont appelés des adder compressors.

Les adder compressors ne calculent pas la population count, mais ils fournissent son bit de poids faible, ainsi que les différentes retenues des additions intermédiaires. Les retenues sont combinées ensemble par des circuits à part, typiquement un autre parallel counter. Les parallel counters sont construits en combinant un adder compressor et un circuit qui additionne les retenues (un autre parallel counter). Il est possible d'utiliser des adder compressors pour fabriquer un parallel counter, mais ils sont aussi utilisés dans d'autres circuits d'addition multiopérande.

Pour créer un adder compressor, une solution est de prendre plusieurs FA et de les combiner entre eux. Il est possible de fusionner deux additionneurs complets à la suite, voire trois, quatre, cinq, voire plus. Cependant, faire ainsi n'est pas optimal. Idéalement, il faut 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.

Circuit de calcul de PCOUNT de 5 bits

Pour comprendre pourquoi, prenons l'exemple d'un adder compressor 4:2". Contrairement à ce que son nom peut faire penser, il prend 5 bits en opérande et fournit 3 sorties : le bit de poids faible du résultat final, les deux retenues des additions intermédiaires. Une implémentation naïve est illustrée ci-contre. Il additionne 5 bit avec 2 additionneurs complets, combinés avec un parallel counter qui combine les retenues (en jaune). Nous allons nous attarder sur les FA et plus précisément sur la manière dont le bit de poids faible est calculé. Pour rappel, un additionneur complet calcule le bit de poids faible en faisant un XOR entre les bits d'opérande. En enchainant des additionneurs complets, voici ce que l'on obtient 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é

Le but est donc d'obtenir un arbre de portes XOR équilibré, avec une taille minimale. Pour cela, il existe une solution simple : utiliser des HA au lieu des FA. Mais en faisant cela, le nombre de retenue est trop important. L'idée est alors de garder l'arbre des portes XOR obtenu avec des HA, mais de calculer 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é est une technique qui permet de détecter si une donnée a été corrompue. Elle permet de détecter qu'un bit a été inversé, à savoir qu'un bit censé être à 1 est passé à 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, si la donnée a un nombre de bit à 1 pair, alors le bit de parité vaut 0. Mais si le nombre est impair, alors le bit de parité vaut 1. Dans cette section, nous allons voir un circuit qui calcule le bit de parité d'un opérande.

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é n'est ni plus ni moins que le bit de poids faible de la population count. Mais heureusement, il existe une autre méthode bien plus simple, plus rapide, et plus économe en circuits. Pour comprendre comment, nous allons commencer avec un cas simple : le calcul à partir d'un opérande de 2 bits. Le circuit étant simple, il suffit d'utiliser les techniques vues précédemment, avec la table de vérité. En écrivant la table de vérité du circuit, on remarque rapidement que la table de vérité donne la table de vérité d'une porte XOR.

Bit 1 Bit 2 Bit de parité
0 0 0
0 1 1
1 0 1
1 1 0

Pour la suite, nous allons partir d'un nombre de trois bits. On pourrait tenter de créer ce circuit à partir d'une table de vérité, mais nous allons utiliser une autre méthode, qui nous donnera un indice important. Ce nombre de 3 bits est composé d'un nombre de 2 bits auquel on a jouté un troisième bit. L'ajout de ce troisième bit modifie naturellement le bit de parité du nombre précédent. Dans ce qui va suivre, nous allons créer un circuit qui calcule le bit de parité final, à partir : du bit de parité du nombre de 2 bits, et du bit ajouté. On voit alors que la table de vérité est celle d'une porte XOR.

Bit de parité précédent Bit ajouté Bit de parité final
0 0 0
0 1 1
1 0 1
1 1 0

Chose assez intéressante, ce mécanisme fonctionne quel que soit le nombre de bits de l'opérande. Ajouter un bit à un nombre modifie sa parité, celle-ci état alors égale à : bit ajouté XOR bit-parité du nombre. L’explication est relativement simple : ajouter n 0 ne modifie pas le nombre de 1, et donc le bit de parité, tandis qu'ajouter un 1 inverse le bit de parité.

Circuit de parité

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. Effectué naïvement, il suffit d’enchaîner des portes XOR les unes à la suite des autres. 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]

Pour ce qui est des codes de Hamming, ils calculent plusieurs bits de parité, qui sont calculés en prenant en compte une partie des bits de l'opérande. Un circuit qui génère le code de Hamming est donc 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.

Hamming(7,4)

Par exemple, voici ci-dessous le circuit pour vérifier un code de Hamming de type 7,4. Pour rappel, 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. 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.

Le circuit est composé d'une première couche de portes XOR qui calculent le code de Hamming des 4 bits de données. Une seconde couche de portes XOR compare ce code calculé avec les trois bits d'ECC présents dans l'opérande. 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. Une couche de portes ET/NON sert de pseudo-décodeur, qui 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.

Pour ce qui est de calculer l'ECC en logiciel, c'est assez simple si on dispose d'une opération de population count. Il suffit d'appliquer un masque avant de calculer le bit de parité via la population count. Le masque adéquat est appliqué pour sélectionner les bits adéquats de l'opérande, puis on calcule la population count et ne garde que le bit de poids faible. En appliquant la procédure précédente pour toutes les combinaisons de bits d'opérandes nécessaires, on obtient les différents bits de parités. Il ne reste alors qu'à les combiner entre eux pour obtenir les données d'ECC nécessaires.


Dans ce chapitre, nous allons voir les circuits qui additionnent plus de deux nombres 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 pu en parler dans le prochain chapitre sur la multiplication, mais les circuits d'addition multiopérande étant un peu compliqués, nous en avons fait un chapitre propédeutique séparé.

Additionneur multiopérande de 4 bits, pour 3 opérandes.

L'interface de ces additionneurs est la même que celle des additionneurs normaux, sauf qu'ils disposent de plusieurs entrées pour les opérandes. L'un d'entre eux est illustré ci-contre, pour l'addition de trois opérandes de 4 bits. Il est préférable de voir les circuits d'addition multiopérande séparément des circuits pour la multiplication, pour diverses raisons pédagogiques, ce qui est fait dans ce cours.

Les implémentations naïves, non-optimisées

[modifier | modifier le wikicode]

À cet instant du cours, nous ne disposons que d'additionneurs 2-opérandes, à savoir qu'ils additionnent deux nombres. Pour créer un additionneur multiopérande, nous pouvons utiliser un ou plusieurs additionneurs 2-opérandes.

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.

Les autres circuits que nous verrons dans ce chapitre sont moins flexibles. Ils additionnent toujours le même nombre d'opérandes, ce qui n'est pas un problème dans les cas où ils sont utilisés. Rien n'empêche de mettre certaines opérandes à 0, ce qui permet de faire moins, de faire des calculs avec moins d'opérandes.

Les additionneurs multiopérande sériels et parallèles

[modifier | modifier le wikicode]
Additionneur multiopérande série versus parallèle.

Une autre méthode combine plusieurs additionneurs 2-opérandes. Et cette solution peut se mettre en œuvre de deux manières, illustrées ci-contre. La première solution utilise des additionneurs en série, placés l'un après l'autre. La seconde solution effectue certaines additions en parallèle d'autres : on obtient alors un additionneur parallèle.

Les additionneurs utilisés peuvent être n'importe quel additionneur 2-opérandes. Par exemple, si on utilise des additionneurs à propagation de retenue, le circuit d'un additionneur série de 3 opérandes de 4 bits est celui-ci :

Multiplieur en chaine fait avec des additionneurs à propagation de retenues.

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 en parallèle !

Sans rentrer dans des calculs de complexité algorithmique, la raison est liée à la propagation des retenues, qui limite les performances. 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, en enchainant des additionneurs à anticipation de retenue, certains additionneurs doivent attendre que des retenues arrivent. Idem si on les met en parallèle.

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 à plus de 3 opérandes

[modifier | modifier le wikicode]

L'addition carry save permet d'additionner trois opérandes assez simplement, avec un cout en circuit à peine plus élevé que pour l'addition deux-opérandes. Mais qu'en est-il pour additionner plus de trois opérandes ? L'idée est d'additionner les opérandes par groupes de deux/trois, avec une addition carry save pour chaque groupe de deux/trois, avant de combiner les résultats carry save entre elles.

Les additionneurs précédents peuvent être adaptés de manière à fonctionner avec des additionneurs carry save. 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 et un registre pour additionner les opérandes, avant d'envoyer le résultat final à un additionneur normal pour calculer le résultat final. Le circuit obtenu est un L'additionneur multiopérande hybride, mi-itératif, mi carry save.

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.

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

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]

Les arbres de Dadda sont plus difficiles à comprendre. Contrairement à l'arbre de Wallace qui cherche à réduire la hauteur de l'arbre le plus vite possible, l'arbre de Dadda cherche à diminuer le nombre d'additionneurs carry-save utilisés. Pour cela, l'arbre de Dadda se base sur un principe mathématique simple : un additionneur carry-save peut additionner trois nombres, pas plus. Cela implique que l'utilisation d'un arbre de Wallace gaspille des additionneurs si on additionne n nombres, avec n non multiple de trois.

L'arbre de Dadda résout ce problème d'une manière simple :

  • si n est multiple de trois, on ajoute une couche complète d'additionneurs carry-save ;
  • si n n'est pas multiple de trois, on ajoute seulement 1 ou 2 additionneur carry-save : le but est de faire en sorte que la couche suivante fournisse un nombre d'opérandes multiple de trois.

Et on répète cette étape d'ajout de couche jusqu'à ce qu'il ne reste plus que deux résultats : on se retrouve avec une couche finale composée d'un seul additionneur carry-save. Là, on rajoute un additionneur normal, pour additionner retenues et sommes.

Arbre de Dadda pour l'addition de 8 nombres de 8 bits.

Les additionneurs multiopérande basés sur des adder compressors

[modifier | modifier le wikicode]

L'usage d'additionneurs carry save est une première étape, mais il y a moyen d'aller encore plus loin. L'addition carry save permet d'additionner trois opérandes en même temps, mais on peut les additionner par paquets de 4 ou 5 opérandes. Dans le chapitre précédent, nous avons vu les compteurs parallèles, des circuits qui additionnent plusieurs bits et fournissent un résultat encodé en binaire. L'idée est de faire comme l'addition en carry save, sauf qu'au lieu d'utiliser des FA qui additionnent 3 bits à la fois, on utilise des compteurs parallèles qui additionnent 4/5/6/ bits d'un coup. Par contre, vu qu'ils fournissent plusieurs retenues, le circuit est plus complexe.

Le résultat est assez variable. Les adder compressors 4:2 sont souvent utilisés, mais ceux allant au-delà (qui fusionnent plus de 2 additionneurs complets) sont déjà plus controversés.

L'addition multiopérande à propagation de retenue avec des compteurs parallèles

[modifier | modifier le wikicode]

Additionner N opérandes demande d'additionner les bits sur la même colonne, ceux de même poids, avec éventuellement une retenue. Il se trouve qu'additionner les bits d'une même colonne revient à calculer la population count de cette colonne, ce que fait un compteur parallèle ! Cependant, le résultat du compteur parallèle sera codé sur 2, 3, 4 bits, voire plus. Il contiendra un bit de somme et deux bits de retenue. Les retenues doivent être propagées à la colonne suivante et additionnées avec les autres bits.

Il est donc possible de créer un circuit équivalent à un additionneur à propagation de retenue. Pour rappel, l'additionneur à propagation de retenue était composé de FA enchainés les uns à la suite des autres. Chacun additionnait deux bits, et la retenue provenant de la colonne précédente. Ici, les FA sont remplacés par des compteurs parallèles qui additionnent tous les bits de la colonne, mais aussi les retenues des colonnes précédentes. Un exemple avec une addition de 5 opérande est illustré ci-dessous : on voit qu'il y a deux retenues, une provenant de la colonne précédente, une provenant de deux colonnes avant. Le nombre de retenues et la distance des colonnes précédente varie suivant le nombre d'opérandes.

Addition multiopérande avec des compteurs parallèles, circuit à propagation de retenues.

L'addition multiopérande parallèle avec des compteurs parallèles

[modifier | modifier le wikicode]

Une autre solution permet de se passer de la propagation de la retenue. L'idée est de ne pas tenir compte des retenues, mais de les laisser à plus tard. Les bits d'une même colonne sont additionnés par un compteur parallèle, qui fournit un bit de somme et des retenues pour chaque colonne. Le tout est suivi par un autre additionneur multiopérande qui additionne les retenues et sommes.

En effet, il est possible de faire une analogie avec le carry save. Le carry save donne un résultat pour la somme et un pour les retenues. Mais avec des compteurs parallèles autres que des FA, on a un résultat somme et plusieurs résultats pour les retenues. Mais la logique reste la même : les différents résultats peuvent être additionnés pour donner le résultat. Il faut juste penser à décaler les retenues du nombre de rangs adéquat pour chaque retenues.

Addition multiopérande avec des adder compressor, sans propagation de retenue

Avec moins de 8 opérandes, on peut utiliser des compteurs parallèles assez directement. En effet, un compteur parallèle ayant 4 à 7 bits d'entrée fournit trois bits de résultat : un bit de somme, un premier bit de retenu et un bit de retenue de poids fort. Donc, l'additionneur fournit trois résultats : un pour les bits de somme, un autre pour les retenues intermédiaires, un troisième pour les retenues de poids fort.

Le circuit est donc composé de trois additionneurs à la suite. Un premier additionneur est composé de compteurs parallèles qui fournit trois résultats. Ces trois résultats sont additionnés avec un additionneur trois-opérandes carry save, en faisant attention au fait que les bits ne sont pas sur les mêmes colonnes. Les trois résultats sont à additionner, en tenant compte que les bits de chaque résultat sont décalés. Ils ne sont pas sur les mêmes colonnes : si un bit de somme est sur une colonne, la retenue intermédiaire se trouve sur la colonne suivante, et la retenue de poids fort sur la colonne encore suivante.

Additionneur multiopérande en carry save

Mais avec plus de 7 opérandes, on a plus de trois couches. Par exemple, prenons l'exemple d'un additionneur 64 opérandes. Il est tout d'abord composé d'une couche de compteurs parallèles qui additionnent chacun 64 bits et fournissent 7 bits par colonne. Puis, une seconde couche de compteurs parallèles additionne les 7 bits pour fournir 3 bits en sortie, le tout étant suivi par un additionneur carry save et un additionneur 2-opérande.


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.

La multiplication non-signée

[modifier | modifier le wikicode]

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]

Une première solution calcule tous les produits partiels en parallèle, en même temps, avant de les additionner avec un additionneur multi-opérandes non-itératif, composé d'additionneurs carry-save. C'est une solution simple, qui utilise beaucoup de circuits, mais est très rapide. C'est la solution utilisée dans les processeurs haute performance moderne, dans presque tous les processeurs grand public, depuis plusieurs décennies.

Multiplieur en arbre.

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.

Les multiplieurs itératifs

[modifier | modifier le wikicode]

Les multiplieurs les plus simples génèrent les produits partiels les uns après les autres, et les additionnent au fur et à mesure. Le multiplicateur et le multiplicande sont mémorisés dans des registres. Le reste du circuit est composé d'un circuit de génération des produits partiels, suivi d'un additionneur multiopérande itératif. La multiplication est finie quand tous les bits du multiplicateur ont étés traités (ce qui peut se détermine avec un compteur).

Circuit itératif de multiplication sans optimisation.

Rappelons que l'additionneur multiopérande itératif est composé d'un additionneur normal, à deux opérandes, couplé à un registre appelé le registre accumulateur. Il mémorise le résultat temporaire de l'addition des produits partiels. A la fin de la multiplication, une fois tous les produits partiels additionnés, il contient le résultat.

Circuit itératif de multiplication sans optimisation, détaillée.

Il existe plusieurs multiplieurs itératifs, qui différent par la façon dont ils génèrent le produit partiel. Dans tous les cas, la multiplication multiplie un bit du multiplicateur par le multiplicande. La différence tient dans le sens de parcours : certains traitent les bits du multiplicateur de droite à gauche, les autres dans le sens inverse. Dans le premier cas, le multiplieur subit un décalage à droite et il est traité de droite à gauche, des bits de poids faible vers les bits de poids fort. Dans le second cas, il subit un décalage à gauche et est traité de gauche à droite, des bits de poids fort vers les bits de poids faible.

Pour cela, on stocke le multiplieur dans un registre à décalage, ce qui fait qu'un bit sort à chaque cycle. Et c'est ce bit qui sort du registre à décalage qui est utilisé pour générer le produit partiel. Voici comment se déroule une multiplication avec un multiplieur qui fait le calcul de droite à gauche, qui commence par les bits de poids faible du multiplicateur :

Fonctionnement multiplieur.

Il faut noter que le contenu du registre accumulateur est aussi décalé d'un cran vers la gauche ou la droite à chaque cycle, pour tenir compte du poids des bits multipliés. Pour comprendre pourquoi, rappelez-vous que dans une multiplication, en décimal ou binaire, les produits partiels ne sont pas alignés sur une même colonne : ils sont 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. En théorie, le décalage doit être réalisé à la génération des produits partiels, par un circuit décaleur. Mais cette solution demande d'utiliser des produits partiels qui sont deux fois plus longs que le multiplicande/multiplicateur.

Une solution alternative, beaucoup plus simple, effectue ce décalage directement dans le registre accumulateur. En effet, chaque produit partiel est décalé d'un cran vers la gauche par rapport au précédent, d'un cran vers la droite par rapport au précédent. On peut donc faire le décalage entre chaque addition. Le registre accumulateur est donc un registre à décalage ! Ce qui n'est pas le cas sur un additionneur multiopérande itératif normal. 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.

Les deux solutions ne sont pas strictement équivalentes, car la seconde à un avantage. 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é

Les optimisations liées aux opérandes

[modifier | modifier le wikicode]

les circuits plus incorporent certaines optimisations, notamment concernant le sens de parcours du multiplieur, afin de rendre les calculs plus rapides. Mais d'autres optimisations permettent de gagner encore plus en performance, mais qui dépendent de 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. L'idée est de zapper certains cycles où on sait que le multiplicande sera multiplié par zéro, ce qui arrive quand le bit du multiplieur vaut 0. En théorie, on pourrait faire cela à chaque cycle, mais cela n'a pas d'intérêt, car contourner l'additionneur n'a pas grand intérêt. Mais on peut cependant faire quelques optimisations.

La première optimisation consiste à terminer l'opération une fois que tous les calculs nécessaires ont été faits, c'est à dire 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 su multiplieurs valent 0, alors il faudra décaler le résultat dans le registre accumulateur de N rangs vers la gauche.

Voyons maintenant une autre optimisation, qui va de pair avec l'optimisation précédente. Prenons d'abord une multiplication qui part du bit de poids fort, et décale le multiplieur vers la gauche. Elle fonctionne sur les opérandes dont les N bits de poids fort sont à 0. L'idée est de commencer la multiplication pour le premier bit du multiplieur à 1, et de zapper les 0 précédents en décalant le multiplier. Ce faisant, on utilise un circuit qui effectue l'opération adéquate, à savoir l'opération de count leading zeros, puis décale le multiplieur de ce nombre, ainsi que le reste partiel. La même optimisation s'applique si on commence la multiplication à partir du bit de poids faible, il faut alors effectuer l'opération count trailing zeros pour savoir de combien décaler.

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

Si on décale le multiplieur vers la droite, on ne doit gérer les 0 que dans les bits de poids fort. Pour gérer les bits de poids faible à 0, il suffit de tester si l'opérande est zéro, à savoir appliquer l'optimisation précédente. Les deux optimisations sont complémentaires, elles sont deux faces d'une même pièce.

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. On ne tient compte que d'un seul bit du multiplieur, qui est multiplié avec le multiplicande, ce qui génère un produit partiel. À l'inverse, avec les multiplieurs basés sur un additionneur multiopérande non-itératif, on génère tous les produits partiels en même temps, pour les additionner tous en même temps (ou presque). 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 qu'au lieu de faire la multiplication bit par bit pour le multiplicande, on prend deux bits du multiplicande, ou trois ou quatre. Concrètement, on génère deux, trois, quatre produits partiels en même temps et on les additionne d'un seul coup au résultat temporaire dans le registre. 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, mélange l'usage d'additionneurs carry save et multiplieur itératif. L'idée est simple : on génère plusieurs produits partiels à la fois, on les additionne avec le registre accumulateur avec un additionneur multiopérande normal. En clair, on 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 prendre en compte les décalages entre produits partiels. Déjà, 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 changeant son organisation. L'idée est de faire les additions de produits partiels 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. De plus, le registre accumulateur mémorise 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]

La méthode précédente utilise un multiplieur hybride. Mais il est aussi possible d'utiliser un multiplieur itératif normal, et de le modifier pour en faire un multiplieur en base 4, 8, 16, etc. Les méthodes pour cela sont moins intuitives, plus complexes, mais sont cependant intéressantes à étudier. Elles ne sont pas utilisées, car elles utilisent plus de circuits ou sont moins performantes.

Leur idée est d'additionner les produits partiels entre eux avant de les additionner au registre accumulateur. Le problème est que l'on ne va pas rajouter un second additionner dans le circuit, aussi diverses méthodes permettent de ruser. La ruse consiste à précalculer tous les produits partiels possibles et de les stocker dans des registres. Le choix du produit partiel à envoyer à l'additionneur se fait avec un MUX commandé par un paquet de 2, 3, 4 bits du multiplieur.

Prenons l'exemple le plus simple : celui d'un multiplieur en base 4, qui demande d’additionner deux produits partiels à la fois, de traiter l'opérande multiplieur par paquets de deux bits. Dans ce cas, 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.

Les multiplieurs diviser pour régner

[modifier | modifier le wikicode]

Il existe enfin un tout dernier type de multiplieurs : les multiplieurs diviser pour régner. Pour comprendre le principe, nous allons prendre un multiplieur qui multiplie deux nombres de 32 bits. Les deux opérandes A et B peuvent être décomposées en deux morceaux de 16 bits, qu'il suffit de multiplier entre eux pour obtenir les produits partiels voulus : une seule multiplication 32 bits se transforme en quatre multiplications d'opérandes de 16 bits. En clair, ces multiplieurs sont composés de multiplieurs qui travaillent sur des opérandes plus petites, associés à des additionneurs.

La multiplication de nombres signés

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


Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons surtout nous concentrer sur les nombres flottants au format IEEE754, mais feront un aparté sur les flottants logarithmiques. Rappelons que la norme IEEE754 précise le comportement de 5 opérations: l'addition, la soustraction, la multiplication et la division.

Normalisation in circuit

Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La normalisation corrige 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.

Avant le calcul, il y a aussi 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). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.

Il faut noter que 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.

Les multiplications/divisions flottantes

[modifier | modifier le wikicode]

Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.

La multiplication flottante

[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. Le circuit est donc composé d'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 que l'additionneur-soustracteur utilisé est un additionneur-soustracteur spécifiques à cette représentation.
  • 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.
Multiplieur flottant avec normalisation

La division flottante

[modifier | modifier le wikicode]

La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.

Pour le démontrer, prenons deux flottants et et divisons le premier par le second. On a alors :

On applique les règles sur les fractions :

On simplifie la puissance de 2 :

On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.

La racine carrée flottante

[modifier | modifier le wikicode]

Le calcul de la racine carrée d'un flottant est relativement simple. Par définition, la racine carrée d'un flottant vaut :

La racine d'un produit est le produit des racines :

Vu que , on a :

On voit qu'il suffit de calculer la racine carrée de la mantisse et de diviser l'exposant par deux (ou le décaler d'un rang vers la droite ce qui est équivalent). Voici le circuit que cela doit donner :

Racine carrée FPU

L'addition et la soustraction flottante

[modifier | 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 ce n'est pas le cas pour la plupart des calculs flottants qu'on souhaite faire, ce qui n’empêche cependant pas de ruser. L'idée est de mettre les deux flottants au même exposant, 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).

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

[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, mais il doit aussi passer par d'autres étapes dites de normalisation.

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 une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.

Circuit d'arrondi flottant basé sur une ROM.

Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.

Circuit de postnormalisation.

Le circuit de normalisation/arrondi final

[modifier | modifier le wikicode]

Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :

Circuit de normalisation-arrondi

Les flottants logarithmiques

[modifier | modifier le wikicode]

Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre Le codage des nombres, dans la section sur les flottants logarithmiques. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.

Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.

La multiplication et la division de deux flottants logarithmiques

[modifier | modifier le wikicode]

Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.

Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par , c'est multiplier par . Or, il faut se rappeler que . On obtient alors, en combinant ces deux expressions :

La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.

L'addition et la soustraction de deux flottants logarithmiques

[modifier | modifier le wikicode]

Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.

Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :

Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :

Pour rappel, les représentations de x et y en flottant logarithmique sont égales à et . En notant ces dernières et , on a :

Par définition, et . En injectant dans l'équation précédente, on obtient :

On simplifie la puissance de deux :

On a donc :

, avec f la fonction adéquate.

Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :

, avec g une fonction différente de f.

On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :

  • un circuit qui additionne/soustrait les deux opérandes ;
  • une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
  • et un autre additionneur pour le résultat.

Pour implémenter les quatre opérations, on a donc besoin :

  • de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
  • de deux autres additionneurs/soustracteur pour la multiplication et la division ;
  • et d'une ROM.

Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.

Unité de calcul logarithmique


Il est possible de créer des circuits qui effectuent des opérations trigonométriques, mais ceux-ci sont peu utilisés dans les ordinateurs actuels. La raison est que les calculs trigonométriques sont assez rares et ne sont réellement utilisés que dans les jeux vidéos (pour les calculs des moteurs physique et graphique), dans les applications graphiques de rendu 3D et dans les applications de calcul scientifique. Ils sont par contre plus courants dans les systèmes embarqués, bien que leur utilisation reste quand même assez peu fréquente.

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. Les circuits de ce chapitre sont rarement utilisés, sans compter qu'ils sont assez complexes. Je recommande d'aborder ce chapitre comme s'il s'agissait d'une annexe, pour ceux qui sont vraiment motivés.

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.

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 qui contient les résultats des calculs possibles. La raison est que tout circuit combinatoire existant peut être remplacé par une mémoire ROM.

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 importante pour être implémentée. Rien qu'avec des nombres à virgule fixe de plus de 16 bits, il faudrait une mémoire de 2^16 cases mémoire, chacune faisant 16 bits, soit 128 kiloctets, et ce pour une seule opération. Ne parlons même pas du cas avec des nombres de 32 ou 64 bits ! Pour cela, on va donc devoir ruser pour réduire la taille de cette ROM.

Mais qui dit réduire la taille de la ROM signifie la ROM mémorisera 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.


Les comparateurs sont des circuits qui permettent de comparer deux nombres, à savoir s'ils sont égaux, si l'un est supérieur à l'autre, ou inférieur, ou différent, etc. Dans ce qui va suivre, nous allons voir quatre types de comparateurs. Le premier type de circuit vérifie si un nombre est nul ou différent de zéro. Le second vérifie si deux nombres sont égaux, tandis que le troisième vérifie s'ils sont différent. Enfin, le quatrième vérifie si un nombre est supérieur ou inférieur à un autre. Il faut signaler que nous avons déjà vu comment vérifier qu'un nombre est égal à une constante dans le chapitre sur les circuits combinatoires, aussi nous ne reviendrons pas dessus.

Le comparateur de 1 bit

[modifier | modifier le wikicode]
Comparateur 1 bit.

Maintenant, nous allons voir comment vérifier si deux bits sont égaux/différents, si le premier est inférieur au second ou supérieur. Le but est de créer un circuit qui prend en entrée deux bits A et B, et fournit en sortie quatre bits :

  • qui vaut 1 si le bit A est supérieur à B et 0 sinon ;
  • qui vaut 1 si le bit A est inférieur à B et 0 sinon ;
  • qui vaut 1 si le bit A est égal à B et 0 sinon ;
  • qui vaut 1 si le bit A est différent de B et 0 sinon.

Peut-être avez-vous remarqué l'absence de sortie qui indique si ou . Mais ces deux sorties peuvent se calculer en combinant la sortie avec les sorties et . De plu, vous pouvez remarquer que les deux dernières entrées sont l'inverse l'une de l'autre, ce qui n'est pas le cas des deux premières. Nous allons d'abord voir comment calculer les deux premières sorties, à savoir et . La raison à cela est que les deux autres sorties peuvent se calculer à partir de celles-ci.

Le comparateur de supériorité/infériorité stricte

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

On voit que quand les deux bits sont égaux, alors les deux sorties sont à zéro.

Le comparateur d'inégalité 1 bit

[modifier | modifier le wikicode]

La seconde étape est de concevoir un petit circuit qui vérifie si deux bits sont différents, inégaux. Pour cela, on peut combiner les deux signaux précédents. En effet, on sait que si A et B sont différents, alors soit A>B, soit A<B. En conséquence, on a juste à combiner les deux signaux vus précédemment avec une porte OU.

Comparateur d'inégalité 1 bit.

Ce circuit devrait vous dire quelque chose. Pour ceux qui n'ont pas déjà remarqué, le mieux est d'établir la table de vérité du circuit, ce qui donne :

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

On voit rapidement qu'il s'agit d'une porte XOR.

Le comparateur d'égalité 1 bit

[modifier | modifier le wikicode]

Enfin, nous allons concevoir un petit circuit qui vérifie si deux bits sont égaux. La logique veut que l'égalité est l'inverse de l'inégalité, ce qui fait qu'il suffirait d'ajouter une porte NON en sortie du circuit. Le circuit précédent est alors facile à adapter : il suffit de remplacer la porte OU par une porte NOR. Et si le comparateur d'inégalité 1 bit était une porte XOR, on devine rapidement que le comparateur d'égalité 1 bit est quant à lui une porte NXOR. Ce qui se vérifie quand on regarde la table de vérité du comparateur :

A B A=B
0 0 1
0 1 0
1 0 0
1 1 1

L'interprétation de ce circuit est assez simple. Par définition, deux nombres sont égaux si les deux conditions et sont fausses. C’est pour cela que le circuit calcule d'abord les deux signaux et , puis qu'il les combine avec une porte NOR.

Le circuit complet

[modifier | modifier le wikicode]

Fort de ces résultats, nous pouvons fabriquer un comparateur de 1 bit. Celui-ci prend deux bits en entrée, et fournit trois bits en sortie : un pour l'égalité, un autre pour la supériorité et un autre pour l'infériorité. Voici le circuit au complet (sans le bit de différence). On pourrait rajouter le bit de différence assez simplement, en ajoutant une porte OU en sortie des signaux et , mais nous le ferons pas pour ne pas surcharger inutilement les schémas.

Comparateur de magnitude 1 bit.
Comparateur de magnitude 1 bit, alternatif.

Il est aussi possible de reformuler le schéma précédent pour supprimer une redondance invisible dans le circuit, au niveau de la porte NXOR qui calcule le bit d'égalité. À la place, il est possible de prendre les signaux et et de les combiner avec une porte NOR. Ou encore mieux : on utilise une porte OU et une porte NON, la sortie de la porte OU donnant directement le bit d'inégalité, celle de la porte NON donnant le bit d'égalité.

Comparateur 1 bit complet

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 dans les autres représentations pour les entiers signés. La raison est la présence de deux zéros : un zéro positif et un zéro négatif. Mais nous 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 et d'inverser son résultat avec une porte NON. Le résultat est que l'on change la porte ET finale du premier circuit par une porte NAND, la porte NOR du second circuit par une porte OU.

Les comparateurs pour entiers non-signés

[modifier | modifier le wikicode]
Comparateur 4 Bits.

Dans ce qui va suivre, nous allons créer un circuit qui est capable de vérifier l'égalité, la supériorité ou l'infériorité de deux nombres. Celui-ci prend deux nombres en entrée, et fournit trois bits : un pour l'égalité des deux nombres, un autre pour préciser que le premier est supérieur au second, et un autre pour préciser que le premier est inférieur au second.

Il existe deux méthodes pour créer des comparateurs : une basée sur une soustraction, l'autre sur des comparaisons bit par bit. Dans cette section, nous allons nous concentrer sur le second type, les comparateurs basés sur une soustraction étant vu à la fin du chapitre. De plus, nous allons nous concentrer sur des comparateurs qui testent deux opérandes non-signées (nulles ou positives, mais pas négatives).

Les comparateurs sériels

[modifier | modifier le wikicode]

La manière la plus simple est de faire la comparaison bit par bit avec un circuit séquentiel. Pour cela, on doit utiliser deux registres à décalage pour les opérandes. Les bits sortants à chaque cycle sont les bits de même poids et ils sont comparés par un comparateur 1 bit. Le résultat de la comparaison est mémorisé dans une bascule de quelques bits, qui mémorise le résultat temporaire de la comparaison. A chaque cycle, le comparateur de 1 bit fournit son résultat, qu'il faut combiner avec celui dans la bascule, ce qui permet de combiner les résultats temporaire avec les résultats de la colonne en cours de traitement. Et c'est une chose que le comparateur 1 bit précédent ne sait pas faire. Pour cela, nous allons devoir créer un circuit, nommé le comparateur complet.

Comparateur sériel.

Dans ce qui va suivre, la comparaison va s'effectuer en partant du bit de poids faible et se fera de gauche à droite, chose qui a son importance pour ce qui va suivre. Quel que soit le sens de parcours, on sait comment calculer le bit d'égalité : celui-ci est égal à un ET entre le bit d'égalité fournit en entrée, et le résultat de la comparaison A = B. Pour les bits de supériorité et d'infériorité, les choses changent : la colonne en cours de traitement supplante l'autre colonne. Dit autrement, les bits de poids fort donnent le résultat de la comparaison. Pour déterminer comment calculer ces bits, le mieux est encore d'établir la table de vérité du circuit pour ces deux bits. Nous allons postuler que les bits A > B et A < B ne peuvent être à 1 en même temps : il s'agit de supériorité et d'infériorité stricte. Voici la table de vérité :

Entrée > Entrée < A > B A < B Sortie > Sortie <
0 0 0 0 0 0
0 0 0 1 0 1
0 0 1 0 1 0
0 0 1 1 X X
0 1 0 0 0 1
0 1 0 1 0 1
0 1 1 0 1 0
0 1 1 1 X X
1 0 0 0 1 0
1 0 0 1 0 1
1 0 1 0 1 0
1 0 1 1 X X
1 1 0 0 X X
1 1 0 1 X X
1 1 1 0 X X
1 1 1 1 X X

Les équations logiques obtenues sont donc les suivantes :

  • Sortie (=) :
  • Sortie (>) :
  • Sortie (<) :

Il est possible de simplifier le circuit de manière à ce qu'il ne fasse pas la comparaison d'égalité, qui se déduit des deux autres comparaisons. Si les deux sorties < et > sont à 0, alors les deux bits sont égaux. Si on avait utilisé des sorties >= et <=, il aurait fallu que les deux bits soient à 1 pour montrer une égalité.

On peut aussi fabriquer un comparateur d'égalité sériel. Nous aurons à utiliser ce circuit plusieurs fois dans la suite du cours, notamment dans le chapitre sur les mémoires associatives. Le circuit est strictement identique au précédent, si ce n'est que l'on retire les portes pour les comparaisons non-voulues, et que la bascule n'a besoin que de mémoriser un seul bit. La comparaison d'égalité est réalisée par une porte NXOR, et le résultat est combiné avec le contenu de la bascule avec une porte ET.

Comparateur d'égalité sériel.

Les comparateurs parallèles

[modifier | modifier le wikicode]

Il est possible de dérouler le circuit précédent, de la même manière que l'on peut dérouler un additionneur sériel pour obtenir un additionneur à propagation de retenue. L'idée est de traiter les bits les uns après les autres, chacun avec un comparateur complet par colonne. Les comparateurs sont connectés de manière à traiter les opérandes dans un sens bien précis : soit des bits de poids faible vers ceux de poids fort, soit dans le sens inverse. Tout dépend du comparateur. Un tel circuit n'a pas l'air très optimisé et ne paye pas de mine, mais il a déjà été utilisé dans des processeurs commerciaux. Par exemple, c'est ce circuit qui était utilisé dans le processeur HP Nanoprocessor, un des tout premiers microprocesseurs.

Comparateur série

Un inconvénient de cette méthode est que le résultat est lent à calculer, car on doit traiter les opérande colonne par colonne, comme pour les additionneurs. Sur ces derniers, il fallait propager une retenue de colonne en colonne, mais diverses optimisations permettaient d'optimiser le tout, soit en accélérant la propagation de la retenue, soit en l’anticipant, soit en faisant les calculs de retenue en parallèle, etc. Pour les comparateurs, ce n'est pas une retenue qui se déplace, mais des résultats temporaires. Sauf qu'il n'y a pas de moyen simple pour faire comme avec les additionneurs/soustracteurs et d'anticiper ou calculer en parallèle ces résultats temporaires.

Les comparateurs combinés

[modifier | modifier le wikicode]

Il est possible de combiner plusieurs comparateurs simples pour traiter des nombres assez longs. Par exemple, on peut enchainer 2 comparateurs série 4 bits pour obtenir un comparateur série 8 bits. Pour cela, il y a deux grandes solutions : soit on enchaine les comparateurs en série, soit on les fait travailler en parallèle.

La première méthode place les comparateurs en série, c'est à dire que le second comparateur prend le résultat du premier pour faire son travail. Le second comparateur doit avoir trois entrées nommées <, > et =, qui fournissent le résultat du comparateur précédent. On peut faire la même chose avec plus de 2 comparateurs, l'essentiel étant que les comparateurs se suivent.

Interface d'un comparateur parallèle avec "retenues".

Une autre solution est de faire les comparaisons en parallèle et de combiner les bits de résultats des différents comparateurs avec un dernier comparateur. Le calcul de la comparaison est alors légèrement plus rapide qu'avec les autres méthodes, mais le circuit devient plus compliqué à concevoir.

Comparateur entier parallèle.

Les comparateurs pour nombres signés

[modifier | modifier le wikicode]

Comparer deux nombres signés n'est pas la même chose que comparer deux nombres non-signés. Autant la comparaison d'égalité et de différence est strictement identique, autant les comparaisons de supériorité et d'infériorité ne le sont pas. Les comparateurs pour nombres signés sont naturellement différents des comparateurs vus précédemment. On verra plus tard qu'il en est de même avec les comparateurs pour nombres flottants. Il fut aussi faire attention : la comparaison ne s'effectue pas de la même manière selon que les nombres à comparer sont codés en signe-magnitude, en complément à deux, ou dans une autre représentation. Dans ce qui va suivre, nous allons aborder la comparaison en signe-magnitude et en complément à deux, mais pas les autres représentations.

Le comparateur signe-magnitude

[modifier | modifier le wikicode]

Un comparateur en signe-magnitude n'est pas trop différent d'un comparateur normal. Effectuer une comparaison entre deux nombres en signe-magnitude demande de comparer les valeurs absolues, ainsi que comparer les signes. Une fois cela fait, il faut combiner les résultats de ces deux comparaisons en un seul résultat.

Comparateur en signe-magnitude

Comment comparer les signes ?

[modifier | modifier le wikicode]

Comparer les signes est relativement simple : le circuit n'ayant que deux bits à comparer, celui-ci est naturellement simple à concevoir. On pourrait penser que l'on peut réutiliser le comparateur 1 bit vu précédemment dans ce chapitre, mais cela ne marcherait pas ! En effet, un nombre positif a un bit de signe nul alors qu'un nombre négatif a un bit de signe égal à 1 : les résultats sont inversés. Un bon moyen de s'en rendre compte est d'écrire la table de vérité de ce circuit, selon les signes des nombres à comparer. Nous allons supposer que ces deux nombres sont appelés A et B.

Bit de signe de A Bit de signe de B Sortie = Sortie < Sortie >
0 (+) 0 (+) 1 0 0
1 (-) 0 (+) 0 0 1
0 (+) 1 (-) 0 1 0
1 (-) 1 (-) 1 0 0

On obtient les équations suivantes :

  • Sortie = :
  • Sortie < :
  • Sortie > :

On peut remarquer que si le bit d'égalité est identique au comparateur 1 bit vu plus haut, les bits de supériorité et l'infériorité sont inversés, échangés. On peut donc réutiliser le comparateur à 1 bit, mais en intervertissant les deux sorties de supériorité et d'infériorité.

Comment combiner les résultats ?

[modifier | modifier le wikicode]

Une fois qu'on a le résultat de la comparaison des signes, on doit combiner ce résultat avec le résultat de la comparaison des valeurs absolues. Pour cela, on doit rajouter un circuit à la suite des deux comparateurs. On pourrait penser qu'il suffit d'établir la table de vérité de ce comparateur, mais il faut faire attention au cas où les deux opérandes sont nulles : dans ce cas, peut importe les signes, les deux opérandes sont égales. Pour le moment, mettons ce fait de côté et établissons la table de vérité du circuit. Dans tous les cas, la comparaison des bits de signe prend le pas sur la comparaison des valeurs absolues. Par exemple, 2 est supérieur à -255, bien que ce ne soit pas le cas pour les valeurs absolues. Ce fait n'est que l'expression du fait qu'un nombre positif est systématiquement supérieur à un négatif, de même qu'un négatif est systématiquement inférieur à un positif. Ce n'est que quand les deux bits de signes sont égaux que la comparaison des valeurs absolue est à prendre en compte. Cela devrait donner les équations suivantes. On note les résultats de la comparaison des bits de signe comme suit : , alors que les résultats de la comparaison des valeurs absolues sont notés : .

  • pour le résultat d'égalité ;
  • pour l’infériorité ;
  • pour la supériorité.

Néanmoins, il faut prendre en compte le cas où les deux opérandes sont nulles, ce qui complique un petit peu les équations Pour cela, il fat rajouter une sortie au comparateur de valeurs absolues, qui indique si les deux nombres valent tous deux zéro. Notons cette sortie . Les équations deviennent :

  • pour l'égalité ;
  • pour l’infériorité ;
  • pour la supériorité.

Les comparateurs en complément à un et en complément à deux

[modifier | modifier le wikicode]

La comparaison en complément à un ou à deux peut s'implémenter comme en signe-magnitude : on compare les valeurs absolues, puis les signes, avant de déterminer le résultat. Reste à calculer les valeurs absolues, ce qui est loin d'être simple.

Circuit de comparaison en complément à 1 et à 2

En complément à 1, il suffit d'inverser tous les bits si le bit de signe est de 1. E, complément à deux, il faut faire pareil, puis incrémenter le résultat.

Circuit de comparaison entière en complément à 1.

Les comparateurs basés sur un soustracteur

[modifier | modifier le wikicode]

Les comparateurs présents dans les ordinateurs modernes fonctionnent sur un principe totalement différents. Ils soustraient les deux opérandes et étudient le résultat. Le résultat peut avoir quatre propriétés intéressantes lors d'une comparaison : si le résultat est nul ou non, son bit de signe, s'il génère une retenue sortante (débordement entier non-signé) et s'il génère un débordement entier en complément à deux. Ces quatre propriétés sont extraites du résultat, et empaquetées dans 4 bits, appelés les 4 bits intermédiaires. En effectuant quelques opérations sur ces 4 bits intermédiaires, on peut déterminer toutes les conditions possibles : si la première opérande est supérieure à l'autre, si elle est égale, si elle est supérieure ou égale, etc. Le circuit est donc composé de trois sous-circuits : le soustracteur, un circuit qui calcule les 4 bits intermédiaires, puis un autre qui calcule la ou les conditions voulues.

L'avantage de ce circuit est qu'il est plus rapide que les autres comparateurs. Rappelons que les comparateurs parallèles testent les opérandes colonne par colonne, et qu'on a pas de moyens simples pour éviter cela. Avec un soustracteur, on peut utiliser des techniques d'anticipation de retenue pour accélérer les calculs. Les calculs sont donc faits en parallèle, et non colonne par colonne, ce qui est beaucoup plus rapide, surtout quand les opérandes sont un peu longues. Ce qui fait que tous les processeurs modernes utilisent le circuit suivant pour faire des comparaisons. Il faut dire qu'il n'a que des avantages : plus rapide, il utilise un peu plus de portes logiques mais cela reste parfaitement supportable, il permet de générer des bits intermédiaires utiles que les autres comparateurs peinent à fournir, etc. De plus, il peut comparer aussi bien des entiers codés en complément à deux que des entiers non-signés, là où les comparateurs précédents ne le permettaient pas.

La génération des conditions

[modifier | modifier le wikicode]

La génération des 4 bits intermédiaire est simple et demande peu de calculs. Déterminer si le résultat est nul demande juste d'utiliser un comparateur avec zéro, qui prend en entrée le résultat de la soustraction. La génération des bits de débordement a été vue dans le chapitre sur les circuits d'addition et de soustraction, aussi pas besoin de revenir dessus. Par contre, il est intéressant de voir comment les 4 bits intermédiaires sont utilisés pour générer les conditions voulues.

Déjà, les deux opérandes sont égales si le résultat de leur soustraction est nul, et elles sont différentes sinon. Le bit qui indique si le résultat est nul ou non indique si les deux opérandes ont égales ou différentes. Cela fait déjà deux conditions de faites. De plus, l'égalité et la différence se calculent de la même manière en complément à deux et avec des entiers non-signés.

Pour ce qui est de savoir quelle opérande est supérieure ou inférieure à l'autre, cela dépend de si on analyse deux entiers non-signés ou deux entiers en complément à deux.

  • Avec des entiers non-signés, il faut regarder si la soustraction génère une retenue sortante, un débordement entier non-signé. Si c'est le cas, alors la première opérande est inférieure à la seconde. Si ce n'est pas le cas, alors la première opérande est supérieure ou égale à la seconde.
  • Pour la comparaison en complément à deux on se dit intuitivement qu'il faut regarder le signe du résultat. L'intuition nous dit que la première opérande est inférieure à l'autre si le résultat est négatif, qu'elle est supérieure ou nulle s'il est positif. Sauf que cela ne marche pas exactement comme cela : il faut aussi regarder si le calcul entraine un débordement d'entier en complément à deux. La règle est que le résultat est négatif si on a un débordement d'entier en complément à deux OU que le bit de signe est à 1. Une seule des deux conditions doit être respectée : le résultat est positif sinon. On doit donc faire un XOR entre le bit de débordement et le bit de signe. Le bit sortant de la porte XOR indique que le résultat de la soustraction est négatif, et donc que la première opérande est inférieure à la seconde.
Circuit de comparaison entière en complément à 2.

Avec ces deux informations, égalité et supériorité, on peut déterminer toutes les autres. Dans ce qui va suivre, nous allons partir du principe que le soustracteur est suivi par des circuits qui calculent les bits suivants :

  • un bit S qui n'est autre que le bit de signe en complément à deux ;
  • un bit Z qui indique si le résultat est nul ou non (1 pour un résultat nul, 0 sinon) ;
  • un bit C qui n'est autre que la retenue sortante (1 pour un débordement d'entier non-signé, 0 sinon) ;
  • un bit D qui indique un débordement d'entier signé (1 pour un débordement d'entier signé, 0 sinon).
Condition testée Calcul
A == B Z = 1
A != B Z = 0
Opérandes non-signées
A < B C = 1
A >= B C = 0
A <= B C OU Z = 1
A > B C ET Z = 0
Opérandes en complément à deux
A < B S XOR D = 1
A >= B S XOR D = 0
A <= B (S XOR D) OU Z = 1
A > B (S XOR D) ET Z = 0

Le comparateur-soustracteur total

[modifier | modifier le wikicode]

Il est possible de raffiner ce circuit, de manière à pouvoir choisir la condition en sortie du circuit. L'idée est que le circuit possède une seule sortie, sur laquelle on a le résultat de la condition choisie. Le circuit possède une entrée sur laquelle on envoie un nombre, qui permet de choisir la condition à tester. Par exemple, on peut vouloir vérifier si deux nombres sont égaux. Dans ce cas, on configure le circuit en envoyant sur l'entrée de sélection le nombre qui correspond au test d'égalité. Le circuit fait alors le test, et fournit le résultat sur la sortie de 1 bit. Ce circuit se construit simplement à partir du circuit précédent, en ajoutant un multiplexeur. Concrètement, on prend un soustracteur, on ajoute des circuits pour calculer les bits intermédiaires, puis on ajoute des portes pour calculer les différentes conditions, et enfin, on ajoute un multiplexeur.

Calcul d'une condition pour un branchement

Nous réutiliserons ce circuit bien plus tard dans ce cours, dans les chapitres sur les branchements. Mais ne vous inquiétez pas, dans ces chapitres, nous ferons des rappels sur les bits intermédiaires, la manière dont sont calculées les conditions et globalement sur tout ce qui a été dit dans cette section. Pas besoin de mémoriser par coeur les équations de la section précédente, tout ce qui compte est que vous ayez retenu le principe général.


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

Les CNA sont utilisés dans les cartes son, et ils étaient utilisés dans les anciennes cartes graphiques. Par exemple, il y a un CAN intégré à la carte son, qui sert à convertir le signal provenant d'un microphone en un signal numérique utilisable par la carte son. Il existe aussi un CNA, qui cette fois convertit le signal provenant de la carte son en signal analogique à destination des haut-parleurs. Mais nous reverrons cela dans quelques chapitres.

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 aussi présent sur la plupart des nouveaux modèles. Pour s'interfacer à l'entrée VGA, la carte graphique incorporait un circuit CNA pour transformer les données numériques des pixels en signaux analogiques à envoyer sur l'entrée VGA. De nos jours, la plupart des écrans ont des entrées numériques, et la conversion numérique-analogique est réalisée dans l'écran lui-même.

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. Le lien entre nombre binaire et tension de sortie varie pas mal selon le CNA, mais la plupart sont des convertisseurs dits linéaires.

CNA de 8 bits.
Exemple avec un CNA de 2 bits : chaque nombre binaire de 2 bits correspond à un intervalle de tension précis, tous identiques.

On peut expliquer leur fonctionnement de deux manières différentes. Une première manière de voir un CAN linéaire est de regarder l'association entre tension de sortie et nombre binaire. L'intervalle de la tension de sortie est découpé en sous-intervalles de même taille, chacun d'entre eux se voyant attribuer un nombre binaire. Des sous-intervalles consécutifs codent des intervalles consécutifs, le premier codant un 0 et le dernier la valeur maximale .

La taille de chaque sous-intervalle est appelé le quantum de tension et vaut . Il s'agit de la différence de tension minimale que l'on obtient en changeant l'entrée. En clair, la différence de tension en sortie entre deux nombres binaires consécutifs, est toujours la même, égale au quantum de tension. Par exemple, supposons qu'un 5 et un 6 en binaire donneront des tensions différentes de 1 volt. Alors ce sera la même différence de tension entre un 10 binaire et un 11, entre un 1000 et 1001, etc. La seconde manière de les voir est de considérer que la tension de sortie est proportionnelle au nombre à convertir, le coefficient de proportionnalité n'étant autre que le quantum de tension.

CNA linéaire.

Les CNA uniformes (non-pondérés)

[modifier | modifier le wikicode]

Le CNA peut être construit de diverses manières, qui utilisent toutes des composants analogiques nommés résistances et amplificateurs analogiques, que vous avez certainement vu en cours de collège ou de lycée.

Le premier type utilise autant de générateurs de tension qu'il y a de valeurs possibles en sortie. En clair, ce CNA possède générateurs de tension (en comptant la masse et la tension d'alimentation). L'idée est de connecter le générateur qui fournit la tension de sortie et de déconnecter les autres. Chaque connexion/déconnexion se fait par l'intermédiaire d'un interrupteur commandable, à savoir une porte à transmission et/ou un transistor. Pour faire le lien entre chaque porte à transmission et la valeur binaire, on utilise un décodeur. Il suffit de relier chaque sortie du décodeur (qui correspond à une entrée unique) au transistor (la tension) qui correspond.

CNA uniforme (non-pondéré).

Les CNA pondérés en binaire

[modifier | modifier le wikicode]

Il est maintenant temps de passer aux CNA pondérés. L'idée qui se cache derrière les circuits que nous allons voir est très simple. Partons d'un nombre binaire de bits . Si le bit correspond à un quantum de tension , alors la tension correspondant au bit est de , celle de est de , etc. Une fois chaque bit convertit en tension, il suffit d'additionner les tension obtenues pour obtenir la tension finale. Toute la difficulté est de convertir chaque bit en tension, puis d'additionner le tout. C’est surtout l'addition des tensions qui pose problème, ce qui fait que la plupart des circuits convertit les bits en courants, plus faciles à additionner, avant de convertir le résultat final en tension. Dans ce qui va suivre, nous allons voir deux circuits : les CNA pondérés à résistances équilibrées et non-équilibrées.

CNA à résistances non-équilibrées. CNA à résistances équilibrées.

Le CNA à résistances non-équilibrées

[modifier | modifier le wikicode]

Le circuit suivant utilise des résistances pour convertir un bit en un courant proportionnel à sa valeur. Rappelons que chaque bit est codé par une tension égale à la tension d'alimentation (pour un 1) ou un 0 volt (pour un 0). Cette tension est convertie en courant par un interrupteur, une tension et une résistance. Le courant est obtenu en faisant passer une tension à travers une résistance, l'interrupteur ouvrant ou fermant le circuit selon le bit à coder. Quand le bit est de zéro, l'interrupteur s'ouvre, et le courant ne passe pas : il vaut 0. Quand le bit est à 1, l'interrupteur se ferme et le courant est alors mis à sa valeur de conversion. La valeur de la résistance permet de multiplier chaque bit par son poids (par 1, 2, 4, , 16, ...) : c'est pour cela qu'il y a des résistances de valeur R, 2R, 4R, 8R, etc. Les courants en sortie de chaque résistance sont ensuite additionnés par le reste du circuit, avant d'être transformé en une tension proportionnelle.

Convertisseur numérique-analogique

Le CNA à résistances équilibrées

[modifier | modifier le wikicode]

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 portes ET-OU-NON et OU-ET-NON

[modifier | modifier le wikicode]

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

3-1-OAI

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

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

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

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

Dans la suite du cours, nous verrons quelques circuits qui utilisent cette technologie, mais ils seront rares. Nous l'utiliserons quand nous parlerons des additionneurs, ou les multiplexeurs, guère plus. Mais il est sympathique de savoir que cette technologie existe.

La porte à transmission

[modifier | modifier le wikicode]

Le circuit de base est une porte à transmission, à savoir une porte logique qui agit comme une sorte d'interrupteur, qui s'ouvre ou se ferme suivant ce qu'on met sur l'entrée de commande. Le circuit peut soit connecter l'entrée et la sortie, soit déconnecter la sortie de l'entrée. Le choix entre les deux dépend de l’entrée de commande.

Intuitivement, on se dit qu'une transistor fonctionne déjà comme un interrupteur, mais une porte à transmission est construit avec deux transistors. La raison la plus intuitive est que la logique CMOS fait que tout transistor PMOS doit être associé à un transistor NMOS et réciproquement. 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 le suivant :

CMOS Transmission gate

Vous remarquerez que le circuit est fondamentalement différent des circuits précédents. Les précédents connectaient la sortie soit à la tension d'alimentation, soit à la masse. Ici, la sortie est connectée sur l'entrée, rien de plus. Il n'y a pas d'alimentation électrique ni de contact à la masse. Retenez ce point, il sera important par la suite.

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.

Les multiplexeurs 2 vers 1 en pass transistor logic

[modifier | modifier le wikicode]

Dans les chapitres précédents, nous avions vu que les portes à transmission sont assez peu utilisées. Nous ne nous en sommes servies que dans de rares cas, mais l'un d'entre eux va nous intéresser : les multiplexeurs et les démultiplexeurs. Pour rappel, il est assez simple de fabriquer un multiplexeur 2 vers 1 en utilisant des portes à transmission. L'idée est de relier chaque entrée à la sortie par l'intermédiaire d'une porte à transmission. Quand l'une sera ouverte, l'autre sera fermée. Le résultat n'utilise que deux portes à transmission et une porte NON. 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 pass transistor logic utilise des multiplexeurs 2 vers 1

[modifier | modifier le wikicode]

La pass transistor logic permet d'implémenter des multiplexeurs assez facilement, en combinant des portes NON avec des portes à transmission. Et la pass transistor logic en profite pour implémenter les portes logiques d'une manière assez étonnante : les portes logiques sont basées sur un multiplexeur 2 vers 1 amélioré ! Un multiplexeur 2 vers 1 peut être utilisé pour implémenter de nombreux circuits différents. Par exemple, il peut être utilisé pour implémenter des portes logiques, tout dépend de ce qu'on met sur ses entrées.

L'idée est d'émuler une porte logique à deux entrées avec un multiplexeur 2 vers 1. Et intuitivement, vous vous dites que les deux entrées de la porte logique correspondent aux deux entrées de donnée du multiplexeur. Mais non, c'est une erreur ! 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.

Portes logiques faites à partir de multiplexeurs

Ils peuvent aussi être utilisés pour implémenter une bascule D, (pour rappel : une petite mémoire de 1 bit), comme on l'a vu dans les chapitres sur les bascules. Il suffit pour cela de boucler la sortie d'un multiplexeur sur une entrée, en ajoutant deux portes NON dans la boucle pour régénérer le signal électrique.

Implémentation alternative d'une bascule D.

La porte XOR en pass transistor logic

[modifier | modifier le wikicode]

Il est facile d'implémenter une porte XOR avec un multiplexeur 2 vers 1. 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. Un tel circuit commandable n'est autre qu'une porte logique XOR, qui XOR A et B. Et cela nous dit comment implémenter une porte XOR avec un multiplexeur : il suffit de prendre un multiplexeur qui choisit sa sortie parmi deux entrées : A et  ! Pour deux bits A et B, l'un est envoyé sur l'entrée de commande, l'autre bit est envoyée sur les deux entrées (le bit sur une entrée, son inverse sur l'autre). Le circuit obtenu, sans les portes NON, est celui-ci :

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

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

XOR en pass transistor logic

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

Quelques processeurs implémentaient leurs portes XOR en pass transistor logic, alors que les autres portes étaient en CMOS normal. Un exemple est le mythique processeur Z80.

Les avantages et défauts de la pass transistor logic

[modifier | modifier le wikicode]

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 d'une porte à transmission est alimentée par la tension d'entrée. Et vu que les transistors ne sont pas parfaits, on a toujours une petite perte de tension en sortie d'une porte à transmission.

Le résultat est que si on enchaine les portes à transmission, la tension de sortie a tendance à diminuer, et ce d'autant plus vite qu'on a enchainé de portes à transmission. 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 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 pahse 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 de base, à savoir les circuits vus dans les chapitres précédents. Le circuit intégré est conçu en combinant 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 résultat est 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 découpée en plusieurs bytes, des blocs de mémoire qui contiennent chacun un nombre fini et constant de bits. Le plus souvent, ces bytes sont composés de plusieurs groupes de 8 bits, appelés des octets. Mais certaines mémoires assez anciennes utilisaient des cases mémoires contenant 1, 2, 3, 4, 7, 18, 36 bits. Si les notions de byte et d'octet sont très différentes, la confusion est souvent faite et elle le sera dans ce cours.

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.

Le fait que les mémoires aient presque toutes des bytes faisant un ou plusieurs octets nous arrange pour compter la capacité d'une mémoire. Au lieu de compter cette capacité en bits, on préfère mesurer la capacité d'une mémoire en donnant le nombre d'octets que celle-ci peut contenir. Mais les mémoires des PC font plusieurs millions ou milliards d'octets. Pour se faciliter la tâche, on utilise des préfixes pour désigner les différentes capacités mémoires. Vous connaissez sûrement ces préfixes : kibioctets, mébioctets et gibioctets, notés respectivement Kio, Mio et Gio.

Préfixe Capacité mémoire en octets Puissance de deux
Kio 1024 210 octets
Mio 1 048 576 220 octets
Gio 1 073 741 824 230 octets

On peut se demander pourquoi utiliser des puissances de 1024, et ne pas utiliser des puissances un peu plus communes ? Dans la majorité des situations, les électroniciens préfèrent manipuler des puissances de deux pour se faciliter la vie. Par convention, on utilise souvent des puissances de 1024, qui est la puissance de deux la plus proche de 1000. Or, dans le langage courant, kilo, méga et giga sont des multiples de 1000. Quand vous vous pesez sur votre balance et que celle-ci vous indique 58 kilogrammes, cela veut dire que vous pesez 58 000 grammes. De même, un kilomètre est égal à 1000 mètres, et non 1024 mètres.

Autrefois, on utilisait les termes kilo, méga et giga à la place de nos kibi, mebi et gibi, par abus de langage. Mais peu de personnes sont au courant de l'existence de ces nouvelles unités, et celles-ci sont rarement utilisées. Et cette confusion permet aux fabricants de disques durs de nous « arnaquer » : Ceux-ci donnent la capacité des disques durs qu'ils vendent en kilo, mega ou giga octets : l’acheteur croit implicitement avoir une capacité exprimée en kibi, mebi ou gibi octets, et se retrouve avec un disque dur qui contient moins de mémoire que prévu.

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 accesible 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 accesible par le processeur. A 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.

Sur une mémoire RAM ou ROM, on ne peut lire ou écrire qu'un byte, qu'un registre à la fois : une lecture ou écriture ne peut lire ou modifier qu'un seul byte. Techniquement, le processeur doit préciser à quel byte il veut accéder à chaque lecture/écriture. Pour cela, chaque byte se voit attribuer un nombre binaire unique, l'adresse, qui va permettre de le sélectionner et de l'identifier celle-ci parmi toutes les autres. En fait, on peut comparer une adresse à un numéro de téléphone (ou à une adresse d'appartement) : chacun de vos correspondants a un numéro de téléphone et vous savez que pour appeler telle personne, vous devez composer tel numéro. Les adresses mémoires en sont l'équivalent pour les bytes.

Une autre explication est que l'adresse donne la position dans la mémoire d'une donnée.

Exemple : on demande à la mémoire de sélectionner le byte d'adresse 1002 et on récupère son contenu (ici, 17).

Il existe des mémoires qui ne fonctionnent pas sur ce principe, mais passons : ce sera pour la suite.

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.

Un processeur contient des registres et communique avec la mémoire

[modifier | modifier le wikicode]

Un processeur contient des circuits de calcul, des circuits annexes pour gérer les instructions, mais aussi des registres. 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. Tout processeur contient des registres pour fonctionner, leur utilité dépendant du registre considéré. 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.

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.

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.

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]

L'architecture de base vue plus haut est une architecture assez simple. Tous les ordinateurs modernes, mais aussi dans les smartphones, les consoles de jeu et autres, utilisent cependant une version grandement modifiée et améliorée. Les ordinateurs modernes utilisent un grand nombre de périphériques, ont des systèmes d'exploitation sur des disques durs/SSD, il y a 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 cette architecture de base.

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, et allons voir que même des systèmes assez simples et anciens apportent des modifications à l'architecture de base. La console de base a une architecture très simple, avec seulement un CPU, de la RAM, et quelques entrées-sorties. Malgré tout, on trouve des détails assez intéressants. L'architecture de la NES est illustrée ci-dessous.

Architecture de la NES

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. Ce processeur est associé à 2 KB de mémoire RAM et à une mémoire ROM. La mémoire ROM se trouve dans la cartouche de jeu, et non dans la console : Le programme du jeu est dans la cartouche, dans cette ROM. On trouve aussi une RAM dans la cartouche, qui est utilisée pour les sauvegardes, et 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. A 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. A 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. A 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 tous les bytes fonctionnent bien. Il arrive que les mémoires RAM aient des pannes : certains bytes tombent en panne après quelques années d'utilisation, et deviennent inaccessibles. Lorsque cela arrive, tout se passe bien tant que les bytes défectueux ne sont pas lus ou écrits. Mais quand cela arrive, les lectures renvoient des données incorrectes. Les conséquences peuvent être très variables, mais cela cause généralement des bugs assez importants, voire des écrans ou de beaux plantages. De nombreux cas d'instabilité système sont liés à ces bytes défectueux. Il est possible de vérifier l'intégrité de la mémoire avec des logiciels spécialisés, qui vérifient chaque byte de la mémoire un par un. Les systèmes d'exploitation modernes incorporent un logiciel de ce genre, comme Windows qui en a un d'intégré. Le BIOS ou l'UEFI de votre ordinateur a de bonnes chances d'intégrer un logiciel de ce genre. Ces logiciels de diagnostic mémoire balayent la mémoire byte par byte, case mémoire par case mémoire, et effectuent divers traitements dessus. Dans le cas le plus simple, ils écrivent une donnée dans chaque byte, avant de le lire : si la donnée lue et écrite ne sont pas la même, le byte est défectueux. Mais d'autres traitements sont possibles. Toujours est-il que ces utilitaires balayent la mémoire, généralement plusieurs fois. Le temps de balayage donne alors une idée du temps que mettront ces logiciels de diagnostic pour s’exécuter.

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

A forte intensité calculatoire, on fait beaucoup de calculs comparé aux accès mémoires. On demande donc plus au processeur qu'à la mémoire. A 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.

L'évolution de la fréquence des mémoires DRAM et de la fréquence du bus mémoire

[modifier | modifier le wikicode]

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 avec l'évolution des processeurs : 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.

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.

Le Power Gating et le Clock Gating

[modifier | modifier le wikicode]
Power Gating.

Une première solution consiste à ne pas dépenser d'énergie inutilement, ne pas alimenter ce qui ne fonctionne pas, ce qui est en pause ou inutilisé, afin qu'ils ne consomment plus de courant : on parle de power gating. Elle s'implémente en utilisant des Power Gates qui déconnectent les circuits de la tension d'alimentation quand ceux-ci sont inutilisés. Cette technique est très efficace, surtout pour couper l'alimentation du cache du processeur. Cette technique réduit la consommation statique des circuits, mais pas leur consommation dynamique, par définition.

Une autre solution consiste à jouer sur la manière dont l'horloge est distribuée dans le processeur. On estime qu'une grande partie des pertes ont lieu dans l'arbre d'horloge (l'ensemble de fils qui distribuent l'horloge aux bascules), approximativement 20 à 30% (mais tout dépend du processeur). La raison est que l'horloge change d'état à chaque cycle, même si les circuits cadencés par l'horloge sont inutilisés, et que la dissipation thermique a lieu quand un bit change de valeur. S'il est possible de limiter la casse en utilisant des bascules spécialement conçues pour consommer peu, il est aussi possible de déconnecter les circuits inutilisés de l'horloge : on appelle cela le Clock Gating.

Clock gating.

Pour implémenter cette technique, on est obligé de découper le processeur en plusieurs morceaux, reliés à l'horloge. Un morceau forme un tout du point de vue de l'horloge : on pourra tous le déconnecter de l'horloge d'un coup, entièrement. Pour implémenter le Clock Gating, on dispose entre l'arbre d'horloge et le circuit, une 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.

A l'époque, de nombreux jeux ou logiciels interactifs se basaient sur la fréquence du CPU pour timer 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 avait une fréquence de 4,77 MHz et le 8086 allait à 5 MHz. 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]

LIntel Management Engine (ME) et lAMD Platform Security Processor (PSP) sont des microcontrôleurs intégrés au chipset de la carte mère. Il a 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. A un instant t, seul l'émetteur et le récepteur sont connectés au bus. Mais cela demande pouvoir déconnecter du bus les entrées/sorties qui n'envoient pas de données. Plus précisément, leurs sorties peuvent être mises dans un état de haute impédance, qui n'est ni un 0 ni un 1. Quand une sortie est en haute impédance, elle n'a pas la moindre influence sur le bus et ne peut donc pas y écrire. Tout se passe comme si elle était déconnectée du bus, et dans les faits, elle l'est souvent.

Dans le chapitre sur les circuits intégrés, nous avons vu qu'il existait trois types de sorties : les sorties totem-pole, à drain/collecteur ouvert, et trois-état. Les sorties totem-pole fournissent soit un 1, soit un zéro, et ne peuvent pas être déconnectées proprement dit. Les deux autres types de sorties en sont capables. Et nous allons les voir dans ce qui suit.

L'interfaçage avec le bus avec des circuits trois-états

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


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]
Mémoire à bande magnétique.

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. Le support magnétique est généralement un plateau dont la surface est aimantée et aimantable, comme sur les disques durs et disquettes, ou une bande magnétique similaire à celle des vielles cassettes audio. Elles avaient des performances inférieures aux mémoires électroniques, mais une meilleure capacité, d'où leur utilisation en tant que mémoire de masse. Un autre de leur avantage est qu'elles ont une durée de vie assez importante, liée au support de mémorisation. On peut aimanter, ré-aimanter, désaimanter le support de mémorisation un très très grand nombre de fois sans que cela endommage le support de mémorisation. Le support de mémorisation magnétique tient donc dans le temps, bien plus que les supports de mémorisation électronique dont le nombre d'accès avant cassure est généralement limité. Malheureusement, les mémoires magnétiques contiennent des circuits électroniques faillibles, ce qui fait qu'elles ne sont pas éternelles.

Mémoire optique, en l’occurrence en DVD.

Enfin, n’oublions pas les mémoires optiques comme les CD ou les DVD, dont le support de mémorisation est une surface réfléchissante. Elles sont composées d'une couche de plastique dans laquelle on fait des creux, creux qui sont utilisés pour coder des bits. Elles ont l'avantage d'avoir une bonne capacité, même si les temps d'accès et les débits sont minables. Elles ont une capacité et des performances plus faibles que celles des disques durs magnétiques, mais souvent meilleure que les autres formes de mémoire magnétique. Cette capacité intermédiaire est un avantage sur les mémoires magnétiques, hors disque dur. Leur inconvénient majeur est qu'elles s’abîment facilement. Toute personne ayant déjà eu des CD/DVD sait à quel point ils se rayent facilement et à quel point ces rayures peuvent tout simplement rendre le disque inutilisable.

Enfin, il faut mentionner les mémoires 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 byte bien précis, alors que la reprogrammation efface toute la mémoire et la réécrit en totalité. De plus, la reprogrammation est généralement beaucoup plus lente qu'une écriture, sans compter qu'il est plus fréquent d'écrire dans une mémoire que la reprogrammer. Ce terme de programmation vient du fait que les mémoires ROM sont souvent utilisées pour stocker des programmes sur certains ordinateurs assez simples.

Les mémoires ROM sont souvent des mémoires électroniques, même si les exceptions sont loin d'être rares. On peut classer les mémoires ROM électroniques en plusieurs types :

  • les mask ROM sont fournies déjà programmées et ne peuvent pas être reprogrammées ;
  • les mémoires PROM sont fournies intégralement vierges, et on peut les programmer une seule fois ;
  • les mémoires RPROM sont reprogrammables, ce qui signifie qu'on peut les effacer pour les programmer plusieurs fois ;
    • les mémoires EPROM s'effacent avec des rayons UV et peuvent être reprogrammées plusieurs fois de suite ;
    • certaines RPROM peuvent être effacées par des moyens électriques : ce sont les mémoires EEPROM.
Les mémoires Flash sont un cas particulier d'EEPROM, selon la définition utilisée plus haut.

Les mémoires de type mask ROM sont utilisées dans quelques applications particulières. Par exemple, elles étaient utilisées sur les vieilles consoles de jeux, pour stocker le jeu vidéo dans les cartouches. Elles servent aussi pour les firmware divers et variés, comme le firmware d'une imprimante ou d'une clé USB. Par contre, le BIOS d'un PC (qui est techniquement un firmware) est stocké dans une mémoire EEPROM, ce qui explique qu'on peut le mettre à jour (on dit qu'on flashe le BIOS).

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 byte, contrairement aux autres mémoires RWM comme les disques durs ou les disques optiques où le temps d'accès dépend de la position de la donnée. Mais nous verrons dans la suite du cours que cette définition est quelques peu trompeuse et qu'elle omet des éléments importants. Un point important est que les mémoires RAM sont des mémoires électroniques : les mémoires RWM magnétiques, optiques ou mécaniques ne sont pas considérées comme des mémoires RAM.

Précisons que la définition des mémoires RWM contient quelques subtilités assez contre-intuitives. Par exemple, prenez les CD et DVD. Ceux qui ne sont pas réinscriptibles sont naturellement des mémoires ROM, comme l'a dit plus haut. Mais qu'en est-il des CD/DVD réinscriptibles ? On pourrait croire que ce sont des mémoires RWM, car on peut modifier leur contenu sans avoir formellement à les reprogrammer. Mais en fait non, ce n'en sont pas. Là encore, on retrouve la distinction entre écriture et reprogrammation des mémoires ROM. Pouvoir effacer totalement une mémoire pour y réinscrire de nouvelles données ensuite n'en fait pas une mémoire RWM. Il faut que l'écriture puisse être localisée, qu'on puisse modifier des données sans avoir à réécrire toute la mémoire. La capacité de modifier les données des mémoires RWM doit porter sur des données individuelles, sur des morceaux de données bien précis.

Une classification des mémoires suivant la possibilité de lecture/écriture/reprogrammation

[modifier | 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. A 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 ordinateurs personnels de type PC contiennent tous une mémoire nvRAM ou BBSRAM, appelée la CMOS RAM. Elle mémorise des paramètres de configuration basique (les paramètres du BIOS). Les paramètres en question permettent de configurer le matériel lors de l'allumage, de stocker l'heure et la date et quelques autres paramètres du genre. Les BBSRAM ont aussi été utilisées dans les cartouches de jeux vidéo, pour stocker les sauvegardes. C'est le cas sur les cartouches de jeux vidéo NES ou d'anciennes consoles de cette époque, qui contenaient une puce de sauvegarde interne à la cartouche. La puce de sauvegarde n'était autre qu'une BBSRAM dans laquelle le processeur de la console allait écrire les données de sauvegarde. Les données de sauvegarde n'étaient pas effacée quand on retirait la cartouche de la console grâce à une petit pile bouton qui alimentait la BBSRAM. Si vous ouvrez une cartouche de ce type, vous verrez la pile assez facilement. Il y avait la même chose sur les cartouches de GameBoy ou de GBA, et la pile était parfois visible sur certaines cartouches transparentes (c'était notamment le cas sur la cartouche de Pokemon Cristal).

RAM drive.

Un autre exemple de mémoire non-volatile fabriquée à partir de mémoires RAM est celui des RAM drive matériels, des disques durs composés de barrettes de RAM connectées à une carte électronique et une batterie. La carte électronique fait l'interface entre le connecteur du disque dur et les barrettes de mémoire, afin de simuler un disque dur à partir des barrettes de RAM. Les barrettes sont alimentées par une batterie, afin qu'elles ne s'effacent pas. Ces RAM drive sont plus rapides que les disques durs normaux, mais ont le défaut de consommer beaucoup plus d'électricité et d'avoir une faible capacité mémoire. Ils sont peu utilisés, car très cher et peu utiles au quotidien.

Le lien avec la technologie de fabrication et les autres critères

[modifier | 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 actuelles utilisent l'adressage : chaque case mémoire se voit attribuer un numéro, l'adresse, qui va permettre de la sélectionner et de l'identifier parmi toutes les autres. On peut comparer une adresse à un numéro de téléphone (ou à une adresse d'appartement) : chacun de vos correspondants a un numéro de téléphone et vous savez que pour appeler telle personne, vous devez composer tel numéro. Les adresses mémoires en sont l'équivalent pour les cases mémoires. Ces mémoires adressables peuvent se classer en deux types : les mémoires à accès aléatoire, et les mémoires adressables par contenu.

Exemple : on demande à notre mémoire de sélectionner le byte d'adresse 1002 et on récupère son contenu (ici, 17).

Les mémoires à accès aléatoire sont des mémoires adressables, sur lesquelles on doit préciser l'adresse de la donnée à lire ou modifier. Certaines d'entre elles sont des mémoires électroniques non-volatiles de type ROM, d'autres sont des mémoires volatiles RWM, et d'autres sont des mémoires RWM non-volatiles. Comme exemple, les disques durs de type SSD sont des mémoires adressables. La mémoire principale, la fameuse mémoire RAM est aussi une mémoire adressable. D'ailleurs, le terme de mémoire RAM (Random Access Memory) désigne des mémoires qui sont à la fois adressables, de type RWM et surtout volatiles.

Les mémoires associatives fonctionnent comme une mémoire à accès aléatoire, mais dans le sens inverse. Au lieu d'envoyer l'adresse pour accéder à la donnée, on va envoyer la donnée pour récupérer son adresse : à la réception de la donnée, la mémoire va déterminer quelle case mémoire contient cette donnée et renverra l'adresse de cette case mémoire. Cela peut paraître bizarre, mais ces mémoires sont assez utiles dans certains cas de haute volée. Dès que l'on a besoin de rechercher rapidement des informations dans un ensemble de données, ou de savoir si une donnée est présente dans un ensemble, ces mémoires sont reines. Certains circuits internes au processeur ont besoin de mémoires qui fonctionnent sur ce principe. Mais laissons cela à plus tard.

Les mémoires caches

[modifier | 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 ! Et ces mémoires sont loin d'être les seules. Les CD-ROM et les DVD/Blue-Ray sont dans le même cas, dans une certaine mesure. Les mémoires de ce type sont appelées des mémoires séquentielles. Ce sont des mémoires spécialisées qui ne fonctionnent pas avec des adresses et qui ne permettent d’accéder aux données que dans un ordre bien précis, qui contraint l'accès en lecture ou en écriture.

Mémoire à accès séquentiel.

Il existe plusieurs types de mémoires séquentielles, qui se différencient par l'ordre dans lequel les données sont lues ou écrites, ou encore par leur caractère électronique, magnétique ou optique. Dans ce qui va suivre, nous allons nous restreindre aux mémoires séquentielles qui sont volatiles, la totalité étant électroniques. Si on omet les registres à décalage, les mémoires séquentielles électroniques sont toutes soit des mémoires FIFO, soit des mémoires LIFO. Ces deux types de mémoire conservent les données triées dans l'ordre d'écriture (l'ordre d'arrivée). La différence est qu'une lecture dans une mémoire FIFO renvoie la donnée la plus ancienne, alors qu'elle renverra la donnée la plus récente pour une mémoire LIFO, celle ajoutée en dernier dans la mémoire. Dans les deux cas, la lecture sera destructrice : la donnée lue est effacée.

On peut voir les mémoires FIFO comme des files d'attente, des mémoires qui permettent de mettre en attente des données tant qu'un composant n'est pas prêt. Seules deux opérations sont possibles sur de telles mémoires : mettre en attente une donnée (enqueue, en anglais) et lire la donnée la plus ancienne (dequeue, en anglais).

Fonctionnement d'une file (mémoire FIFO).

De même, on peut voir les mémoires LIFO comme des piles de données : toute écriture empilera une donnée au sommet de cette mémoire LIFO (on dit qu'on push la donnée), alors qu'une lecture enlèvera la donnée au sommet de la pile (on dit qu'on pop la donnée).

Fonctionnement d'une pile (mémoire LIFO).

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 et d'adresse

[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 un byte à chaque transmission (à chaque cycle d'horloge). Un bus qui permet cela est appelé un bus parallèle. Quelques mémoires sont cependant connectées à un bus qui ne peut transmettre qu'un seul bit à la fois. Un tel bus est appelé un bus série. Les mémoires avec un bus série ne sont pas forcément adressables bit par bit. Elles permettent de lire ou écrire par bytes complets, mais ceux-ci sont transmis bits par bits sur le bus de données. La conversion entre byte et flux de bits sur le bus est réalisée par un simple registre à décalage. On pourrait croire que de telles mémoires séries sont rares, mais ce n'est pas le cas : les mémoires Flash, très utilisées dans les clés USB ou les disques durs SSD sont des mémoires séries.

Mémoire série et parallèle

Le sens de transmission sur le bus

[modifier | modifier le wikicode]

Le bus de données est généralement un bus bidirectionnel, rarement unidirectionnel (pour les mask ROM qui ne gèrent que la lecture). Sur la plupart des mémoires, le bus de données est bidirectionnel et sert aussi bien pour les lectures que pour les écritures.

Mémoire simple-port

Sur d'autres mémoires, on trouve deux bus de données : un dédié aux lectures et un autre pour les écritures. Le bus de commande est alors assez compliqué, dans le sens où il y a deux bus d'adresses : un qui commande l'entrée d'écriture et un pour la sortie de lecture. Le bus d'adresse est donc dupliqué et d'autres bits du bus de commande le sont aussi, mais les signaux d'horloge et le bit CS ne sont pas dupliqués. En théorie, il n'y a pas besoin de bit R/W, qui est remplacé par deux bits : un qui indique qu'on veut faire une écriture sur le bus dédié, un autre pour indiquer qu'on veut faire une lecture sur l'autre bus. L’avantage d'utiliser un bus de lecture séparé du bus d'écriture est que cela permet d'effectuer une lecture en même temps qu'une écriture. Cependant, cet avantage signifie que la conception interne de la mémoire est naturellement plus compliquée. Par exemple, la mémoire doit gérer le cas où la donnée lue est identique à celle écrite en même temps. L'augmentation du nombre de broches est aussi un désavantage.

Mémoire double port (lecture et écriture séparées)

Les mémoires multiport

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

Les mémoires multiport permettent de transférer plusieurs données à la fois, une par port. Le débit est sont donc supérieur à celui des mémoires mono-port. De plus, chaque port peut être relié à des composants différents, ce qui permet de partager une mémoire entre plusieurs composants.

Dans l'exemple de la section précédente, on a un port pour les lectures et un autre pour les écritures. Chaque port est donc spécialisé soit dans les lectures, soit dans les écritures. D'autres mémoires suivent ce principe et ont deux/trois ports de lecture et un d'écriture, d'autres trois ports de lecture et deux d'écriture, bref : les combinaisons possibles sont légion. Mais d'autres mémoires ont des ports bidirectionnels, capables d'effectuer soit une lecture, soit une écriture. On peut imaginer une mémoire avec 5 ports, chacun faisant lecture et écriture.

L'interface d'une mémoire ne correspond pas forcément à celle du bus mémoire

[modifier | modifier le wikicode]

En théorie, une mémoire est utilisé avec un bus qui utilise la même interface. Par exemple, une mémoire multiport est utilisée avec un bus lui-même multiport, avec des fils séparés pour les lectures et les écritures. De même, un bus multipléxé est utilisé avec une mémoire multiplexé. Mais dans certains cas, ce n'est pas le cas.

La raison à de telles configurations tient dans un fait simple : le processeur doit économiser des broches, alors que les mémoires sont épargnée par cette économie. Il faut dire qu'un processeur a besoin de beaucoup plus de broches qu'une mémoire pour faire son travail, vu que l'interface d'une mémoire est plus simple que celle du processeur. Les processeurs doivent donc utiliser pas mal de ruses pour économiser des broches, comme un usage de bus multiplexés, de bus d'adresse multiplexé, etc. A l'inverse, les mémoires peuvent parfaitement s'en passer. Les mémoires de faible capacité sont souvent sans bus multiplexés, alors que les processeurs à bas cout avec bus multiplexés sont plus fréquents.

Les bus et mémoires multiplexés

[modifier | modifier le wikicode]

Il est possible d'utiliser un bus multiplexé avec une mémoire qui ne l'est pas. La raison est que le processeur que l'on utilise un bus multiplexé pour économiser des broches, mais que la mémoire n'a pas besoin de faire de telles économies. Cela arrive si l'on prend un processeur et une mémoire à bas prix. Les mémoires multiplexées ont tendance à être plus rares et plus chères, alors que c'est l'inverse pour les processeurs.

Un exemple est donné dans le schéma ci-dessous. On voit que le processeur et une mémoire EEPROM sont reliées à un bus multiplexé très simple. Le processeur possède un bus multiplexé, alors que la mémoire a un bus d'adresse séparé du bus de données. Dans cet exemple, le processeur ne peut faire que des lectures, vu que la mémoire est une mémoire EEPROM, mais la solution marche bien dans le cas où la mémoire est une RAM. L'interface entre bus multiplexé et mémoire qui ne l'est pas se résume à deux choses : l'ajoput d'un registre en amont de l'entrée d'adresse de la mémoire, et une commande adéquate de l'entrée OE.

Pour faire une lecture, le processeur procède en deux étapes, comme sur un bus multiplexé normale : l'envoi de l'adresse, puis la lecture de la donnée.

  • Lors de l'envoi de l'adresse, l'adresse est mémorisée dans le registre, la broche ALE étant reliée à l'entrée Enable du registre. De plus, on doit déconnecter la mémoire du bus de donnée pour éviter un conflit entre l'envoi de la donnée par la mémoire et l'envoi de l'adresse par le processeur. Pour cela, on utilise l'entrée OE (Output Enable).
  • La lecture de la donnée consiste à mettre ALE à 0, et à récupérer la donnée sur le bus. Pendant cette étape, le registre maintient l'adresse sur le bus d'adresse. Le bit OE est configuré de manière à activer la sortie de données.
8051 ALE


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.

Les bus mémoire à multiples canaux

[modifier | modifier le wikicode]

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.

Dual channel slots

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.

Le préchargement des mémoires Dual et Quad data rate

[modifier | modifier le wikicode]

Accroître plus la largeur du bus a trop de désavantages : il faudrait câbler beaucoup trop de fils. Une autre solution est d'augmenter la fréquence du bus, mais cela demande alors d'augmenter la fréquence de la mémoire, qui ne suit 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. Elle donne naissance aux mémoires mémoires Dual Data Rate, aussi appelées mémoires DDR. Il s'agit de mémoires SDRAM améliorées, avec une interface avec la mémoire légèrement bidouillée.

Les mémoires sans préchargement

[modifier | modifier le wikicode]

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.

Mémoire SDR.

Toute augmentation de la fréquence et/ou de la largeur du bus se répercute sur le plan mémoire et réciproquement. Problème, le plan mémoire est difficile à faire fonctionner à haute fréquence, mais peut avoir une largeur assez importante sans problèmes. Pour le bus, c'est l'inverse : le faire fonctionner à haute fréquence est possible, bien que cela requière un travail d'ingénierie assez conséquent, alors qu'en augmenter la largeur poserait de sérieux problèmes.

Les mémoires avec préchargement

[modifier | modifier le wikicode]

L'idée du préchargement est un compromis idéal entre les deux contraintes précédentes : on augmente la largeur du plan mémoire sans en augmenter la fréquence, mais on fait l'inverse pour le bus. En faisant cela, le plan mémoire a une fréquence inférieure à celle du bus, mais a une largeur plus importante 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 trouver un moyen pour découper un mot mémoire de 128 bits en deux blocs de 64, à envoyer sur le bus dans le bon ordre. Cela se fait dans l'interface avec le bus, grâce à une sorte de mémoire tampon un peu spéciale, dans laquelle on accumule les 128 bits lus ou à écrire.

Mémoire DDR.
Sur les mémoires DDR dans les ordinateurs personnels, seul un signal d'horloge est utilisé, que ce soit pour le bus, le plan mémoire, ou le contrôleur. Seulement, le bus et les contrôleurs mémoire réagissent à la fois sur les fronts montants et sur les fronts descendants de l'horloge. Le plan mémoire, lui, ne réagit qu'aux fronts montants.

Il existe aussi des mémoires quad data rate, pour lesquelles la fréquence du bus est quatre fois celle du plan mémoire. Évidemment, la mémoire peut alors lire ou écrire 4 fois plus de données par cycle que ce que le bus peut supporter.

Mémoire QDR.

Vous remarquerez que le préchargement se marie extrêmement bien avec le mode rafale.

Le préchargement augmente donc le débit théorique maximal. Sur les mémoires sans préchargement, le débit théorique maximal se calcule en multipliant la largeur du bus de données par sa fréquence. Par exemple, une mémoire SDRAM fonctionnant à 133 Mhz et qui utilise un bus de 8 octets, aura un débit de 8 * 133 * 1024 * 1024 octets par seconde, ce qui fait environ du 1 giga-octets par secondes. Pour les mémoires DDR, il faut multiplier la largeur du bus mémoire par la fréquence, et multiplier le tout par deux pour obtenir le débit maximal théorique. En reprenant notre exemple d'une mémoire DDR fonctionnant à 200 Mhz et utilisée en simple channel utilisera un bus de 8 octets, ce qui donnera un débit de 8 * 200 * 1024 * 1024 octets par seconde, ce qui fait environ du 2.1 gigaoctets par secondes.

Les 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 dont le byte est de 1 bit. Une telle mémoire est dite bit-adressable, c’est-à-dire que chaque bit de la mémoire a sa propre adresse. Nous étudierons le cas d'une mémoire quelconque plus loin, et ce pour une raison : on peut construire une mémoire quelconque en améliorant le plan mémoire d'une mémoire bit-adressable, d'une manière assez simple qui plus est. Parler de ces dernières est donc un bon marche-pied pour aboutir au cas général.

Le cas d'une mémoire bit-adressable

[modifier | modifier le wikicode]

Une mémoire bit-adressable est de loin celle qui a le plan mémoire le plus rudimentaire. Quand on sélectionne un bit, avec son adresse, son contenu va se retrouver sur le bus de données. Dit autrement, la cellule mémoire va se connecter sur ce fils pour y placer son contenu. On devine donc comment est organisé le plan mémoire : il est composé d'un fil directement relié au bus de donnée, sur lequel les cellules mémoire se connectent si besoin. Le plan mémoire se résume donc à un ensemble de cellules mémoires dont l'entrée/sortie est connectée à un unique fil. Ce fil s'appelle la ligne de bit (bitline en anglais). Une telle organisation se marie très bien avec les cellules de DRAM, qui disposent d'une unique broche d'entrée-sortie, par laquelle se font à la fois les lectures et écritures.

Plan mémoire simplifié d'une mémoire bit-adressable.

Il est possible d'utiliser une organisation avec deux lignes de bits, où la moitié des cellules est connectée à la première ligne, l'autre moitié à la seconde, avec une alternance entre cellules consécutives. Cela permet d'avoir moins de cellules mémoires connectées sur le même fil, ce qui améliore certains paramètres électriques des lignes de bit. Cette organisation porte le nom de ligne de bit repliée.

Lignes de bit repliées

Pour ce qui est des cellules mémoire double port, les choses sont un petit peu compliquées. Normalement, les cellules mémoire double port demandent d'utiliser deux lignes de bit : une pour le port de lecture, une autre pour le port d'écriture. Le tout est illustré ci-dessous. Mais certaines mémoires font autrement et utilisent des cellules mémoires double port avec des lignes de bit unique ou repliées. Dans ce cas, l'entrée et la sortie de la cellule mémoire sont connectées à la ligne de bit, et la lecture ou l'écriture sont contrôlés par l'entrée Enable de la cellule mémoire (qui autorise ou interdit les écritures).

Lignes de bits pour les cellules mémoires double port

En réalité, peu de mémoires suivent actuellement des lignes de bit normales. Les mémoires assez évoluées utilisent deux lignes de bit par colonne ! La première transmet le bit lu et l'autre son inverse. La mémoire utilise la différence de tension entre ces deux fils pour représenter le bit lu ou à écrire. Un tel codage est appelé un codage différentiel. L'utilité d'un tel codage assez difficile à expliquer sans faire intervenir des connaissances en électricité, mais tout est une histoire de fiabilité et de résistance aux parasites électriques.

De telles lignes de bits différentielles sont le plus souvent associées à des cellules mémoires elles aussi différentielles, notamment les cellules de SRAM abordées au chapitre précédent. Mais elles se marient très mal avec les cellules de SRAM non-différentielles, ainsi qu'avec les cellules mémoire de DRAM, qui n'ont qu'une seule broche d'entrée-sortie non-différentielle. Mais quelques astuces permettent d'utiliser des lignes de bit différentielles sur ces mémoires. La plus connue est de loin l'utilisation de cellules factices (dummy cells), des cellules mémoires vides placées aux bouts des lignes de bit. Lors d'une lecture, ces cellules vides se remplissent avec l'inverse du bit à lire. La ligne de bit inverse (celle qui contient l'inverse du bit) est alors remplie avec le contenu de la cellule factice, ce qui donne bien un signal différentiel. Le bit inversé est fournit par une porte logique qui inverse la tension fournie par la cellule mémoire. Cette tension remplis alors la cellule factice, avec l'inverse du bit lu.

Bitlines différentielles.

Certaines mémoires ont amélioré les lignes de bit différentielles en interchangeant leur place à chaque cellule mémoire. La ligne de bit change donc de côté à chaque passage d'une cellule mémoire. Cette organisation porte le nom de lignes de bit croisées.

Bitlines croisées.

Le cas d'une mémoire quelconque (avec byte > 1)

[modifier | modifier le wikicode]

Après avoir vu le cas des mémoires bit-adressables, il est temps d'étudier les mémoires quelconques, celles où un byte contient plus que 1 bit. Surprenamment, ces mémoires peuvent être conçues en utilisant plusieurs mémoires bit-adressables. Par exemple, prenons une mémoire dont le byte fait deux bits (ce qui est rare, convenons-en). On peut l'émuler à partir de deux mémoires de 1 bit : la première stocke le bit de poids faible de chaque byte, alors que l'autre stock le bit de poids fort. Et on peut élargir le raisonnement pour des bytes de 3, 4, 8, 16 bits, et autres. Par exemple, pour une mémoire dont le byte fait 64 bits, il suffit de mettre en parallèle 64 mémoires de 1 bit.

Mais cette technique n'est pas appliquée à la lettre, car il y a moyen d'optimiser le tout. En effet, on ne va pas mettre effectivement plusieurs mémoires bit-adressables en parallèle, car seuls les plans mémoires doivent être dupliqués. Si on utilisait effectivement plusieurs mémoires, chacune aurait son propre plan mémoire, mais aussi son propre contrôleur mémoire, ses propres circuits de communication avec le bus, etc. Or, ces circuits sont en fait redondants dans le cas qui nous intéresse.

Prenons le cas du contrôleur mémoire, qui reçoit l'adresse à lire/écrire et qui envoie les signaux de commande au plan mémoire. Avec N mémoires en parallèle, N contrôleurs mémoire recevront l'adresse et généreront les N mêmes signaux, qui seront envoyés à N plans mémoire distincts. Au lieu de cela, il est préférable d'utiliser un seul contrôleur mémoire, mais de dupliquer les signaux de commande en autant N exemplaires (autant qu'il y a de plan mémoire). Et c'est ainsi que sont conçues les mémoires quelconques : pour un byte de N bits, il faut prendre N plans mémoires de 1 bit. Cela demande donc d'utiliser N lignes de bits, reliée convenablement aux cellules mémoires. Le résultat est un rectangle de cellules mémoires, où chaque colonne est traversée par une ligne de bit. Chaque ligne du tableau/rectangle, correspond à un byte, c'est-à-dire une case mémoire.

Là encore, chaque colonne peut utiliser des lignes de bits différentielles ou croisées.

Plan mémoire, avec les bitlines.

La connexion des broches de commande : le transistor et le signal de sélection

[modifier | 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 correspondant au mot adressé se connectent sur la ligne de bit, alors que les autres ne doivent pas le faire. La connexion des cellules mémoire à la ligne de bit est réalisée par un interrupteur commandable, c’est-à-dire par un transistor appelé transistor de sélection. Quand la cellule mémoire est sélectionnée, le transistor se ferme, ce qui connecte la cellule mémoire à la ligne de bit. À l'inverse, quand une cellule mémoire n'est pas sélectionnée, le transistor de sélection se comporte comme un interrupteur ouvert : la cellule mémoire est déconnectée du bus.

La commande du transistor de sélection est effectuée par le contrôleur mémoire. Pour chaque ligne de bit, le contrôleur mémoire n'ouvre qu'un seul transistor à la fois (celui qui correspond à l'adresse voulue) et ferme tous les autres. La correspondance entre un transistor de sélection et l'adresse est réalisée dans le contrôleur mémoire, par des moyens que nous étudierons dans les prochains chapitres. Toujours est-il que le contrôleur mémoire génère, pour chaque octet, un bit qui dit si celui-ci est adressé ou non. Ce bit est appelé le signal de sélection. Le signal de sélection est envoyé à toutes les cellules mémoire qui correspondent au byte adressé. Vu que tous les bits d'un byte sont lus ou écrits en même temps, toutes les cellules correspondantes doivent être connectées à la ligne de bit en même temps, et donc tous les transistors de sélection associés doivent se fermer en même temps. En clair, le signal de sélection est partagé par toutes les cellules d'un même mot mémoire.

Signal de sélection et Byte.

Le cas des lignes de bit simples et repliées

[modifier | 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 ps é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 bytes large, 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 byte 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 du byte adressé à la bitline.

Principe d'un plan mémoire linéaire.

Le rôle du contrôleur mémoire est de déduire quelle entrée Row Line mettre à un à partir de l'adresse envoyée sur le bus d'adresse. Pour cela, le contrôleur mémoire doit répondre à plusieurs contraintes :

  • il reçoit des adresses codées sur n bits : ce contrôleur a donc n entrées ;
  • l'adresse de n bits peut adresser 2n bytes : notre contrôleur mémoire doit donc posséder 2^n sorties ;
  • la sortie numéro N est reliée au N-iéme signal Row Line (et donc à la N-iéme case mémoire) ;
  • on ne doit sélectionner qu'une seule case mémoire à la fois : une seule sortie devra être placée à 1 ;
  • et enfin, deux adresses différentes devront sélectionner des cases mémoires différentes : la sortie de notre contrôleur qui sera mise à 1 sera différente pour deux adresses différentes placées sur son entrée.

Le seul composant électronique qui répond à ce cahier des charges est le décodeur, ce qui fait que le contrôleur mémoire se résume à un simple décodeur, avec quelques circuits pour gérer le sens de transfert (lecture ou écriture), et la détection/correction d'erreur. Ce genre d'organisation s'appelle l'adressage linéaire.

Décodeur et plan mémoire d'une mémoire à accès aléatoire.

Les mémoires à adressage ligne-colonne

[modifier | 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 bytes à partir d'une mémoire contenant B fois moins de bytes, mais dont chacun des bytes seront B fois plus gros.

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 un « super-byte » de la mémoire interne, et le copie dans le tampon de ligne? Puis, un multiplexeur sélectionner le byte 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 byte par byte. Autre avantage : en concevant correctement la mémoire, il est possible d'améliorer les performances lors de l'accès à des données proches en mémoire. Par contre, cette organisation consomme beaucoup d'énergie. Il faut dire que pour chaque lecture d'un byte dans notre mémoire, on doit charger une ligne complète dans le tampon de ligne.

L'avantage principal est que l'accès à des données proches, localisées sur la même ligne, est fortement accéléré sur ces mémoires. Quand une ligne est chargée dans le tampon de ligne, elle reste dans ce tampon durant plusieurs accès consécutifs. Et les accès ultérieurs peuvent utiliser cette possibilité. Nous allons supposer qu'une ligne a été copiée dans le tampon de ligne, lors d'un accès précédent. Deux cas sont alors possibles.

  • Premier cas : on accède à une donnée située dans une ligne différente : c'est un défaut de tampon de ligne. Dans ce cas, il faut vider le tampon de ligne, en plus de sélectionner la ligne adéquate et la colonne. L'accès mémoire n'est alors pas différent de ce qu'on aurait sur une mémoire sans tampon de ligne, avec cependant un temps d'accès un peu plus élevé.
  • Second cas : la donnée à lire/écrire est dans la ligne chargée dans le tampon de ligne. Ce genre de situation s'appelle un succès de tampon de ligne. Dans ce cas, la ligne entière a été recopiée dans le tampon de ligne et on n'a pas à la sélectionner : on doit juste changer de colonne. Le temps nécessaire pour accéder à notre donnée est donc égal au temps nécessaire pour sélectionner une colonne, auquel il faut parfois ajouter le temps nécessaire entre deux sélections de deux colonnes différentes.

Les accès consécutifs à une même ligne sont assez fréquents. Ils surviennent lorsque l'on doit accéder à des données proches les unes des autres en mémoire, ce qui est très fréquent. La majorité des programmes informatiques accèdent à des données proches en mémoire : c'est le principe de localité spatiale vu dans les chapitres précédents. Les mémoires à tampon de ligne profitent au maximum de cet effet de localité spatiale, ce qui leur donne un boost de performance assez important. Dans les faits, la mémoire RAM de votre ordinateur personnel est une mémoire à tampon de ligne. Toutes les mémoires RAM des ordinateurs grand public sont des mémoires à tampon de ligne, et ce depuis plusieurs décennies.

L'adressage par coïncidence

[modifier | 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 bytes sans y accéder, mais en accédant à leurs voisins est une faille exploitable par les pirates informatiques. L'intérêt est de contourner les protections mémoires liées au système d'exploitation. Sur les systèmes d'exploitation modernes, chaque programme se voit attribuer certaines portions de la mémoire, auxquelles il est le seul à pouvoir accéder. Des mécanismes de protection mémoire intégré dans le processeur permettent d'isoler la mémoire de chaque programme, comme nous le verrons dans le chapitre sur la mémoire virtuelle. Mais avec row hammer, les accès à un byte attribué à un programme peuvent déborder sur les bytes d'un autre programme, avec des conséquences assez variables. Par exemple, un virus présent en mémoire pourrait interagir avec le byte qui mémorise un mot de passe ou une clé de sécurité RSA, ou toute donnée confidentielle. Il pourrait récupérer cette information, ou alors la modifier pour la remplacer par une valeur connue et l'attaquant.

La faille row hammer est d'autant plus simple que la physique des cellules mémoire est médiocre. Les progrès de la miniaturisation rendent cette attaque de plus en plus facilement exploitable, les fuites étant d'autant plus importantes que les cellules mémoires sont petites. Mais exploiter cette attaque est compliqué, car il faut savoir à quelle adresse se situe a donnée à altérer, sans compter qu'il faut avoir des informations sur l'adresse des cellules voisines. Rappelons que la répartition physique des adresses/bytes dépend de comment la mémoire est organisée en interne, avec des banques, rangées et autres. Deux adresses consécutives ne sont pas forcément voisines sur la barrette de mémoire et l relation entre deux adresses de cellules mémoires voisines n'est pas connue avec certitude tant elle varie d'un système mémoire à l'autre.

Les solutions pour mitiger l'attaque row hammer sont assez limitées. Une première solution est d'utiliser les techniques de correction et de détection d'erreur comme l'ECC, mais là l'effet est limité. Une autre solution est de rafraichir la mémoire plus fréquemment, mais cela a un effet assez limité, sans compter que cela a un impact sur les performance et la consommation d'énergie de la RAM. Les concepteurs de matériel ont dû inventer des techniques spécialisées, comme le pseudo target row refresh d'Intel ou le target row refresh des mémoires LPDDR4. Ces techniques consistent, pour simplifier, à détecter quand une ligne mémoire est accédée très souvent, à rafraichir les lignes de mémoire voisines assez régulièrement. L'effet sur les performances est limité, mais cela demande d'intégrer cette technique dans le contrôleur mémoire externe/interne.


Les mémoires vues au chapitre précédent sont les mémoires les plus simples qui soient. Mais ces mémoires peuvent se voir ajouter quelques améliorations pas franchement négligeables, afin d'augmenter leur rapidité, ou de diminuer leur consommation énergétique. Dans ce chapitre, nous allons voir quelles sont ces améliorations les plus courantes.

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 bytes 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 au byte 3, par exemple. Dans ce cas, on commence par lire le byte numéroté 3, puis le 4, le 5, le 6 et le 7. Puis, l'accès reprend au bloc 0, avant d'accèder aux blocs 1, 2 et 3. En clair, la mémoire est découpée en bloc de 8 bytes consécutifs et l'accès lit un bloc complet. Si la première adresse lue commence à la première adresse du bloc, l'accès est identique à l'accès séquentiel. Mais si l'adresse de départ de la rafale est dans le bloc, la lecture commence à 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 bytes consécutifs 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 bytes consécutifs en rusant quelque peu. L'idée est que des accès consécutifs se fassent dans des banques différentes, et donc que des bytes consécutifs soient localisés dans des banques différentes. Les mémoires qui fonctionnent sur ce principe sont appelées des mémoires à entrelacement simple.

Répartition des adresses dans une mémoire interleaved.

Pour cela, il suffit de prendre une mémoire à arrangement vertical, avec un petit changement : il faut utiliser les bits de poids faible pour sélectionner la banque, et les bits de poids fort pour le Byte.

Adresse mémoire d'une mémoire entrelacée

En faisant cela, on peut accéder à un plusieurs bytes consécutifs assez rapidement. Cela rend les accès en rafale plus rapide. Pour cela, deux méthodes sont possibles.

  • La première méthode utilise un accès en parallèle aux banques, d'où son nom d'accès entrelacé parallèle. Sans entrelacement, on doit accéder à chaque banque l'une après l'autre, en lisant chaque byte l'un après l'autre. Avec l’entrelacement parallèle, on lit plusieurs bytes consécutifs en même temps, en accédant à toutes les banques en même temps, avant d'envoyer chaque byte l'un après l'autre sur le bus (ce qui demande juste de configurer le multiplexeur). Un tel accès est dit en rafale : on envoie une adresse, puis on récupère plusieurs bytes consécutifs à partir de cette adresse initiale.
  • Une autre méthode démarre un nouvel accès mémoire à chaque cycle d'horloge, pour lire des bytes consécutifs un par un, mais chaque accès se fera dans une banque différente. En faisant cela, on n’a pas à attendre que la première banque ait fini sa lecture/écriture avant de démarrer la lecture/écriture suivante. Il s'agit d'une forme de pipelining, qui fait que l'accès à des bytes consécutifs est rendu plus rapide.

Les mémoires à entrelacement par décalage

[modifier | 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 byte, on rajoute quelques bits calculés à partir des données du byte, 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 au byte mémorisé, de manière à ce que le nombre de bits à 1 soit pair. En clair, si on compte les bits à 1 dans le byte, 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 byte dans la mémoire, le contrôleur mémoire calculait le bit de parité du byte lu. Le résultat était alors comparé au bit de parité stocké dans le byte. 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 du byte est corrompue, 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 byte, 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 byte eux-même, 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 bytes, 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 byte. 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 du byte 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 byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à un byte, 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 byte régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque byte 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", le byte 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 bytes 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 le byte d'adresse contient la donnée . Et réciproquement : une telle ROM est équivalente au circuit précédent. En clair, on peut créer un circuit combinatoire quelconque en utilisant une ROM, ce qui est très utilisé dans certains circuits que nous verrons dans quelques chapitres. Cependant, cela ne signifie pas que chaque circuit combinatoire soit une mémoire ROM : une vraie ROM contient un plan mémoire, un décodeur, de même que les circuits d'interface avec le bus, des bitlines, et j'en passe.

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, byte par byte. Lorsqu'une ligne est sélectionnée, on place une tension très importante sur les bitlines voulues. Les fusibles de la ligne connectés à ces bitlines sont alors grillés, ce qui les met à 0. Les autres bitlines sont soumises à une tension normale, ce qui est insuffisant pour griller les fusibles. En choisissant bien les bitlines en surtension pour chaque ligne, on arrive à programmer la mémoire FROM comme souhaité.

Les mémoires EPROM et EEPROM

[modifier | 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 byte par byte, alors que les Flash les font par paquets de plusieurs bytes. Là où on peut effacer/programmer un byte individuel sur une EEPROM, ce n'est pas possible sur une mémoire Flash. Sur une mémoire Flash, on est obligé d'effacer/programmer un bloc entier de la mémoire, le bloc faisant plus de 512 bytes. C’est une simplification, qui cache le fait que la distinction entre EEPROM et Flash n'est pas très claire. Dans les faits, on considère que le terme EEPROM est à réserver aux mémoires dont les unités d'effacement/programmation sont petites (elles ne font que quelques bytes, pas plus), alors que les Flash ont des unités beaucoup plus larges.

Les différences entre EEPROM, Flash NOR et Flash NAND

[modifier | modifier le wikicode]

Dans ce cours, nous ferons la distinction entre EEPROM et Flash sur le critère suivant : l'effacement peut se faire byte par byte sur une EEPROM, alors qu'il se fait par blocs entiers sur une Flash. Quant à la reprogrammation, tout dépend du type de mémoire. Sur les EEPROM, elle a forcément lieu byte par byte, comme l'effacement. Mais sur les mémoires Flash, elle peut se faire soit byte par byte, soit par paquets de plusieurs centaines de bytes. Cela permet de distinguer deux sous-types de mémoires Flash : les mémoires Flash de type NOR et les Flash de type NAND. Nous verrons ci-dessous d'où proviennent ces termes, mais laissons cela de côté pour le moment. Sur les Flash de type NOR, on doit effacer la mémoire par blocs, mais on peut reprogrammer les bytes uns par uns, indépendamment les uns des autres. Par contre, sur les Flash de type NAND, effacement et reprogrammation se font par paquets de plusieurs centaines de bytes. Pire : les blocs pour l'effacement n'ont pas la même taille que pour la reprogrammation : environ 512 à 8192 octets pour la reprogrammation, plus de 64 kibioctets pour l'effacement. Par exemple, il est possible de lire un octet individuel, d'écrire par paquets de 512 octets et d'effacer des paquets de 4096 octets. Sur les Flash NAND, l'unité d'effacement s'appelle un bloc (comme pour les Flash NOR), alors que l'unité de reprogrammation s'appelle une page mémoire.

Différences entre EEPROM, Flash NAND et Flash NOR
Reprogrammation Byte par byte Reprogrammation par blocs entiers
Effacement Byte par byte EEPROM N'existe pas
Effacement par blocs entiers Flash de type NOR Flash de type NAND

Les avantages et inconvénients de chaque type d'EEPROM

[modifier | 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. L'adresse et les signaux de commande sont mémorisés dans des registres, mais pas les données. Elle partent d'un modèle asynchrone, sur lequel on ajoute des registres pour les entrées d'adresse et de commande, mais pas pour les ports de lecture/écriture, pas pour le bus de 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 byte, chaque case mémoire, est stockée dans un registre à décalage, et il en est de même pour le mot d'entrée à comparer. Ainsi, à chaque cycle, les deux bits à comparer sortent de ces deux registres à décalage. La comparaison bit à bit est effectuée par un comparateur d'égalité sériel. Le bit qui sort de la case mémoire est envoyé au comparateur sériel, mais il est aussi réinjecté dans le registre à décalage, afin que la case mémoire ne perde pas son contenu. L'idée est que le contenu de la case mémoire fasse une boucle avant de revenir à son état initial : on applique une opération de rotation dessus.

Case mémoire d'un processeur associatif bit serial avec une bascule.

Pour donner un exemple, je vais prendre l'exemple d'un Byte qui contient la valeur 1100, et une donnée d'entrée qui vaut 0100. À chaque cycle d’horloge, le processeur associatif compare un bit de l'entrée avec le bit de même poids dans la case mémoire. La comparaison est le fait d'un circuit comparateur sériel (vu dans le chapitre sur les circuits comparateurs). Le résultat de la comparaison est disponible dans la bascule de 1 bit une fois la comparaison terminée. La bascule est reliée à la sortie sur laquelle on envoie le signal Trouvé / Non-trouvé.

Illustration d'une opération sur un processeur associatif sériel.

Les mémoires bit-sérielles basées sur un système de sélection des colonnes

[modifier | 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 du byte adéquat.

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.


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.

Fonctionnement des mémoires FIFO-LIFO.

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. Les seules utilisations connues sont liées à la pile d'appel du processeur, que nous verrons d'ici une dizaine de chapitres. Il existe quelques processeurs qui placent cette pile d'appel dans une mémoire LIFO séparée, mais ce n'est pas le cas sur la quasi-totalité des processeurs récents. Il a existé quelques processeurs qui intégraient le sommet de la pile d'appel dans une mémoire LIFO intégrée au processeur, mais ce n'est plus d'actualité. Il en existe dans les unités de prédiction de branchement des processeurs modernes, 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. Elles sont surtout utilisées pour mettre en attente des données ou commandes, tout en conservant leur ordre d'arrivée. Si on utilise une mémoire FIFO dans cet optique, elle prend le nom de mémoire tampon. L'utilisation principale des mémoires tampons est l’interfaçage de deux composants de vitesse différentes qui doivent communiquer entre eux. Le composant rapide émet des commandes à destination du composant lent, mais le fait à un rythme discontinu, par rafales entrecoupées de "moments de silence". Lors d’une rafale, le composant lent met en attente les commandes dans la mémoire FIFO et il les consomme progressivement lors des moments de silence.

On retrouve des mémoires tampons dans beaucoup de matériel électronique : dans les disques durs, des les lecteurs de CD/DVD, dans les processeurs, dans les cartes réseau, etc. Prenons par exemple le cas d'un disque dur : il reçoit régulièrement, de la part du processeur, des commandes de lecture écriture et les données associées. Mais le disque dur étant un périphérique assez lent, il doit mettre en attente les commandes/données réceptionnées avant de pouvoir les traiter. Et cette mise en attente doit conserver l'ordre d'arrivée des commandes, sans quoi on ne lirait pas les données demandés dans le bon ordre. Pour cela, les commandes sont stockées dans une mémoire FIFO et sont consommées au fur et à mesure. On trouve le même genre de logique dans les cartes réseau, qui reçoivent des paquets de données à un rythme discontinu, qu'elles doivent parfois mettre en attente tant que la carte réseau n'a pas terminé de gérer le paquet précédent.

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 comme un bit R/W.

La mémoire FIFO/LIFO doit indiquer quand celle-ci est pleine, à savoir qu'elle ne peut plus accepter de nouvelle donnée en écriture. Elle doit aussi indiquer quand elle est vide, ce qui signifie qu'il n'y a pas possibilité de lire son contenu. Pour cela, la mémoire possède deux sorties nommées FULL et EMPTY. Ces deux bits indiquent respectivement si la mémoire est pleine ou vide, quand ils sont à 1.

Les mémoires FIFO/LIFO les plus simples n'ont qu'un seul port qui sert à la fois pour la lecture et l'écriture, mais elles sont cependant rares. Dans les faits, les mémoires FIFO sont presque toujours des mémoires double ports, avec un port pour la lecture et un autre pour les écritures. Du fait de la présence de deux ports dédiés, le bit R/W est souvent séparé en deux bits distincts : un bit Write Enable sur le port d'écriture, qui indique qu'une écriture est demandée, et un bit Read Enable sur le port de lecture qui indique qu'une lecture est demandée. La séparation du bit R/W en deux bits séparés pour chaque port s'expliquera dans les paragraphes suivants.

Les mémoires FIFO/LIFO sont souvent des mémoires synchrones, les implémentations asynchrones étant plus rares. Elles ont donc au moins une entrée pour le signal d'horloge. Point important, de nombreuses mémoires FIFO ont une fréquence pour la lecture qui est distincte de la fréquence pour l'écriture. En conséquence, elles reçoivent deux signaux d'horloge sur deux entrées séparées : un pour la lecture et un pour l'écriture. La présence de deux fréquences s'explique par le fait que les mémoires FIFO servent à interfacer deux composants qui ont des vitesses différentes, comme un disque dur et un processeur, un périphérique et un chipset de carte mère, etc. D'ailleurs, c'est ce qui explique que le bit R/W soit scindé en deux bits, un par port : les deux ports n'allant pas à la même vitesse, ils ne peuvent pas partager un même bit d'entrée sans que cela ne pose problème.

L'interface la plus classique pour une mémoire FIFO/LIFO est illustrée ci-dessous. On voit la présence d'un port d'écriture séparé du port de lecture, et la présence de deux signaux d'horloge distincts pour chaque port.

Interface des mémoires FIFO et LIFO

Les mémoires LIFO

[modifier | 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. Pour cela, il suffit d'enchainer des registres les uns à la suite des autres. Les données peuvent passer d'un registre à son suivant, ou d'un registre aux précédents. Toutes les lectures ou écritures ont lieu dans le même registre, celui qui contient le sommet de la pile. Quand on écrit une donnée, celle-ci est placée dans ce registre de sommet de pile. Pour éviter de perdre des données, celles-ci sont déplacées de leur registre actuel au précédent. Toutes les données sont donc décalées d'un registres, avant l'écriture de la donnée au sommet de pile. Lors d'une lecture, le sommet de la pile est effacé. Pour cela, toutes les données sont avancées d'un registre, en passant du registre actuel au suivant. Les échanges de données entre registres sont gérés par divers circuits d’interfaçage, commandés par un gigantesque circuit combinatoire (le contrôleur mémoire).

Mémoire LIFO fabriquée à partir de registres

Les mémoires FIFO

[modifier | modifier le wikicode]

Les FIFO de taille fixe

[modifier | modifier le wikicode]

Les mémoires FIFO les plus simples ont une taille fixe, à savoir qu'elles contiennent toujours le même nombre de données mises en attente. Elles peuvent se fabriquer en enchainant des registres.

Register based parallel SAM

Il est aussi possible de fabriquer une FIFO en utilisant plusieurs registres à décalages. Chaque registre à décalage contient un bit pour chaque byte mis en attente. Le circuit en question est décrit dans le schéma ci-dessous.

FIFO de m bytes de n bits fabriquées avec des registres à décalages

Les FIFO de taille variable

[modifier | modifier le wikicode]

La plupart des applications requièrent des FIFOs qui mémorisent un nombre variable de données. De telles FIFO de taille variable peuvent se fabriquer à partir d'une mémoire RAM en y ajoutant deux compteurs/registres et quelques circuits annexes. Les deux compteurs mémorisent l'adresse de la donnée la plus ancienne, ainsi que l'adresse de la plus récente, à savoir l'adresse à laquelle écrire la prochaine donnée à enqueue, et l'adresse de lecture pour la prochaine donnée à dequeue. Quand une donnée est retirée, l'adresse la plus récente est décrémentée, pour pointer sur la prochaine donnée. Quand une donnée est ajoutée, l'adresse la plus ancienne est incrémentée pour pointer sur la bonne donnée.

Mémoire FIFO construite avec une RAM.

Petit détail : quand on ajoute des instructions dans la mémoire, il se peut que l'on arrive au bout, à l'adresse maximale, même s'il reste de la place à cause des retraits de données. La prochaine entrée à être remplie sera celle numérotée 0, et on poursuivra ainsi de suite. Une telle situation est illustrée ci-dessous.

Débordement de FIFO.

La gestion des bits EMPTY et FULL se fait par comparaison des deux compteurs. S'ils sont égaux, c'est que la pile est soit vide, soit pleine. On peut faire la différence selon la dernière opération : la pile est vide si c'est une lecture et pleine si c'est une écriture. Une simple bascule suffit pour mémoriser le type de la dernière opération. Un simple circuit combinatoire contenant un comparateur permet alors de gérer les flags simplement.

FIFO pleine ou vide.

L'implémentation des deux compteurs

[modifier | modifier le wikicode]

L'implémentation d'une mémoire FIFO demande donc d'utiliser deux compteurs. Vous vous attendez à ce que ces deux compteurs soient des compteurs binaires normaux, pas quelque chose d'exotique. Mais dans les faits, il est parfaitement possible d'utiliser des compteurs en anneau, ou des registres à décalage à rétroaction linéaire, ou des compteurs modulo comme compteurs dans une FIFO.

Premièrement, rien n'impose que les compteurs comptent de 1 en 1. Dans les faits, la séquence comptée peut être arbitraire : tout ce qui compte est qu'elle est la même entre les deux compteurs, pour vérifier si la RAM est pleine ou vide. Et cela amène à une optimisation assez simple : on peut utiliser des registres à décalage à rétroaction linéaire (LFSR ), au lieu de compteurs binaires usuels. Le résultat fonctionnera de la même façon vu de l'extérieur : on aura une FIFO qui fonctionne normalement, sans problèmes particuliers. Certes, la mémoire RAM ne sera pas remplie en partant de l'adresse 0, puis en remplissant la mémoire en sautant d'une adresse à sa voisine. A la place, les données seront ajoutées dans un ordre différent, mais qui ne change globalement pas grand chose. Les raisons de faire ainsi sont que les compteurs à base de LFSR est qu'ils sont plus simples à concevoir, moins gourmands en circuits et plus rapides. Le seul défaut est que les LFSR ne peuvent en effet pas contenir la valeur 0, ce qui fait qu'une adresse est gâchée. Mais sur les FIFOs assez grosses, le gain en vitesse et l'économie de circuits liée au compteur vaut la peine de gâcher une adresse.

Deuxièmement, rien n'impose que le compteur contienne une valeur codée en binaire. L'encodage de la valeur en binaire est cependant nécessaire pour adresser la mémoire RAM, mais il suffit d'ajouter un circuit pour traduire le contenu du compteur en binaire usuel pour cela. Sur les FIFO avec un port de lecture et un port d'écriture cadencés à des fréquences différentes, on utilise deux compteurs qui encodent des entiers en code Gray. La raison à cela est que les deux compteurs sont respectivement dans le port de lecture et dans le port d'écriture, et sont donc cadencés à deux fréquences différentes. Et la différence de fréquence entre compteurs peut causer des soucis pour calculer les bits EMPTY et FULL. Pour le dire autrement, la FIFO contient deux domaines d'horloge qui doivent communiquer entre eux pour calculer les bits EMPTY et FULL. C'est un cas de clock domain crossing tel que vu dans le chapitre sur le signal d'horloge, et la solution est alors celle évoquée dans ce chapitre : utiliser le code Gray.

Lors de l'incrémentation d'un compteur, tous les bits ne sont pas modifiés en même temps : les bits de poids faible sont typiquement modifiés avant les autres. Évidemment, à la fin de l'incrémentation, on obtient le résultat final, correct. Mais pendant le temps de calcul, le compteur peut se retrouver dans un état transitoire, où certains bits ont été modifiés mais pas les autres. Or, le comparateur qui s'occupe de déterminer les bits EMPTY et FULL est cadencé de manière à être au moins aussi rapide que le plus rapide des deux compteurs. Le comparateur est donc plus rapide que le compteur le plus lent et peut "voir" cet état transitoire. Il se peut que le comparateur compare le contenu du compteur le plus rapide avec l'état transitoire de l'autre, ce qui donnera un résultat temporairement faux. L'usage de compteurs en code Gray permet d'éviter ce problème : vu que seul un bit est modifié lors d'une incrémentation/décrémentation, les états transitoires n'existent tout simplement pas.


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. A l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort.

Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le bit de retenue. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (ADD with Carry). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares.

Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue.

La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (Substract with Carry). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant :

Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, et les 8 bits de poids forts de ces opérandes, et et leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1.

  • Si la retenue de l'addition est de 1, on a , ce qui veut dire que la seconde soustraction doit calculer .
  • A l'inverse, si la retenue de l'addition est de 0, on a . Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer .

Récapitulons dans un tableau.

Retenue Opération à effectuer pour la seconde soustraction
0
1

On voit que ce qu'il faut ajouter à est égal à la retenue. L'opération SUBC fait donc le calcul suivant :

La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple.

Les instructions de 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. A 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. A 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 p^ê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. A 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. A 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 . A 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.

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

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.

Pour résumer, les instructions les plus courantes sont les suivantes :

Instruction Utilité
Instructions arithmétiques Ces instructions font simplement des calculs sur des nombres. On peut citer par exemple :
  • L'addition ;
  • la multiplication ;
  • la division ;
  • le modulo ;
  • la soustraction ;
  • la racine carrée ;
  • le cosinus ;
  • et parfois d'autres.
Instructions logiques Elles travaillent sur des bits ou des groupes de bits. On peut citer :
  • Le ET logique.
  • Le OU logique.
  • Le OU exclusif (XOR).
  • Le NON , qui inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1.
  • Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus.
  • Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre.
Instructions de test et de contrôle (branchements) Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses.
Instructions d’accès mémoire Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. 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.

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 mémoire

[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 pseudo-aliasing des registres sur les CPU Intel 8 bits et le Z80

[modifier | modifier le wikicode]

Pour commencer, voyons le cas des premiers processeurs Intel, à savoir les processeurs 4004, 4040, 8008 et 8080. Ils avaient un système de pseudo-aliasing de registres. Formellement, ce n'est pas un système d'alias, mais un système où les registres sont regroupés lors de certaines opérations.

Les premiers CPU Intel étaient des processeurs 8 bits. Ils incorporaient 7 registres de 8 bits nommés A, B, C, D, E, H, L. Le Z80 regroupe les 7 registres de 8 bits en 3 paires de registres. Les 3 paires en question sont la paire BC, la paire DE et la paire HL, le registre A est laissé de côté. Une paire de registres de 8 bits est considérée comme un registre unique de 16 bits pour certaines opérations. Par exemple, le registre BC de 16 bits est composé des deux registres B et C de 8 bits, idem pour les paires DE et HL.

La quasi-totalité des opérations arithmétiques ne manipule que ces registres de 8 bits, sauf l'opération d'incrémentation qui est un peu à part. Il est possible d'effectuer une opération d'incrémentation sur une paire de 16 bit complète, avec une instruction spécialisée.

Cela peut paraître étrange, mais c'est en réalité un petit plus qui se marie bien avec le reste de l'architecture. Le Z80 gère des adresses de 16 bits, son pointeur de pile et son program counter sont de 16 bits tous les deux. Aussi, pour mettre à jour le pointeur de pile et le program counter, le processeur incorpore un incrémenteur de 16 bits. Les concepteurs du processeur ont rentabilisé cet incrémenteur, en lui permettant d'incrémenter des données de 16 bits. Et pour avoir une donnée de 16 bits, il fallait regrouper les registres de 8 bits par paire.

Le système d'aliasing de registres sur les processeurs x86

[modifier | modifier le wikicode]

Le système décrit dans la section précédent décrit le comportement des registres sur les processeurs 8 bits d'Intel. Mais ce système a été abandonné sur ses CPU 16 bits, les fameux 8086 et 8088. Si c'étaient des processeurs 16 bits, ils étaient 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.

Les modes d'adressage pseudo-absolus pour adresser plus de mémoire

[modifier | modifier le wikicode]

Une variante de l'adressage base + décalage a été utilisée sur d'anciennes architectures Motorola pour permettre d'adresser plus de mémoire. La variante en question visait à améliorer l'adressage absolu, qui intègre l'adresse à lire/écrire dans l'instruction elle-même. L'adresse en question est généralement très courte et n'encode que les bits de poids faible de l'adresse, ce qui ne permet que d'adresser une partie de la mémoire de manière absolue, là où les autres instructions ne sont pas limitées. Pour contourner ce problème, plusieurs modes d'adressages ont été inventées. Tous incorporent une adresse dans l'instruction, mais combinent cette adresse avec un registre adressé implicitement, ce qui en fait des adressages mi-indirects, mi-absolus. Nous regrouperons ces modes d'adressages sous le terme de modes d'adressages pseudo-absolus, terme de mon invention.

La première idée met les bits de poids fort de l'adresse dans un registre spécialement dédié pour. L'adresse finale est obtenue en concaténant ce registre avec l'adresse mémoire intégrée dans l’instruction. Le registre est appelé le registre de page. Un exemple est celui des premiers processeurs Motorola. Le registre de page 8 bits, l'adresse finale de 16 bits était obtenue en concaténant le registre de page avec les 8 bits fournit par adressage absolu. Le résultat est que la mémoire était découpée en blocs de 256 consécutifs, chacun pouvant servir de fenêtre de 256 octets.

Un autre exemple est celui du HP 2100, qui avait un registre de page de 5 bits et qui encodait 10 bits d'adresse dans ses instructions. Ses instructions d'accès mémoire disposaient d'un bit qui choisit quel mode d'adressage utiliser. S'il était à 0, l'adressage absolu était utilisé, le registre de page n'était pas utilisé. Mais s'il était à 1, le registre de page était utilisé pour calculer l'adresse.

Formellement, ce mode d'adressage a des ressemblances avec la commutation de banques, une technique qu'on verra dans les chapitres sur l'espace d'adressage et la mémoire virtuelle. Mais ce n'en est pas du tout : le registre de page est utilisé uniquement pour les accès mémoire avec le mode d'adressage base + décalage, mais pas pour les autres accès mémoire, qui gèrent des adresses complètes. Notons que l'usage d'un registre de page dédié fait que celui-ci est adressé implicitement.

Une évolution de l'adressage précédent est le mode page direct. Avec lui, le registre de page est étendu et contient une adresse mémoire complète. L'adresse finale n'est pas obtenue par concaténation, mais en additionnant le registre de page avec l'adresse fournie par adressage absolu. Un exemple est celui des premiers processeurs Motorola, qui géraient des adresses courtes de 8 bits. L'adresse courte de 8 bits correspondait non pas aux 256 premiers octets de la mémoire, mais à une fenêtre de 256 octets déplaçable en mémoire. La position de la fenêtre de 256 octets était spécifiée par le registre de page de 16 bits, qui précisait l'adresse du début de la fenêtre, celle de sa première donnée.

Il s'agit donc formellement d'adressage base + décalage, à un détail près : il n'y a qu'un seul registre de base. Le fait que ce registre de base soit unique fait qu'il est adressé implicitement, on n'a pas à encoder le numéro/noms de registre dans l'instruction. Le registre de base est utilisé uniquement pour l'adressage absolu, pas pour les autres accès mémoire. S'il y a des ressemblances avec la segmentation, une technique de mémoire virtuelle qu'on abordera dans quelques chapitres, ce n'en est pas vu que le registre de base est utilisé seulement pour un mode d'adressage bien précis.

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. A 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. 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. L'idée est de commencer à remplir les cases mémoires à partir de l'adresse 0, à partir du début de la mémoire. La première adresse est placée dans l'adresse 0, la 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, la mémoire RAM est couplée à un registre qui mémorise où se situe le sommet de la pile dans la mémoire RAM. Le registre 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

Si quelques architectures à pile ont utilisé une pile 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 alternative est d'intégrer la mémoire LIFO au processeur. Avec l'augmentation du nombre de transistors, intégrer une pile d'adresse de retour au processeur n'est plus un problème. De nombreux microcontrôleurs et microprocesseurs embarqués utilisent cette technique. Mais elle a un défaut : la pile peut contenir un nombre maximal d'adresses, 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 !

Une autre 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 piel, 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 !

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, il y a deux solutions.

  • La première est d'ajouter une instruction spécifique pour additionner/soustraire une constante au pointeur de pile.
  • La seconde est de rendre le registre pointeur de pile adressable, de lui donner un nom/numéro de registre.

La seconde solution a un désavantage : on perd un registre adressable. Par exemple, sur un processeur avec des numéros de registres sur 4 bits, on peut adresser 16 registres. Un pointeur de pile adressable fait qu'on a 15 registres généraux et un pointeur de pile adressable. Avec un pointeur de pile sans numéro de registre, on a 16 registres généraux et un pointeur de pile séparé.

Les processeurs CISC utilisaient autrefois un registre dédié au pointeur de pile, qui était adressé implicitement ou explicitement. Sur les architectures RISC, le pointeur de pile a disparu et un registre général est utilisé à la place. Il faut dire que le pointeur de pile contient une adresse encodée avec un nombre entier, qui est incrémentée/décrémentée avec des opérations entières classiques. Autant utiliser un registre entier pour le pointeur de pile et effectuer des instructions arithmétiques normales.

La function local area et la function argument area sont donc 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. L'instruction LOAD/STORE ajoute alors automatiquement cette position au pointeur de pile, pour générer l'adresse à lire/écrire.

Du moins, tout cela est valable si c'est un registre dédié. 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 : systèmes d'exploitation et virtualisation

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

Pour simplifier, le processeur peut fonctionner en plusieurs modes, qui sont appelés : mode utilisateur, noyau, hyperviseur et système. Les quatre modes précédents ont une utilisation spécifique. Par utilisation spécifique, on veut dire qu'il y a un mode réservé au système d'exploitation, un autre pour le firmware/BIOS, un autre pour les logiciels usuels, et un autre pour les hyperviseurs. Les hyperviseurs sont des logiciels de virtualisation, dont le rôle est de tourner plusieurs OS en même temps. Ils sont en quelque sorte situés sous le système d'exploitation et on peut les voir comme une sorte de sous-système d'exploitation. Le mode utilisateur est réservé aux logiciels basiques, le mode noyau est réservé au noyau du système d'exploitation (d'où son nom), le mode hyperviseur est quant à lui utilisé par l'hyperviseur si l'ordinateur utilise la virtualisation, 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 les autres modes. Des instructions précises sont interdites en mode utilisateur, d'autres sont interdites en mode noyau et utilisateur, etc. Seul le mode système permet absolument tout.

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

[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 modes hyperviseur pour la virtualisation

[modifier | modifier le wikicode]

Sur les CPU modernes, d'autres niveaux de privilèges existent, avec encore plus de privilèges que l'espace noyau. Par exemple, il existe un niveau de privilège appelé le mode hyperviseur qui est utilisé pour les techniques de virtualisation. Ces dernières permettent à plusieurs systèmes d'exploitation de tourner en même temps sur la même machine. Ils sont isolés les uns des autres, dans le sens où ils ont là aussi chacun leur propre mémoire dédiée, leur propre portion de RAM, que les autres OS ne peuvent pas voir.

Pour cela, un logiciel appelé l'hyperviseur gère la virtualisation et il a besoin d'un mode sous le mode noyau, dédié à la virtualisation, qui n'est autre que le mode hyperviseur. Le mode hyperviseur est implémenté sur les processeurs x86 avec deux standards incompatibles entre eux : l'un sur les CPU Intel, l'autre sur les CPU AMD. Ils portent les noms d'Intel VT-x et d'AMD-V. Grâce à eux, le noyau de l'OS est bel et bien en mode noyau.

Différence entre système d'exploitation et hyperviseur.

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.

Prenons l'exemple du processeur Z80 pour simplifier les explications. Comme dit plus haut, ce processeur a beaucoup de registres et tous ne sont pas dupliqués. Les registres pour la pile ne sont pas dupliqués, le program counter non plus. Par contre, les autres registres, qui contiennent des données, sont dupliqués. Il s'agit des registres nommés A (accumulateur), le registre d'état F et les registres généraux B, C, D, E, H, L, ainsi que les registres temporaires W et Z. L'ensemble de registres pour interruption dispose lui aussi de registres nommés A, F et les registres généraux B, C, D, E, H, L, mais il s'agit de registres différents. Pour éviter les confusions, ils sont notés A', F', B', C', D', E', H', L', mais les noms de registres sont en réalité identiques. Le processeur ne confond pas les deux, car il sait s'il est dans une interruption ou non.

Les deux ensembles de registres sont censés être isolés, il n'est pas censé y avoir d'échanges de données entre les deux. Mais le Z80 permettait d'échanger des données entre les deux ensembles de registres. Dans le détail, une instruction permettait d'échanger le contenu d'une paire de registres avec la même paire dans l'autre ensemble. En clair, on peut échanger les registres BC avec BC', DE avec DE′, et HL avec HL′. Par contre, il est impossible d'échanger le registre A avec A', et F avec F'. Le résultat est que les programmeurs utilisaient l'autre ensemble de registres comme registres pour les programmes, même s'ils n'étaient pas prévus pour.

Ce système permet de simplifier grandement la gestion des interruptions matérielles. Lors d'une interruption sur un processeur sans ce système, l'interruption doit sauvegarder les registres qu'elle manipule, qui sont potentiellement utilisés par le programme qu'elle interrompt. Avec ce système, il n'y a pas besoin de sauvegarder les registres lors d'une interruption, car les registres utilisés par le programme et l'interruption ne sont en réalité pas les mêmes. Les interruptions sont alors plus rapides.

Notons qu'avec ce système, seuls les registres adressables par le programmeur sont dupliqués. Les registres comme le pointeur de pile ou le program counter ne sont pas dupliqués, car ils n'ont pas à l'être. Et attention : certains registres doivent être sauvegardés par l'interruption. Notamment, l'adresse de retour, qui permet de reprendre l'exécution du programme interrompu au bon endroit. Elle est réalisée automatiquement par le processeur.

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.


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.

Dans les chapitres précédents, nous avons parlé des mots mémoire, à savoir des blocs de mémoire dont le nombre de bits correspond à la largeur du bus mémoire. Le premier sens possible est donc la quantité de données que l'on peut transférer entre CPU et RAM en un seul cycle d'horloge. Il s'agit d'une définition basée sur les transferts réels entre processeur et mémoire. Le terme que nous avons utilisé pour cette définition est : mot mémoire. Remarquez la subtile différence entre les termes "mot" et "mot mémoire" : le second terme indique bien qu'il s'agit de quelque de lié à la mémoire, pas le premier. Les deux ne sont pas confondre, et nous allons voir pourquoi.

La définition précédente ne permet pas de définir ce qu'est un byte et un mot, vu que la distinction se fait au niveau du processeur, au niveau du jeu d'instruction. 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. Voyons dans le détail ces histoires de mots, de bytes, et autres.

Les architectures à adressage par mot

[modifier | modifier le wikicode]

Au tout début de l'informatique, sur les anciens ordinateurs datant d'avant les années 80, les processeurs géraient qu'une seule taille pour les données. Par exemple, de tels processeurs ne géraient que des données de 8 bits, pas autre chose. Les données en question était des mots. Aux tout début de l'informatique, certaines machines utilisaient des mots de 3, 4, 5, 6 7, 13, 17, 23, 36 ou 48 bits. Pour donner quelques exemples, l'ordinateur ERA 1103 utilisait des mots de 36-bits, tout comme le PDP-10, et ne gérait pas d'autre taille pour les données : c'était 36 bits pour tout le monde.

Les processeurs en question ne disposaient que d'une seule instruction de lecture/écriture, qui lisait/écrivait des mots entiers. On pouvait ainsi lire ou écrire des paquets de 3, 4, 5, 6 7, 13, 17, 23, 36 ou 48 bits. Les registres du processeur avaient généralement la même taille qu'un mot, ce qui fait que les processeurs de l'époque avaient des registres de 4, 8, 12, 24, 26, 28, 31, 36, 48, voire 60 bits.

Les mots en question sont en théorie à distinguer des mots mémoire, mais ce n'est pas souvent le cas en pratique. Les architectures à adressage par mot faisaient en sorte qu'un mot soit de la même taille qu'un mot mémoire. La mémoire était donc découpée en mots, chacun avait sa propre adresse. Par exemple, une mémoire de 64 kilo-mots contenait 65 536 mots, chacun contenant autant de bits qu'un mot. Les mots faisaient tous la même taille, qui variait suivant la mémoire ou le processeur utilisé. Chaque mot avait sa propre adresse, ce qui fait qu'on parlait d'adressage par mot. Il n'y avait qu'une seule unité d'adressage, ce qui fait que le byte et le mot étaient la même chose sur de telles architectures. La distinction entre byte et mot est apparue après, sur des ordinateurs/processeurs différents.

Les architectures à adressage par byte

[modifier | modifier le wikicode]

Par la suite, des processeurs ont permis d'adresser des données plus petites qu'un mot. Les processeurs en question disposent de plusieurs instructions de lecture/écriture, qui manipulent des blocs de mémoire de taille différente. Par exemple, il peut avoir une instruction de lecture pour lire 8 bits, une autre pour lire 16 bits, une autre 32, etc. Une autre possibilité est celle où le processeur dispose d'une instruction de lecture, qu'on peut configurer suivant qu'on veuille lire/écrire un octet, deux, quatre, huit.

Dans ce cas, on peut faire une distinction entre byte et mot : le byte est la plus petite donnée, le mot est la plus grande. Par exemple, un processeur disposant d'instruction d'accès mémoire capables de lire/écrire 8 ou 16 bits sont dans ce cas. Le byte fait alors 8 bits, le mot en fait 16. La séparation entre byte et mot peut parfois se compléter avec des tailles intermédiaires. Par exemple, prenons un processeur qui dispose d'une instruction de lecture capable de lire soit 8 bits, soit 16 bits, soit 32 bits, soit 64 bits. Dans ce cas, le byte vaut 8 bits, le mot en fait 64, les autres tailles sont des intermédiaires. Pour résumer, un mot est la plus grande unité adressable par le processeur, un byte est la plus petite.

En général, le byte fait 8 bits, un octet. Mais ça n'a pas toujours été le cas, pas mal de jeux d'instructions font exception. L'exemple le plus parlant est celui des processeurs décimaux, qui utilisaient des entiers codés en BCD mais ne géraient pas les entiers codés en binaire normal. De tels processeurs encodaient des nombres sous la forme d'une suite de chiffres décimaux, codés en BCD sur 4 bits. Ils avaient des bytes de 4 bits, voire de 5/6 bits pour les ordinateurs qui ajoutaient un bit de parité/ECC par chiffre décimal. D'autres architectures avaient un byte de 3 à 7 bits.

La taille d'un mot mémoire est de plusieurs bytes : un mot mémoire contient un nombre entier de bytes. La norme actuelle est d'utiliser des bytes d'un octet (8 bits), avec des mots contenant plusieurs octets. 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, mais l'usage de mots qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra plus bas.

Sur de telles architectures, il y a une adresse mémoire par byte, et non par mot, ce qui fait qu'on parle d'adressage par byte. Tous les ordinateurs modernes utilisent l'adressage par byte. Concrètement, sur les processeurs modernes, chaque octet de la mémoire a sa propre adresse, peu importe la taille du mot utilisé par le processeur. Par exemple, les anciens processeurs x86 32 bits et les processeurs x86 64 bits utilisent tous le même système d'adressage, où chaque octet a sa propre adresse, la seule différence est que les adresses sont plus nombreuses. Avec un adressage par mot, on aurait eu autant d'adresses qu'avant, mais les mots seraient passés de 32 à 64 bits en passant au 64 bits. Les registres font encore une fois la même taille qu'un mot, bien qu'il existe quelques rares exceptions.

Les processeurs à adressage par byte ont souvent plusieurs instructions de lecture/écriture, chacune pour une taille précise. Pour rendre cela plus concret, prenons le cas de l'instruction de lecture. Il y a au minimum une instruction de lecture qui lit un byte en mémoire, une autre qui lit un mot complet. Il y a souvent 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.

Les architectures à adressage par mot de type hybrides

[modifier | modifier le wikicode]

Il a existé des architectures adressées par mot qui géraient des bytes, mais sans pour autant leur donner des adresses. Leur idée était que les transferts entre CPU et mémoire se faisaient par mots, mais les instructions de lecture/écriture pouvaient 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 l'adresse identifie un mot, pas un byte. Les bytes en question n'avaient pas d'adresses en eux-mêmes, il n'y avait pas d'adressage par byte. La sélection des bytes se faisait dans le processeur : le processeur lisait des mots entiers, avant que le hardware du processeur sélectionne automatiquement le byte voulu. D'ailleurs, aucune de ces architectures ne supportait de mode d'adressage base+index ou base+offset pour sélectionner des bytes dans un mot. Elles supportaient de tels modes d'adressage pour un mot, pas pour les bytes. Pour faire la différence, nous parlerons de pseudo-byte dans ce qui suit, pour bien préciser que ce ne sont pas de vrais bytes.

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 pseudo-byte dans le mot, et enfin la taille d'un pseudo-byte ! L'adressage était donc très flexible, car on pouvait configurer la taille du pseudo-byte. Outre l'instruction de lecture LDB et celle d'écriture DPB, d'autres instructions permettaient de manipuler des pseudo-bytes. L'instruction IBP incrémentait le numéro du pseudo-byte, par exemple.

Les architectures à mot de taille variable

[modifier | modifier le wikicode]

D'autres architectures codaient leurs nombres en utilisant un nombre variable de bytes ! Dit autrement, elles avaient des mots de taille variable, d'où leur nom d'architectures à mots de taille variable. Il s'agit d'architectures qui codaient les nombres par des chaines de caractères terminées par un byte de terminaison.

La grande majorité étaient des architectures décimales, à savoir des ordinateurs qui utilisaient des nombres encodés en BCD ou dans un encodage similaire. Les nombres étaient codés en décimal, mais chaque chiffre était encodé en binaire sur quelques bits, généralement 4 à 6 bits. Les bytes stockaient chacun un caractère, qui était utilisé pour encoder soit un chiffre décimal, soit un autre symbole comme un byte de terminaison. Un caractère faisait plus de 4 bits, vu qu'il fallait au minimum coder les chiffres BCD et des symboles supplémentaires. La taille d'un caractère était généralement de 5/6 bits.

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 des chiffres codés en BCD, chaque caractère était codé sur 6 bits. 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. Chaque caractère/chiffre avait sa propre adresse, ce qui fait l'architecture est techniquement adressable par byte, alors que les mots correspondaient aux nombres de taille variable.

La comparaison entre l'adressage par mot et par byte

[modifier | modifier le wikicode]

Plus haut, nous avons vu deux types d'adressage : par mot et par byte. Avec la première, ce sont les mots qui ont des adresses. Les bytes n'existent pas forcément sur de telles architectures. Si une gestion des bytes est présente, les instructions de lecture/écriture utilisent des adresses pour les mots, couplé à la position du byte dans le mot. Les lectures/écritures se font pas mots entiers. À l'opposé, sur les architectures adressées par byte, une adresse correspond à un byte et non à un mot.

Les deux techniques font que l'usage des adresses est différent. Entre une adresse par mot et une par byte, le nombre d'adresse n'est pas le même à capacité mémoire égale. Prenons un exemple assez simple, où l'on compare deux processeurs. Les deux ont des mots mémoire de 32 bits, pour simplifier la comparaison. Le premier processeur gère des bytes de 8 bits, et chacun a sa propre adresse, ce qui fait que c'est un adressage par byte qui est utilisé. Le second ne gère pas les bytes mais seulement des mots de 32 bits, ce qui fait que c'est un adressage par mot qui est utilisé.

Dans les deux cas, la mémoire n'est pas organisée de la même manière. Prenons une mémoire de 24 octets pour l'exemple, soit 24/4 = 6 mots de 4 octets. Le premier processeur aura une adresse par byte, soit 24 adresses, et ce sera pareil pour la mémoire, qui utilisera une case mémoire par byte. Le second processeur n'aura que 6 adresses : une par mot. La mémoire a des cases mémoire qui contiennent un mot entier, soit 32 bits, 4 octets.

Adressage par mot et par Byte.

L'avantage de l'adressage par mot est que l'on peut adresser plus 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 un mot mémoire de 4 bytes d'un octet chacun, on peut adresser : soit 65 536 bytes/octets, soit 65 536mots et donc 65 536 × 4 octets. L'adressage par mot permet donc d'adresser plus de mémoire avec les mêmes adresses. Une autre manière de voir les choses est qu'une architecture à adressage par byte va utiliser beaucoup plus d'adresses qu'une architecture par mot, à capacité mémoire égale.

L'avantage des architectures à 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 indépendamment des autres, 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. Le tout est plus simple avec l'adressage par byte : pas besoin d'opérations de masquage !

Par contre, les architectures à adressage par byte ont de nombreux 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.

Le boutisme : une spécificité de l'adressage par byte

[modifier | modifier le wikicode]

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

L'implémentation de l'adressage par byte au niveau de la mémoire RAM/ROM

[modifier | modifier le wikicode]

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 cas classiques sont les plus courants.

Les architectures avec une mémoire adressable par byte

[modifier | modifier le wikicode]

Le premier est celui où le bus mémoire 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. Ce qui explique le désavantage de cette méthode : la performance est assez mauvaise. 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.

L'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 localisée à l'adresse 4, puis lire une autre donnée de 16 bits localisée à 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]

Pour éviter d'avoir des performances désastreuses, on utilise une autre solution : le bus mémoire a la largeur nécessaire pour lire un mot entier. Le processeur peut charger un mot mémoire entier dans ses registres, en un seul accès mémoire. Et pour lire des données plus petites qu'un mot mémoire, le processeur charge un mot complet, mais ignore les octets en trop.

Exemple du chargement d'un octet dans un registre de trois octets.

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. Mais dans ce cas, 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 ! Tout se passe comme si la mémoire était découpée en blocs de la taille d'un mot. La capacité de la mémoire reste inchangée, ce qui fait que le nombre d'adresses utilisables diminue : il n'y a plus besoin que d'une adresse par mot mémoire et non par octet. Il faut donc faire une sorte d'interface entre les deux.

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

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 pas 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'alignement mémoire des données

[modifier | modifier le wikicode]

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

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]

Les instructions ont toute une certaine taille, et elles peuvent être de taille fixe (toutes les instructions font X octets), ou de taille variable (le nombre d'octets dépend de l'instruction). Dans les deux cas, 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.

Pour les instructions de taille fixe, les instructions sont placées à des adresses précises. Par exemple, prenons des instructions 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 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.

Sur d'autres processeurs, les instructions doivent être alignées. Dans le cas le plus simple, les instructions doivent être 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. D'autres architectures ont des contraintes d'alignement bizarres. 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. 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.

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.

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.


La micro-architecture

[modifier | modifier le wikicode]

Dans le chapitre sur le langage machine, on a vu notre processeur comme une espèce de boite noire contenant des registres qui exécutait des instructions les unes après les autres et pouvait accéder à la mémoire. Mais on n'a pas encore vu comment celui-ci était organisé et comment celui-ci fait pour exécuter une instruction. Pour cela, il va falloir nous attaquer à la micro-architecture du processeur. C'est le but de ce chapitre : montrer comment les grands circuits de notre processeur sont organisés et comment ceux-ci permettent d’exécuter une instruction. On verra que notre processeur est très organisé et est divisé en plusieurs grands circuits qui effectuent des fonctions différentes.

L'exécution d'une instruction

[modifier | modifier le wikicode]

Le but d'un processeur, c'est d’exécuter une instruction. Cela nécessite de faire quelques manipulations assez spécifiques et qui sont toutes les mêmes quel que soit l'ordinateur. Pour exécuter une instruction, notre processeur va devoir faire son travail en effectuant des étapes bien précises.

Le cycle d'exécution d'une instruction

[modifier | modifier le wikicode]
Les trois cycles d'une instruction.

Pour exécuter une instruction, le processeur va effectuer trois étapes :

  • le processeur charger l'instruction depuis la mémoire : c'est l'étape de chargement (Fetch) ;
  • ensuite, le processeur « étudie » la suite de bits de l'instruction et en déduit quelle est l'instruction à éxecuter : c'est l'étape de décodage (Decode) ;
  • enfin, le processeur exécute l'instruction : c'est l'étape d’exécution (Execute).

On verra plus tard dans le cours qu'une quatrième étape peut être ajoutée : l'étape d'interruption. Celle-ci permet de gérer des fonctionnalités du processeur nommées interruptions. Nous en parlerons dans le chapitre sur la communication avec les entrées-sorties.

Les micro-instructions

[modifier | modifier le wikicode]

Ces trois étapes ne s'effectuent cependant pas d'un seul bloc. Chaque de ces étapes est elle-même découpée en plusieurs sous-étapes, qui va échanger des données entre registres, effectuer un calcul, ou communiquer avec la mémoire. Pour l'étape de chargement, on peut être sûr que tous les processeurs vont faire la même chose : il n'y a pas 36 façons pour lire une instruction depuis la mémoire. Même chose pour la plupart des processeur, pour l'étape de décodage. Mais cela change pour l'étape d’exécution : toutes les instructions n'ont pas les mêmes besoins suivant ce qu'elles font ou leur mode d'adressage. Voyons cela avec quelques exemples.

Commençons par prendre l'exemple d'une instruction de lecture ou d'écriture en mode d'adressage absolu. Vu son mode d'adressage, l'instruction va indiquer l'adresse à laquelle lire dans sa suite de bits qui la représente en mémoire. L’exécution de l'instruction se fait donc en une seule étape : la lecture proprement dite. Mais si l'on utilise des modes d'adressages plus complexes, les choses changent un petit peu. Reprenons notre instruction Load, mais en utilisant une mode d'adressage utilisé pour des données plus complexe. Par exemple, on va prendre un mode d'adressage du style Base + Index. Avec ce mode d'adressage, l'adresse doit être calculée à partir d'une adresse de base, et d'un indice, les deux étant stockés dans des registres. En plus de devoir lire notre donnée, notre instruction va devoir calculer l'adresse en fonction du contenu fourni par deux registres. L'étape d’exécution s'effectue dorénavant en deux étapes assez différentes : une implique un calcul d'adresse, et l'autre implique un accès à la mémoire.

Prenons maintenant le cas d'une instruction d'addition. Celle-ci va additionner deux opérandes, qui peuvent être soit des registres, soit des données placées en mémoires, soit des constantes. Si les deux opérandes sont dans un registre et que le résultat doit être placé dans un registre, la situation est assez simple : la récupération des opérandes dans les registres, le calcul, et l'enregistrement du résultat dans les registres sont trois étapes distinctes. Maintenant, autre exemple : une opérande est à aller chercher dans la mémoire, une autre dans un registre, et le résultat doit être enregistré dans un registre. On doit alors rajouter une étape : on doit aller chercher la donnée en mémoire. Et on peut aller plus loin en allant cherche notre première opérande en mémoire : il suffit d'utiliser le mode d'adressage Base + Index pour celle-ci. On doit alors rajouter une étape de calcul d'adresse en plus. Ne parlons pas des cas encore pire du style : une opérande en mémoire, l'autre dans un registre, et stocker le résultat en mémoire.

Bref, on voit bien que l’exécution d'une instruction s'effectue en plusieurs étapes distinctes, qui vont soit faire un calcul, soit échanger des données entre registres, soit communiquer avec la RAM. Chaque étape s'appelle une micro-opération, ou encore une micro-instruction. Toute instruction machine est équivalente à une suite de micro-opérations exécutée dans un ordre précis. Dit autrement, chaque instruction machine est traduite en suite de micro-opérations à chaque fois qu'on l’exécute. Certaines µinstructions font un cycle d'horloge, alors que d'autres peuvent prendre plusieurs cycles. Un accès mémoire en RAM peut prendre 200 cycles d'horloge et ne représenter qu'une seule µinstruction, par exemple. Même chose pour certaines opérations de calcul, comme des divisions ou multiplication, qui correspondent à une seule µinstruction mais prennent plusieurs cycles.

Micro-operations

La micro-architecture d'un processeur

[modifier | modifier le wikicode]

Conceptuellement, il est possible de segmenter les circuits du processeur en circuits spécialisés : des circuits chargés de faire des calculs, d'autres chargés de gérer les accès mémoires, etc. Ces circuits sont eux-mêmes regroupés en deux entités : le chemin de données et l'unité de contrôle. Le tout est illustré ci-contre.

  • Le chemin de données est l'ensemble des composants où circulent les données, là où se font les calculs, là où se font les échanges entre mémoire RAM et registres, etc. Il contient un circuit pour faire les calculs, appelé l'unité de calcul, les registres et un circuit de communication avec la mémoire. On l'appelle ainsi parce que c'est dans ce chemin de données que les données vont circuler et être traitées dans le processeur.
  • L’unité de contrôle charge et interprète les instructions, pour commander le chemin de données. Elle est en charge du chargement et du décodage de l'instruction. Elle regroupe un circuit chargé du Fetch, et un décodeur chargé de l'étape de Decode.

Le chemin de données

[modifier | modifier le wikicode]

Pour effectuer ces calculs, le processeur contient un circuit spécialisé : l'unité de calcul. De plus, le processeur contient des registres, ainsi qu'un circuit d'interface mémoire. Les registres, l'unité de calcul, et l'interface mémoire sont reliés entre eux par un ensemble de fils afin de pouvoir échanger des informations : par exemple, le contenu des registres doit pouvoir être envoyé en entrée de l'unité de calcul, pour additionner leur contenu par exemple. Ce groupe de fils forme ce qu'on appelle le bus interne du processeur. L'ensemble formé par ces composants s’appelle le chemin de données.

Chemin de données

L'unité de contrôle

[modifier | modifier le wikicode]

Si le chemin de données s'occupe de tout ce qui a trait aux donnés, il est complété par un circuit qui s'occupe de tout ce qui a trait aux instructions elles-mêmes. Ce circuit, l'unité de contrôle va notamment charger l'instruction dans le processeur, depuis la mémoire RAM. Il va ensuite configurer le chemin de données pour effectuer l'instruction. Il faut bien contrôler le mouvement des informations dans le chemin de données pour que les calculs se passent sans encombre. Pour cela, l'unité de contrôle contient un circuit : le séquenceur. Ce séquenceur envoie des signaux au chemin de données pour le configurer et le commander.

Il est évident que pour exécuter une suite d'instructions dans le bon ordre, le processeur doit savoir quelle est la prochaine instruction à exécuter : il doit donc contenir une mémoire qui stocke cette information. C'est le rôle du registre d'adresse d'instruction, aussi appelé program counter. Cette adresse ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples. Généralement, on profite du fait que le programmeur/compilateur place les instructions les unes à la suite des autres en mémoire, dans l'ordre où elles doivent être exécutées. Ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction chargée au program counter.

Intérieur d'un processeur

Mais sur d'autres processeurs, chaque instruction précise l'adresse de la suivante. Ces processeurs n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau d'argent. Sur de tels processeurs, chaque instruction précise quelle est la prochaine instruction, directement dans la suite de bit représentant l'instruction en mémoire. Les processeurs de ce type contiennent toujours un registre d'adresse d'instruction, pour faciliter l’interfaçage avec le bus d'adresse. La partie de l'instruction stockant l'adresse de la prochaine instruction est alors recopiée dans ce registre, pour faciliter sa copie sur le bus d'adresse. Mais le compteur ordinal n'existe pas. Sur des processeurs aussi bizarres, pas besoin de stocker les instructions en mémoire dans l'ordre dans lesquelles elles sont censées être exécutées. Mais ces processeurs sont très très rares et peuvent être considérés comme des exceptions à la règle.

Encodage d'une instruction sur un processeur sans Program Counter.

Des processeurs vendus en kit aux premiers microprocesseurs

[modifier | modifier le wikicode]

Un processeur est un circuit assez complexe et qui utilise beaucoup de transistors. Avant les années 1970, il n'était pas possible de produire un processeur en un seul morceau. Impossible de mettre un processeur dans un seul boitier. Les tout premiers processeurs étaient fabriqués porte logique par porte logique et comprenaient plusieurs milliers de boitiers reliés entre eux. Par la suite, les progrès de la miniaturisation permirent de faire des pièces plus grandes. L'invention du microprocesseur permis de placer tout le processeur dans un seul boitier, une seule puce électronique.

Avant l'invention du microprocesseur

[modifier | modifier le wikicode]

Avant l'invention du microprocesseur, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Le processeur était composé de plusieurs circuits intégrés, placés sur la même carte mère et connectés ensemble par des fils métalliques. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés : l'Intel 3001 est le séquenceur, l'Intel 3002 est le chemin de données (ALU et registres), le 3003 est un circuit d'anticipation de retenue censé être combiné avec l'ALU, le 3212 est une mémoire tampon, le 3214 est une unité de gestion des interruptions, les 3216/3226 sont des interfaces de bus mémoire. On pourrait aussi citer la famille de circuits intégrés AMD Am2900.

Les ALUs en pièces détachées de l'époque étaient assez simples et géraient 2, 4, 8 bits, rarement 16 bits. Et il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes, par exemple combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Il s'agit de la méthode du bit slicing que nous avions abordée dans le chapitre sur les unités de calcul.

L'intel 4004 : le premier microprocesseur

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

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

L'évolution des processeurs dans le temps

[modifier | modifier le wikicode]

La miniaturisation a eu des conséquences notables sur la manière dont sont conçus les processeurs, les mémoires et tous les circuits électroniques en général. On pourrait croire que la miniaturisation a entrainé une augmentation de la complexité des processeurs avec le temps, mais les choses sont à nuancer. Certes, on peut faire beaucoup plus de choses avec un milliard de transistors qu'avec seulement 10000 transistors, ce qui fait que les puces modernes sont d'une certaine manière plus complexes. Mais les anciens processeurs avaient une complexité cachée liée justement au faible nombre de transistors.

Il est difficile de concevoir des circuits avec un faible nombre de transistors, ce qui fait que les fabricants de processeurs devaient utiliser des ruses de sioux pour économiser des transistors. Les circuits des processeurs étaient ainsi fortement optimisés pour économiser des portes logiques, à tous les niveaux. Les circuits les plus simples étaient optimisés à mort, on évitait de dupliquer des circuits, on partageait les circuits au maximum, etc. La conception interne de ces processeurs était simple au premier abord, mais avec quelques pointes de complexité dispersées dans toute la puce.

De nos jours, les processeurs n'ont plus à économiser du transistor et le résultat est à double tranchant. Certes, ils n'ont plus à utiliser des optimisations pour économiser du circuit, mais ils vont au contraire utiliser leurs transistors pour rendre le processeur plus rapide. Beaucoup des techniques que nous verrons dans ce cours, comme l’exécution dans le désordre, le renommage de registres, les mémoires caches, la présence de plusieurs circuits de calcul, et bien d'autres ; améliorent les performances du processeur en ajoutant des circuits en plus. De plus, on n'hésite plus à dupliquer des circuits qu'on aurait autrefois mis en un seul exemplaire partagé. Tout cela rend le processeur plus complexe à l'intérieur.

Une autre contrainte est la facilité de programmation. Les premiers processeurs devaient faciliter au plus la vie du programmeur. Il s'agissait d'une époque où on programmait en assembleur, c'est à dire en utilisant directement les instructions du processeur ! Les processeurs de l'époque utilisaient des jeu d'instruction CISC pour faciliter la vie du programmeur. Pourtant, ils avaient aussi des caractéristiques gênantes pour les programmeurs qui s'expliquent surtout par le faible nombre de transistors de l'époque : peu de registres, registres spécialisés, architectures à pile ou à accumulateur, etc. Ces processeurs étaient assez étranges pour les programmeurs : très simples sur certains points, difficiles pour d'autres.

Les processeurs modernes ont d'autres contraintes. Grâce à la grande quantité de transistors dont ils disposent, ils incorporent des caractéristiques qui les rendent plus simples à programmer et à comprendre (registres banalisés, architectures LOAD-STORE, beaucoup de registres, moins d'instructions complexes, autres). De plus, si on ne programme plus les processeurs à la main, les langages de haut niveau passe par des compilateurs qui eux, programment le processeur. Leur interface avec le logiciel a été simplifiée pour coller au mieux avec ce que savent faire les compilateurs. En conséquence, l’interface logicielle des processeurs modernes est paradoxalement plus minimaliste que pour les vieux processeurs.

Tout cela pour dire que la conception d'un processeur est une affaire de compromis, comme n'importe quelle tâche d'ingénierie. Il n'y a pas de solution parfaite, pas de solution miracle, juste différentes manières de faire qui collent plus ou moins avec la situation. Et les compromis changent avec l'époque et l'évolution de la technologie. Les technologies sont toutes interdépendantes, chaque évolution concernant les transistors influence la conception des puces électroniques, les technologies architecturales utilisées, ce qui influence l'interface avec le logiciel, ce qui influence ce qu'il est possible de faire en logiciel. Et inversement, les contraintes du logiciel influencent les niveaux les plus bas, et ainsi de suite. Cette morale nous suivra dans le reste du cours, où nous verrons qu'il est souvent possible de résoudre un problème de plusieurs manières différentes, toutes utiles, mais avec des avantages et inconvénients différents.


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 canoniques 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-instructions. Chaque instruction du langage machine configure directement le bus interne au processeur. Elles peuvent relier les ALU aux registres, relier les registres entre eux, effectuer un branchement, etc. De tels processeurs n'ont pas besoin d'un décodeur d'instruction pour traduire les instructions machines en signaux de commandes. Mais elles ont quand même besoin d'une unité de chargement, d'un program counter et des circuits pour gérer les branchements.

Architecture déclenchée par déplacement (Transport Triggered Architecture).

Les avantages et désavantages d'un processeur actionné par déplacement

[modifier | 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/Le partage de l'espace d'adressage : avec et sans multiprogrammation 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/La mémoire virtuelle 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 Fonctionnement d'un ordinateur/Les pipelines de longueur fixe et dynamiques

Les branchements et le front-end

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les exceptions précises et branchements 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/L'émission dans l'ordre des instructions Fonctionnement d'un ordinateur/Les dépendances de données et 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

Les accès mémoire avec un pipeline

[modifier | modifier le wikicode]

Fonctionnement d'un ordinateur/Les unités mémoires à exécution dans l'ordre 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/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/Les mémoires historiques 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 architectures neuromorphiques Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires Fonctionnement d'un ordinateur/Les circuits réversibles