Version en ligne

Tutoriel : Fonctionnement d'un ordinateur depuis zéro

Table des matières

Fonctionnement d'un ordinateur depuis zéro
Un ordinateur, c'est très bête : ça ne sait pas compter jusqu'à deux !
Nombres entiers
Nombres à virgule
Codage du texte
Nos bits prennent la porte !
Codage NRZ
Transistors
Portes logiques
Créons nos circuits !
Circuits combinatoires
Circuits séquentiels
Tic, Tac, Tic, Tac : Le signal d'horloge
C'est quoi un ordinateur ?
Numérique versus analogique
Architecture de base
Ordinateurs
La gestion de la mémoire
Deux mémoires pour le prix d'une
L'organisation de la mémoire et la pile
Machines à pile et successeurs
Langage machine et assembleur
Instructions
Jeux d'instruction
Registres architecturaux
Représentation en binaire
Classes d'architectures
Un peu de programmation !
C'est un ordre, éxecution !
Et que ca saute !
Structures de contrôle, tests et boucles
Sous-programmes : c'est fait en quoi une fonction ?
Il y a quoi dans un processeur ?
Execution d'une instruction
Les unités de calcul
Registres et interface mémoire
Le chemin de données
Le séquenceur
L'étape de fetch
Les circuits d'une ALU entiére
Décalages et rotations
Addition
Les Overflows
Soustraction
Multiplication
Division
Mémoires
Des mémoires en veux-tu, en voilà !
Donnée, où es-tu ?
Une histoire de bus
Toutes les mémoires ne se valent pas !
Mémoriser un bit
Mémoire SRAM
Mémoire DRAM
Correction d'erreurs
Contrôleur et plan mémoire
Mémoires à adressage linéaire
Mémoires à adressage par coicidence
Mémoire à Row Buffer
Interfacage avec le bus
Assemblages de mémoires
Mémoires DDR, SDRAM et leurs cousines
Les mémoires RAM asynchrones
Les mémoires SDRAM
Les mémoires DDR
Mémoires non-volatiles
Le disque dur
Mémoires FLASH
Bus, cartes mères, chipsets et Front Side Bus
Un bus, c'est rien qu'un tas de fils...
Va falloir partager !
Chipset, back-plane bus, et autres
Communication avec les Entrées-Sorties
Interfacage Entrées-sorties
Interruptions
Direct Memory Access
Adressage des périphériques
Connexion directe
Espace d'adressage séparé
Entrées-sorties mappées en mémoire
La mémoire virtuelle
Solutions matérielles
Segmentation
Pagination
Les mémoires caches
Accés au cache
Localité spatiale et temporelle
Correspondance Index - Adresse
Remplacement des lignes de cache
On n'a pas qu'un seul cache !
Le Prefetching
Array Prefetching
Linked Data Structures Prefetching
Runahead Data Prefetching
Instruction Prefetching
Le pipeline : qu'est-ce que c'est ?
Un besoin : le parallèlisme d'instruction
Le pipeline : rien à voir avec un quelconque tuyau à pétrole !
Etages, circuits et fréquence
Implémentation hardware
Pipelines complexes
Interruptions et Pipeline
NOP Insertion
In-Order Completion
Out Of Order Completion
Les branchements viennent mettre un peu d'ambiance !
Solutions non-spéculatives
Prédiction de branchement
Eager execution
Dépendances de données
Dépendances d'instructions
Pipeline Bubble / Stall
Bypass et Forwarding
Execution Out Of Order
Principe
Scoreboarding
Out Of Order Issue
L'algorithme de Tomasulo et le renommage de registres
Le renommage de registres
Reservations stations
Re-Orders Buffers
Autres formes de renommages
L'unité de renommage
Les optimisations des accès mémoire
Dépendances, le retour !
Dépendances de nommage
Dependances d'alias
Load Adress Prediction
Load Value Prediction
Processeurs Multiple Issue
Processeurs superscalaires
Processeurs VLIW
Processeurs EPIC
Alignement mémoire et endianess
Alignement mémoire
Endianness

Fonctionnement d'un ordinateur depuis zéro

Vous vous êtes déjà demandé comment fonctionne un ordinateur ou ce qu'il y a dedans ?

Alors ce tutoriel est fait pour vous.

Dans ce cours, vous allez apprendre ce qu'il y a dans notre ordinateur, ce qui se passe à l'intérieur de votre processeur ou de votre mémoire RAM. Vous saurez tout des dernières innovations présentes dans nos processeurs, pourquoi la course à la fréquence est terminée, ou encore comment fabriquer des registres. On commencera par des choses simples comme le binaire, pour arriver progressivement jusqu'au fonctionnement des derniers processeurs, en passant par plein de choses passionnantes comme l'assembleur, les mémoires caches, et d'autres choses encore !

Ce tutoriel ne posera pas de soucis, même pour ceux qui n’ont jamais programmé ou qui débutent tout juste : ce cours est accessible à n'importe qui, sans vraiment de prérequis. En clair : on part de zéro !

Un ordinateur, c'est très bête : ça ne sait pas compter jusqu'à deux !

Nombres entiers

On a sûrement déjà dû vous dire qu'un ordinateur comptait uniquement avec des zéros et des uns. Et bien sachez que c'est vrai : on dit que notre ordinateur utilise la numération binaire.

Le binaire, qu'est-ce que c'est que ce truc ?

C'est juste une façon de représenter un nombre en utilisant seulement des 0 et des 1. Et un ordinateur ne sait compter qu'en binaire. Toutefois, le binaire ne sert pas qu'à stocker des nombres dans notre ordinateur. Après tout, votre ordinateur ne fait pas que manipuler des nombres : il peut aussi manipuler du texte, de la vidéo, du son, et pleins d'autres choses encore. Eh bien, sachez que tout cela est stocké... avec uniquement des 0 et des 1. Que ce soit du son, de la vidéo, ou tout autre type de donnée manipulable par notre ordinateur, ces données sont stockées sous la forme de suites de zéros et de uns que notre ordinateur pourra manipuler comme bon lui semble.

Pour comprendre le fonctionnement d'un ordinateur, on va donc devoir aborder le binaire. Nous allons commencer par voir comment sont stockées quelques données de base comme les nombres ou le texte. Et pour cela, nous allons commencer par un petit rappel pour ceux qui n'ont jamais été en CM1. :p

Nombres entiers

Un ordinateur, c'est très bête : ça ne sait pas compter jusqu'à deux ! Nombres à virgule

Nombres entiers

Nous allons commencer par parler des nombres entiers.

Dans notre système de représentation décimal, nous utilisons dix chiffres pour écrire nos nombres entiers positifs : 0, 1, 2, 3, 4, 5, 6, 7, 8 et 9.

Prenons le nombre 1337. Le chiffre le plus à droite est le chiffre des unités, celui à côté est pour les dizaines, suivi du chiffre des centaines...
Cela nous donne :

1 imes 1000 + 3 imes 100 + 3 imes 10 + 7 imes 1

Jusque là vous devez vous ennuyer, non (Enfin j'espère ! :p ) ?
Bref, reprenons notre nombre 1337. On va remplacer les unités, dizaines, centaines et milliers par leurs puissances de dix respectives :

1 imes 10^3 + 3 imes 10^2 + 3 imes 10^1 + 7 imes 10^0

Tous les nombres entiers qui existent peuvent eux aussi être écrits sous cette forme : on peut les décomposer en une somme de multiples de puissances de 10. Lorsque c'est le cas, on dit qu'ils sont en base 10.

Différentes bases

Ce qui peut être fait avec des puissances de 10 peut être fait avec des puissances de 2, 3, 4, 125, etc : on peut utiliser d’autres bases que la base 10. Rien n’empêche de décomposer un nombre en une somme de multiples de puissance de 2, ou de 3, par exemple. On peut ainsi utiliser d'autres bases.

En informatique, on utilise rarement la base 10 à laquelle nous sommes tant habitués. Nous utilisons à la place deux autres bases :

Voici le tableau des 16 premiers nombres des bases citées ci-dessus :

Base 10

Base 2

Base 16

0

0

0

1

1

1

2

10

2

3

11

3

4

100

4

5

101

5

6

110

6

7

111

7

8

1000

8

9

1001

9

10

1010

A

11

1011

B

12

1100

C

13

1101

D

14

1110

E

15

1111

F

Le binaire, la base 2

Le binaire, c'est la base 2. Seuls deux chiffres sont utilisés : 0 et 1. Lorsque vous écrivez un nombre en binaire, celui-ci peut toujours être écrit sous la forme d'une somme de puissances de 2.

Par exemple 6 s'écrira donc 0110 en binaire : 0 imes 2^3 + 1 imes 2^2 + 1 imes 2^1 + 0 imes 2^0 = 6

En général, tout nombre en binaire s'écrit sous la forme a_0 imes 2^0 + a_1 imes 2^1 + a_2 imes 2^2 + a_3 imes 2^3 + a_4 imes 2^4 +...+ a_n imes 2^n.

Les coefficients a_0, a_1, a_2 ... valent 1 ou 0. Ces coefficients ne sont rien d'autres que les "chiffres" de notre nombre écrit en base 2. Ces "chiffres" d'un nombre codé en binaire sont aussi appelés des bits. Pour simplifier, on peut dire qu'un bit est un truc qui vaut 0 ou 1.

L'exposant qui correspond à un bit a_n est appelé le poids du bit. Le bit de poids faible est celui qui a la plus petite valeur dans un nombre : c'est celui qui est le plus à droite du nombre (si vous écrivez vos nombres dans le bon sens, évidemment). Le bit de poids fort c'est l'inverse, évidemment : c'est celui qui est placé le plus à gauche. :lol:

Capacité

Petite remarque assez importante : avec n bits, on peut coder 2^n valeurs différentes, dont le 0. Ce qui fait qu'on peut compter de 0 à 2^n-1. N'oubliez pas cette petite remarque : elle sera assez utile dans le suite de ce tutoriel.

Changement de base

La représentation des entiers positifs en binaire est très simple : il suffit simplement de changer de base, et de passer de la base 10 à la base 2. Il existe un algorithme qui permet de changer un nombre en base décimale vers un nombre en base binaire : il consiste à diviser itérativement le quotient de la division précédente par 2, et de noter le reste. Enfin, il faut lire de bas en haut les restes trouvés.

Exemple :

Image utilisateur

Soit 100010 en binaire.

Représentation en signe-valeur absolue

Bref, maintenant qu'on a vu les entiers strictement positifs ou nuls, on va voir comment faire pour représenter les entiers négatifs n binaire. Avec nos 1 et nos 0, comment va-t-on faire pour représenter le signe moins ("-") ? Eh bien, il existe plusieurs méthodes. Les plus utilisées sont :

La solution la plus simple pour représenter un entier négatif consiste à coder sa valeur absolue en binaire, et rajouter un bit de signe au tout début du nombre. Ce bit servira à préciser si c'est un entier positif ou un entier négatif. C'est un peu la même chose qu'avec les nombres usuels : pour écrire un nombre négatif, on écrit sa valeur absolue, en plaçant un moins devant. Ici, c'est la même chose, le bit de signe servant de signe moins (quand il vaut 1) ou plus (quand il vaut 0).

Bit de signe

Nombre codé en binaire sur n bits

Par convention, ce bit de signe est égal à :

Exemple :

Capacité

En utilisant n bits, bit de signe inclut, un nombre codé en représentation signe-valeur absolue peut prendre toute valeur comprise entre - ( \frac { 2^{n} } {2} - 1 ) et \frac { 2^{n} } {2} - 1. Cela vient du fait qu'on utilise un bit pour le signe : il reste alors N-1 bits pour coder les valeurs absolues. Ces N-1 bits permettent alors de coder des valeurs absolues allant de 0 à \frac { 2^{n} } {2} - 1.

Avec 4 bits, cela donne ceci :

Image utilisateur

On remarque que l'intervalle des entiers représentables sur N bits est symétrique : pour chaque nombre représentable sur n bits en représentation signe-valeur absolue, son inverse l'est aussi.

Désavantages

Vous avez certainement remarqué que le zéro, est représentable par deux entiers signés différents, quand on utilise la représentation signe-magnitude.
Exemple avec un nombre dont la valeur absolue est codée sur 8 bits, et un bit de signe au début. Le bit de signe est coloré en rouge.
00000 0000 = 0
10000 0000 = -0, ce qui est égal à zéro.

Comme vous le voyez sur cet exemple, le zéro est présent deux fois : un -0, et un +0. Cela peut parfois poser certains problèmes, lorsqu'on demande à notre ordinateur d'effectuer des calculs ou des comparaisons avec zéro par exemple.

Il y a un autre petit problème avec ces entiers signe-valeur absolue : faire des calculs dessus est assez compliqué. Comme on le verra plus tard, nos ordinateurs disposent de circuits capables d'additionner, de multiplier, diviser, ou soustraire deux nombres entiers. Et les circuits capables de faire des opérations sur des entiers représentés en signe-magnitude sont compliqués à fabriquer et assez lents, ce qui est une désavantage.

Codage en complément à 1

Passons maintenant à une autre méthode de codage des nombres entiers qu'on appelle le codage en complément à 1. Cette méthode est très simple. Si le nombre à écrire en binaire est positif, on le convertit en binaire, sans rien faire de spécial. Par contre, si ce nombre est un nombre négatif, on code sa valeur absolue en binaire et on inverse tous les bits du nombre obtenu : les 0 deviennent des 1, et vice-versa.

Avec cette méthode, on peut remarquer que le bit de poids fort (le bit le plus à gauche) vaut 1 si le nombre est négatif, et 0 si le nombre représenté est positif. Celui-ci se comporte comme un bit de signe. Par contre, il y a un petit changement comparé à la représentation en signe-valeur absolue : le reste du nombre (sans le bit de signe) n'est pas égal à sa valeur absolue si le nombre est négatif.

Capacité

En utilisant n bits, un nombre représenté en complément à un peut prendre toute valeur comprise entre - ( \frac { 2^{n} } {2} - 1 ) et \frac { 2^{n} } {2} - 1 : pas de changements avec la représentation signe-valeur absolue.

Par contre, les nombres ne sont pas répartis de la même façon dans cet intervalle. Regardez ce que ça donne avec 4 bits pour vous en convaincre :

Image utilisateur
Désavantages

Cette méthode est relativement simple, mais pose exactement les mêmes problèmes que la représentation signe-magnitude. Le zéro est toujours représenté par deux nombres différents : un nombre ne contenant que des 0 (0000 0000 ...), et un nombre ne contenant que des 1 (1111 1111 ...). Pour la complexité des circuits, la situation est un peu meilleure qu'avec la représentation en signe-valeur absolue. Mais les circuits manipulant des nombres en complément à un doivent gérer correctement la présence de deux zéros, ce qui ajoute un peu de complexité inutilement. Il faut avouer que ces problèmes méritent bien une solution !

Pour faciliter la vie des concepteurs de circuits ou des programmeurs, on préfère utiliser une autre représentation des nombres entiers, différente du complément à 1 et de la représentation signe-valeur absolue, qui permet de faire des calculs simplement, sans avoir à utiliser de circuits complexes, et avec laquelle le zéro ne pose pas de problèmes.

Complément à deux

Pour éviter ces problèmes avec le zéro et les opérations arithmétiques, on a dû recourir à une astuce : on ne va utiliser que des entiers non-signés et se débrouiller avec çà. L'idée derrière la méthode qui va suivre est de coder un nombre entier négatif par un nombre positif non-signé en binaire, de façon à ce que les résultats des calculs effectués avec ce nombre positif non-signé soient identiques avec ceux qui auraient étés faits avec notre nombre négatif. Par contre, pour les nombres positifs, rien ne change au niveau de leur représentation en binaire.

Pour cela, on va utiliser les règles de l'arithmétique modulaire. Si vous ne savez pas ce que c'est, ce n'est pas grave ! Il vous faudra juste admettre une chose : nos calculs seront faits sur des entiers ayant un nombre de bits fixé une fois pour toute. En clair, si un résultat dépasse ce nombre de bits fixé (qu'on notera N), on ne gardera que les N bits de poids faible (les N bits les plus à droite).

Prenons un exemple : prenons des nombres entiers non-signés de 4 bits. Ceux-ci peuvent donc prendre toutes les valeurs entre 0 et 15. Prenons par exemple 13 et 3. 13 + 3 = 16, comme vous le savez. Maintenant, regardons ce que donne cette opération en binaire.

1101 + 0011 = 10000.

Ce résultat dépasse 4, qui est le nombre de bits fixé. On doit donc garder uniquement les 4 bits de poids faible et on va virer les autres.
Et voici le résultat :

1101 + 0011 = 0000.

En clair, avec ce genre d'arithmétique, 13 + 3 = 0 ! On peut aussi reformuler en disant que 13 = -3, ou encore que 3 = -13.

Et ne croyez pas que ça marche uniquement dans cet exemple : cela se généralise assez rapidement. Pire : ce qui marche pour l'addition marche aussi pour les autres opérations, tel la soustraction ou la multiplication. Un nombre négatif va donc être représenté par un entier positif strictement équivalent dans nos calculs qu'on appelle son complément à deux.

Capacité

En utilisant n bits, un nombre représenté en complément à deux peut prendre toute valeur comprise entre - \frac { 2^{n} } {2} et \frac { 2^{n} } {2} - 1 : cette fois, l'intervalle n'est pas symétrique. Au passage, avec la méthode du complément à deux, le zéro n'est codé que par un seul nombre binaire.

Exemple avec des nombres codés sur 4 bits

Image utilisateur

Au fait : je ne sais pas si vous avez remarqué, mais le bit de poids fort (le bit le plus à gauche) vaut 1 si le nombre est négatif, et 0 si le nombre représenté est positif. Celui-ci se comporte comme un bit de signe.

Conversion entier -> binaire

Ça a l'air joli, mais comment je fais pour trouver quel est l'entier positif qui correspond à -15, ou à -50 ? Il faut bien que çà serve ton truc, non ?

Ce complément à deux se calcule en plusieurs étapes :

Pas convaincu ? alors on va prendre un exemple : 7 + (-6). On suppose que ces nombres sont codés sur quatre bits.
Pour 7, pas de changements, ça reste 0111. Pour coder -6, on va :

Ensuite, il nous faut faire l'addition : 0111 + 1010 = 10001.

Et là, on prend en compte le fait que nos deux nombres de base sont codés sur 4 bits ! On ne doit garder que les 4 derniers bits de notre résultat. Le résultat de 0111 + 1010 = 10001, une fois tronqué sur 4 bits, donnera alors 0001. On trouve bien le bon résultat.

Sign Extend

Dans nos ordinateurs, tous les nombres sont représenté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 l'ordinateur. Si l'on veut utiliser un entier codé sur 16 bits et que l'ordinateur ne peut manipuler que des nombres de 32 bits, il faut bien trouver un moyen de convertir notre nombre de 16 bits en un nombre de 32 bits, sans changer sa valeur et en conservant son signe. Cette conversion d'un entier en un entier plus grand, qui conserve valeur et signe s'appelle l'extension de signe, ou sign extend.

L'extension de signe des nombres positif ne pose aucun problème : il suffit de remplir les bits à gauche de notre nombre de base avec des 0 jusqu’à arriver à la taille voulue. C'est la même chose qu'en décimal : rajouter des zéros à gauche d'un nombre ne changera pas sa valeur. Cela marche quelque soit la représentation utilisée, que ce soit la représentation signe-valeur absolue, le complément à 1 ou complément à 2.

Exemple, si je veux convertir l'entier positif 0100 0101, prenant 8 bits, en l'entier équivalent mais utilisant 16 bits, il me suffit de remplir les 8 bits à gauche de 0100 0101 par des 0. On obtient ainsi 0000 0000 0100 0101.

Pour les nombres négatifs, la conversion dépend de la représentation utilisée. Avec le complément à 2, l'extension de signe d'un entier négatif est simple à effectuer : il suffit de remplir les bits à gauche du nombre à convertir avec des 1, jusqu'à obtenir le bon nombre de bits.

Exemple, prenons le nombre -128, codé sur 8 bits en complément à deux : 1000 0000. On veut le convertir en nombre sur 16 bits. Il suffit pour cela de remplir les 8 bits de poids fort (les 8bits les plus à gauche) de 1 : on obtient 1111 1111 1000 000.

L'extension de signe d'un nombre codé en complément à 2 se résume donc en une phrase.

Pour un nombre codé en complément à deux, il suffit de recopier le bit de poids fort de notre nombre à convertir à gauche de celui-ci jusqu’à atteindre le nombre de bits voulu.


Un ordinateur, c'est très bête : ça ne sait pas compter jusqu'à deux ! Nombres à virgule

Nombres à virgule

Nombres entiers Codage du texte

Nombres à virgule

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'utiliser des nombres à virgule. Notre ordinateur n'est pas en reste : il est lui aussi capable de manipuler des nombres à virgule sans trop de problèmes (même si de ce point de vue, certains ordinateurs se débrouillent mieux que d'autres). Notre ordinateur va parfaitement pouvoir manipuler des nombres virgule.

Il existe deux méthodes pour coder des nombres à virgule en binaire :

La méthode de la virgule fixe consiste à émuler nos nombres à virgule à partir de nombre entiers. Un nombre à virgule fixe est donc codé par un nombre entier proportionnel à notre nombre à virgule fixe. Pour obtenir la valeur de notre nombre à virgule fixe, il suffit de diviser l'entier servant à le représenter par un nombre constant, fixé une bonne fois pour toute.

Par exemple, pour coder 1,23 en virgule fixe, on peut choisir comme "facteur de conversion" 1000. L'entier permettant de coder 1,23 sera alors 1230. La représentation en virgule fixe était utile du temps où les ordinateurs n'intégraient pas de circuits capables de travailler directement sur des nombres à virgule flottante. Cette méthode n'est presque plus utilisée, et vous pouvez l'oublier sans problème.

Les nombres à virgule fixe ont aujourd'hui étés remplacés par les nombres à virgule flottante. Ce sont des nombres dont le nombre de chiffre après la virgule est variable. De nombreuses méthodes existent pour représenter ces nombres à virgule qui sont souvent incompatibles entre-elles.

Les concepteurs de matériel électronique se sont dit qu'il fallait normaliser le stockage des flottants en mémoire ainsi que les résultats des calculs afin que tous les ordinateurs supportent les mêmes flottants et pour que les calculs flottants donnent les mêmes résultats quelque soit l'ordinateur. C'est ainsi qu'est née la norme IEEE754.

Cette norme IEEE754 impose diverses choses concernant nos flottants. Elle impose une façon d'organiser les bits de nos nombres flottants en mémoire, standardisée par la norme. Il faut tout de même noter qu'il existe d'autres normes de nombres flottants, moins utilisées.

Écriture scientifique

L'écriture d'un nombre flottant en binaire est basée sur son écriture scientifique. Cela permet de coder beaucoup plus de valeurs qu'un nombre en virgule fixe, à nombre de bits égal. 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. Ainsi, un nombre x aura une écriture scientifique en base 10 de la forme :

a imes 10^{Exposant}

Notre nombre a ne possède qu'un seul chiffre à gauche de la virgule : on peut toujours trouver un exposant tel que ce soit le cas. En clair, en base 10, sa valeur est comprise entre 1 (inclus) et 10 (exclu).

En binaire, c'est à peu près la même chose, mais avec une puissance de deux. L'écriture scientifique binaire d'un nombre consiste à écrire celui-ci sous la forme

a imes 2^{exposant}

Le nombre a ne possède toujours qu'un seul chiffre à gauche de la virgule, comme en base 10. Le seul truc, c'est qu'en binaire, seuls deux chiffres sont possibles : 0 et 1. Le chiffre de a situé à gauche de la virgule est donc soit un zéro ou un 1.

Pour stocker cette écriture scientifique avec des zéros et des un, il nous faut stocker la partie fractionnaire de notre nombre a, qu'on appelle la mantisse et l'exposant. On rajoute souvent un bit de signe qui sert à calculer le signe du nombre flottant : ce bit vaut 1 si ce nombre est négatif et vaut 0 si notre flottant est positif.

Bit de signe

Exposant

Mantisse

0

0011 0001

111 0000 1101 1001

Mantisse

Mais parlons un peu de cette mantisse. Vous croyez surement que l'ensemble de cette mantisse est stockée dans notre nombre flottant. Et bien rien n'est plus faux : seule la partie fractionnaire est stockée dans nos nombres flottants : le chiffre situé à gauche de la virgule n'est pas stocké dans la mantisse. Ce bit est stocké dans notre nombre flottant de façon implicite et peut se déduire en fonction de l'exposant : on ne doit pas le stocker dans notre nombre flottant, ce qui permet d'économiser un bit. Il est souvent appelé le bit implicite dans certains livres ou certaines documentations. Dans la majorité des cas, il vaut 1, et ne vaut 0 que dans quelques rares exceptions : les flottants dénormaux. On verra ceux-ci plus tard.

Exposant

Après avoir stocké notre mantisse, parlons de l'exposant. Sachez que celui-ci peut être aussi bien positif que négatif : c'est pour permettre de coder des nombres très petits. Mais notre exposant n'est pas codé avec les représentations de nombres entiers qu'on a vues au-dessus. A la place, notre exposant est stocké en lui soustrayant un décalage prédéterminé. Pour un nombre flottant de n bits, ce décalage vaut 2^{n-1}-1.

Formats de flottants

La norme IEEE754 impose diverses choses concernant la façon dont on gère nos flottants. Elle impose un certain format en mémoire : les flottants doivent être stockés dans la mémoire d'une certaine façon, standardisée par la norme. Elle impose une façon d'organiser les bits de nos nombres flottants en mémoire. Cette norme va (entre autres) définir quatre types de flottants différents. Chacun de ces types de flottants pourra stocker plus ou moins de valeurs différentes.
Voici ces types de flottants :

Format

Nombre de bits utilisés pour coder un flottant

Nombre de bits de l'exposant

Nombre de bits pour la mantisse

Simple précision

32

8

23

Simple précision étendue

Au moins 43

Variable

Variable

Double précision

64

11

52

Double précision étendue

80 ou plus

15 ou plus

64 ou plus

IEEE754 impose aussi le support de certains nombres flottants spéciaux. Parmi eux, on trouve l'infini (aussi bien en négatif qu'en positif), la valeur NaN, utilisée pour signaler des erreurs ou des calculs n'ayant pas de sens mathématiquement, ou des nombres spéciaux nommés les dénormaux qui représentent des valeurs très petites et qui sont utilisés dans des scénarios de calcul assez particuliers.

Flottants dénormalisés

Commençons notre revue des flottants spéciaux par les dénormaux, aussi appelés flottants dénormalisés. Pour ces flottants, l'exposant prend la plus petite valeur possible. Ces flottants ont une particularité : le bit implicite attaché à leur mantisse vaut 0.

Bit de signe

Exposant

Mantisse

1 ou 0

Le plus petit exposant possible

Mantisse différente de zéro

Le zéro

Le zéro est un flottant dénormalisé spécial. Sa seule particularité est que sa mantisse est nulle.

Bit de signe

Exposant

Mantisse

1 ou 0

Le plus petit exposant possible

0

Au fait, remarquez que le zéro est codé deux fois à cause du bit de signe. Si vous mettez l'exposant et la mantisse à la bonne valeur de façon à avoir zéro, le bit de signe pourra valoir aussi bien 1 que 0 : on se retrouve avec un -0 et un +0.

Amusons-nous avec l'infini !

Plus haut, j'ai dit que les calculs sur les flottants pouvaient poser quelques problèmes. Essayez de calculer \frac {5} {0} par exemple. Si vous vous dites que votre ordinateur ne pourra pas faire ce calcul, c'est raté cher lecteur ! :p Le résultat sera un flottant spécial qui vaut + \infty. Passons sous le tapis la rigueur mathématique de ce résultat, c'est comme ça. :diable:

+ \infty est codé de la façon suivante :

Bit de signe

Exposant

Mantisse

0

Valeur maximale possible de l'exposant

0

Il faut savoir qu'il existe aussi un flottant qui vaut -\infty. Celui-ci est identique au flottant codant + \infty à part son bit de signe qui est égal à 1.

Bit de signe

Exposant

Mantisse

1

Valeur maximale possible de l'exposant

0

Et le pire, c'est qu'on peut effectuer des calculs sur ces flottants infinis. Mais cela a peu d'utilité.
On peut donner comme exemple :

NaN

Mais malheureusement, l'invention des flottants infinis n'a pas réglé tous les problèmes. On se retrouve encore une fois avec des problèmes de calculs avec ces infinis.
Par exemple, quel est le résultat de \infty - \infty ? Et pour \frac {\infty} {-\infty} ? Ou encore \frac {0} {0}?

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 dans notre ordinateur sans lui faire prendre feu, 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.
Voici comment celui-ci est codé :

Bit de signe

Exposant

Mantisse

1 ou 0, c'est au choix

Valeur maximale possible de l'exposant

Différent de zéro

NAN est l'abréviation de Not A Number, ce qui signifie : n'est pas un nombre. 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.

Exceptions et arrondis

La norme impose aussi une gestion de certains cas particuliers. Ces cas particuliers correspondant à des erreurs, auxquelles il faut bien "répondre". Cette réponse peut être un arrêt de l’exécution du programme fautif, ou un traitement particulier (un arrondi par exemple).
En voici la liste :

Pour donner un exemple avec l'exception Inexact, on va prendre le nombre 0.1. Ce nombre ne semble pourtant pas méchant, mais c'est parce qu'il est écrit en décimal. En binaire, ce nombre s'écrit comme ceci : 0 \hspace{1mm} . \hspace{1mm} 000 \hspace{1mm} 1100 \hspace{1mm} 1100 \hspace{1mm} 1100 \hspace{1mm} 1100 \hspace{1mm} 1100 \hspace{1mm} 1100... et ainsi de suite jusqu’à l'infini. Notre nombre utilise une infinité de décimales. Bien évidement, 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 :


Nombres entiers Codage du texte

Codage du texte

Nombres à virgule Nos bits prennent la porte !

Codage du texte

Nous savons donc comment faire pour représenter des nombres dans notre ordinateur, et c'est déjà un bon début. Mais votre ordinateur peut parfaitement manipuler autre chose que des nombres. Il peut aussi manipuler des images, du son, ou pleins d'autres choses encore. Eh bien sachez que tout cela est stocké dans votre ordinateur... sous la forme de nombres codés en binaire, avec uniquement des 0 et des 1.

Le codage définit la façon de représenter une information (du texte, de la vidéo, du son...) avec des nombres. Ce codage va attribuer à un nombre : une lettre, la couleur d'un pixel à l'écran... Ainsi, notre ordinateur sera non seulement capable de manipuler des nombres (et de faire des calculs avec), mais il sera aussi capable de manipuler une information ne représentant pas forcément un nombre pour l'utilisateur.

Bien évidement, l'ordinateur n'a aucun moyen de faire la différence entre un nombre qui code un pixel, un nombre qui code une lettre ou même un nombre. Pour lui, tout n'est que suites de zéro et de uns sans aucune signification : une donnée en binaire ne contient aucune information sur l’information qu'elle code (son "type"), et l'ordinateur n'a aucun moyen de le deviner.

Par exemple, si je vous donne la suite de bits suivante : 1100101 codé sur 7 bits ; vous n'avez aucun moyen de savoir s'il s'agit d'une lettre (la lettre e avec l'encodage ASCII), le nombre 101, ou l'entier -26 codé en complément à 1, ou encore l'entier -25 codé en complément à deux.

Ce qui va faire la différence entre les types c'est la façon dont sera interprétée la donnée : on n'effectuera pas les mêmes traitements sur une suite de bits selon ce qu'elle représente. Par exemple, si on veut afficher un 'e' à l'écran, les manipulations effectuées ne seront pas les mêmes que celles utilisée pour afficher le nombre 101, ou le nombre -25, etc.

Pour la suite, on va prendre l'exemple du texte.

Standard ASCII

Pour stocker un texte, rien de plus simple : il suffit de savoir comment stocker une lettre dans notre ordinateur et le reste coule de source. On va donc devoir coder chaque lettre et lui attribuer un nombre. Pour cela, il existe un standard, nommée la table ASCII qui va associer un nombre particulier à chaque lettre. L'ASCII est un standard qui permet à tous les ordinateurs de coder leurs caractères de la même façon. Ce standard ASCII utilise des nombres codés sur 7bits, et peut donc coder 128 symboles différents.

Notre table ASCII est donc une table de correspondance qui attribue un nombre à chaque symbole. La voici dans son intégralité, rien que pour vous.

Image utilisateur

Si vous lisez en entier la table ASCII, vous remarquerez sûrement qu'il n'y a pas que des lettres codées par l'ASCII : il y tous les caractères d'un clavier qui sont inscrits dans cette table.

On peut faire quelques remarques sur cette table ASCII :

Ces symboles présents dans ce standard ASCII ne peuvent même pas être tapés au clavier et ils ne sont pas affichables !

Mais à quoi peuvent-ils bien servir ?

Il faut savoir que ce standard est assez ancien. A l'époque de la création de ce standard, il existait de nombreuses imprimantes et autres systèmes qui l'utilisaient Et 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.

Unicode

Le problème avec la table ASCII, c'est qu'on se retrouve assez rapidement limité avec nos 128 symboles. On n'arrive pas à caser les accents ou certains symboles particuliers à certaines langues dedans. 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 genre de manque, de nombreuses autres méthodes sont apparues qui peuvent coder bien plus de symboles que la table ASCII. Elles utilisent donc plus de 7 bits pour coder leurs symboles : on peut notamment citer l'unicode. Pour plus de simplicité, 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.

Si vous voulez en savoir plus sur ces encodages, sachez qu'il existe un tutoriel sur le sujet, sur le siteduzéro. Le voici : Comprendre les encodages.


Nombres à virgule Nos bits prennent la porte !

Nos bits prennent la porte !

Codage du texte Codage NRZ

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

Codage NRZ

Nos bits prennent la porte ! Transistors

Codage NRZ

Pour compter en binaire , il faut travailler avec des bits qui peuvent prendre deux valeurs notées 0 et 1. Le tout est de savoir comment représenter ces bits dans l'ordinateur. Pour cela, on utilise une grandeur physique nommée la tension. Pas besoin de savoir ce que c'est, sachez juste que ça se mesure en volts et que ça n'est pas synonyme de courant électrique. :-° Rien à voir avec un quelconque déplacement d’électrons, comme certains le pensent.

Avec cette tension, il y a diverses méthodes pour coder un bit : codage Manchester, NRZ, etc. Ces diverses méthodes ont chacune leurs avantages et leurs défauts. Autant trancher dans le vif tout de suite : la quasi-intégralité des circuits de notre ordinateur se basent sur le codage NRZ.

Codage NRZ

Pour coder un 0 ou 1 en NRZ, si suffit de dire que si la tension est en-dessous d'un seuil donné, C'est un 0. Et il existe un autre seuil au-dessus duquel la tension représente un 1. Du moins, c'est ainsi dans la majorité des cas : il arrive que ce soit l'inverse sur certains circuits élèctroniques : en-dessous d'un certain seuil, c'est un 1 et si c'est au-dessus d'un autre seuil c'est 0. 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.

Image utilisateur

Il y a deux seuils, car les circuits qui manipulent des tensions n'ont pas une précision parfaite, et qu'une petite perturbation électrique pourrait alors transformer un 0 en 1. Pour limiter la casse, on préfère séparer ces deux seuils par une sorte de marge de sécurité.

Tensions de référence

Ces tensions vont être manipulées par différents circuits électroniques plus ou moins sophistiqués. Pour pouvoir travailler avec des tensions, nos circuits ont besoin d'être alimentés en énergie. Pour cela, notre circuit possédera une tension qui alimentera le circuit en énergie, qui s'appelle la tension d'alimentation. Après tout, si un circuit doit coder des bits valant 1, il faudra bien qu'il trouve de quoi fournir une tension de 2, 3, 5 volts : la tension codant notre 1 ne sort pas de nulle part ! 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 tous les circuits électroniques (et pas seulement les ordinateurs), cette 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. Et 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 court 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.


Nos bits prennent la porte ! Transistors

Transistors

Codage NRZ Portes logiques

Transistors

Pour commencer, nous allons devoir faire une petite digression et parler un peu d’électronique : sans cela, impossible de vous expliquer en quoi est fait un ordinateur ! Sachez tout d'abord que nos ordinateurs sont fabriqués avec des composants électroniques que l'on appelle des transistors, reliés pour former des circuits plus ou moins compliqués. Presque tous les composants d'un ordinateur sont fabriqués avec un grand nombre de transistors, qui peut monter à quelques milliards sur des composants sophistiqués. Pour donner un exemple, sachez que les derniers modèles de processeurs peuvent utiliser près d'un milliard de transistors. Et le tout doit tenir sur quelques centimètres carrés : autant vous dire que la miniaturisation a fait d’énormes progrès !

Transistors CMOS

Il existe différents types de transistors, chacun avec ses particularités, ses avantages et ses inconvénients. On ne va pas en parler plus que ça, mais il faut préciser que les transistors utilisés dans nos ordinateurs sont des transistors à effet de champ à technologie CMOS. Si vous ne comprenez pas ce que ça signifie, ce n'est pas grave, c'est un simple détail sans grande importance.

Mais qu'est-ce qu'un transistor CMOS ?

Il s'agit simplement d'un composant relié à un circuit électronique par trois morceaux de "fil" conducteur que l'on appelle broches. On peut appliquer de force une tension électrique sur ces broches (attention à ne pas la confondre avec le courant électrique), qui peut représenter soit 0 soit 1 en fonction du transistor utilisé.

Image utilisateur

Ces trois broches ont des utilités différentes et on leur a donné un nom pour mieux les repérer :

Dans les processeurs, on utilise notre transistor comme un interrupteur qui réagit en fonction de sa grille : suivant la valeur de la tension qui est appliquée sur la grille, le transistor conduira ou ne conduira pas le courant entre la source et le drain. En clair, appliquez la tension adéquate et la liaison entre la source et le drain se comportera comme un interrupteur fermé et conduira le courant : le transistor sera alors dit dans l'état passant. Par contre, si vous appliquez une tension à la bonne valeur sur la grille, cette liaison se comportera comme un interrupteur ouvert et le courant ne passera pas : le transistor sera dit dans l'état bloqué.

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

Loi de Moore

De nos jours, le nombre de transistors des composants électroniques actuels augmente de plus en plus, et les concepteurs de circuits rivalisent d'ingéniosité pour miniaturiser le tout.

En 1965, le cofondateur de la société Intel, spécialisée dans la conception des mémoires et de processeurs, a affirmé que la quantité de transistors présents dans un processeur doublait tous les 18 mois. Cette affirmation porte aujourd'hui le nom de première loi de Moore. En 1975, le cofondateur d'Intel réévalua cette affirmation : ce n'est pas tous les 18 mois que le nombre de transistors d'un processeur double, mais tous les 24 mois. Cette nouvelle version, appelée la seconde loi de Moore, a redoutablement bien survécue : elle est toujours valable de nos jours.

Image utilisateur

Ce faisant, la complexité des processeurs augmente de façon exponentielle dans le temps et sont censés devenir de plus en plus gourmands en transistors au fil du temps.

De plus, miniaturiser les transistors permet parfois de les rendre plus rapides : c'est un scientifique du nom de Robert Dennard qui a découvert un moyen de rendre un transistor plus rapide en diminuant certains paramètres physiques d'un transistor. Sans cette miniaturisation, vous pouvez être certains que nos processeurs en seraient pas aussi complexes qu’aujourd’hui. Mais attention, cela ne signifie pas pour autant que le nombre de transistors soit un indicateur efficace de performances : avoir beaucoup de transistors ne sert à rien si on le les utilise pas correctement.

Mais cette miniaturisation a ses limites et elle pose de nombreux problèmes dont on ne parlera pas ici. Sachez seulement que cette loi de Moore restera valable encore quelques dizaines d'années, et qu'au delà, on ne pourra plus rajouter de transistors dans nos processeurs aussi facilement que de nos jours.


Codage NRZ Portes logiques

Portes logiques

Transistors Créons nos circuits !

Portes logiques

C'est bien beau de savoir coder des bits et d'avoir des transistors pour les manipuler, mais j'aimerais savoir comment on fait pour triturer des bits avec des transistors ?

Et bien que vos vœux soient exhaussés ! La solution consiste à rassembler ces transistors dans ce qu'on appelle des circuits logiques.

Ce sont simplement des petits circuits, fabriqués avec des transistors, qui possèdent des sorties et des entrées, sur lesquelles on va placer des bits pour les manipuler. Ces entrées et ces sorties ne sont rien d'autre que des morceaux de "fil" conducteur sur lesquelles on peut mesurer une tension qui représente un zéro ou un 1. Sur chaque entrée du composant, on peut forcer la valeur de la tension, histoire de mettre l'entrée à 0 ou à 1. A partir de là, le circuit électronique va réagir et déduire la tension à placer sur chacune de ses sorties en fonction de ses entrées.

Autant vous le dire tout de suite, votre ordinateur est remplit de ce genre de choses. Quasiment tous les composants de notre ordinateur sont fabriqués avec ce genre de circuits. Par exemple, notre processeur est un composant électronique comme un autre, avec ses entrées et ses sorties.

Image utilisateur

Brochage d'un processeur MC68000.

L'exemple montré au dessus est un processeur MC68000, un vieux processeur, présent dans les calculatrices TI-89 et TI-92, qui contient 68000 transistors (d'où son nom : MC68000) et inventé en 1979. Il s'agit d'un vieux processeur complètement obsolète et particulièrement simple. Et pourtant, il y en a des entrées et des sorties : 37 au total ! Pour comparer, sachez que les processeurs actuels utilisent entre 700 et 1300 broches d'entrée et de sortie. A ce jeu là, notre pauvre petit MC68000 passe pour un gringalet !

Néanmoins, quelque soit la complexité du circuit à créer, celui-ci peut être construit en reliant quelques petits circuits de base entre eux. Ces circuits de base sont nommés des portes logiques. Il existe trois portes logiques qui sont très importantes et que vous devez connaitre : les portes ETET, OU et NON. Mais pour se faciliter la vie, on peut utiliser d'autres portes, plus ou moins différentes. Voyons un peu quelles sont ces portes, et ce qu'elles font.

La porte NON

Le premier opérateur fondamental est la porte NON aussi appelée porte inverseuse. Cette porte agit sur un seul bit.

Elle est symbolisée par le schéma suivant :

Image utilisateur

Pour simplifier la compréhension, je vais rassembler les états de sortie en fonction des entrées pour chaque porte logique dans un tableau qu'on appelle table de vérité. Voici celui de la porte NON :

Entrée

Sortie

0

1

1

0

Le résultat est très simple, la sortie d'une porte NON est exactement le contraire de l'entrée.

Câblage

Cette porte est fabriquée avec seulement deux transistors et son schéma est diablement simple. Voici le montage en question.

Image utilisateur

Je crois que çà mérite une petite explication, non ?

Rappelez-vous qu'un transistor CMOS n'est rien d'autre qu'un interrupteur, qu'on peut fermer suivant ce qu'on met sur sa grille. Certains transistors se ferment quand on place un 1 sur la grille, et d'autres quand on place un zéro.

L'astuce du montage vu plus haut consiste à utiliser deux transistors différents :

Si on met un 1 en entrée de ce petit montage électronique, le transistor du haut va fonctionner comme un interrupteur ouvert, et celui du bas comme un interrupteur fermé. On se retrouvera donc avec notre sortie reliée au zéro volt, et donc qui vaut zéro.

Image utilisateur

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é. On se retrouvera donc avec notre sortie reliée à la tension d'alimentation, qui vaudra donc 1.

Image utilisateur

Comme vous le voyez, avec un petit nombre de transistors, on peur réussir à créer de quoi inverser un bit. Et on peut faire pareil avec toutes les autres portes élémentaires : on prend quelques transistors, on câble cela comme il faut, et voilà une porte logique toute neuve !

La porte ET

Maintenant une autre porte fondamentale : la porte ET.

Cette fois, différence avec la porte NON, la porte ET a 2 entrées, mais une seule sortie.

Voici comment on la symbolise :

Image utilisateur

Cette porte a comme table de vérité :

Entrée 1

Entrée 2

Sortie

0

0

0

0

1

0

1

0

0

1

1

1

Cette porte logique met sa sortie à 1 quand toutes ses entrées valent 1.

Porte NAND

La porte NAND est l'exact inverse de la sortie d'une porte ET. Elle fait la même chose qu'une porte ET suivie d'une porte NON.

Sa table de vérité est :

Entrée 1

Entrée 2

Sortie

0

0

1

0

1

1

1

0

1

1

1

0

Cette porte a une particularité : on peut recréer les portes ET, OU et NON, et donc n'importe quel circuit électronique, en utilisant des montages composés uniquement de portes NAND. A titre d'exercice, vous pouvez essayez de recréer les portes ET, OU et NON à partir de portes NAND. Ce serait un petit entrainement assez sympathique. Après tout, si ça peut vous occuper lors d'un dimanche pluvieux. :p

On la symbolise par le schéma qui suit.

Image utilisateur
Câblage

Implémenter une porte NAND avec des transistors CMOS est un peu plus complexe qu'implémenter une porte NON. Mais qu'à cela ne tienne, voici en exclusivité : comment créer une porte NAND avec des transistors CMOS !

Image utilisateur

Ce schéma peut s'expliquer très simplement. Tout d'abord, vous verrez qu'il y a deux grands blocs de transistors dans ce circuit : un entre la sortie et la tension d’alimentation, et un autre entre la sortie et la masse. Tous les circuits CMOS suivent ce principe, sans exception. Ensuite, on peut remarquer que tous les transistors placés entre la tension d'alimentation et la sortie sont des transistors PMOS. De même, tous les transistors placés entre la masse et la sortie sont des transistors NMOS. Ceci est encore une fois vrai pour tous les circuits CMOS.

Regardons ces deux parties l'une après l'autre, en commençant par celle du haut.

Image utilisateur

Celle-ci sert à connecter la sortie sur la tension d'alimentation du circuit. Nos deux transistors sur de type PMOS : ils se ferment quand on leur met un 0 sur la grille. Or, les transistors sont mis en parallèle : si un seul de ces deux transistors est fermé, la tension d'alimentation sera reliée à la sortie et elle passera à 1. Donc, si une seule des deux entrées est à 0, on se retrouve avec un 1 en sortie.

Passons maintenant à l'autre bloc de transistors.

Image utilisateur

Cette fois-ci, c'est l'inverse : nos transistors sont reliés les uns à la suite des autres : il faut que les deux soient fermés pour que la masse soit connectée à la sortie. Et les transistors sont cette fois des transistors NMOS : ils se ferment quand on leur met un 1 sur leur grille. Donc, pour avoir un zéro en sortie, il faut que les deux entrées soient à 1. Au final on obtient bien une porte NAND.

La porte OU

Maintenant une autre porte fondamentale : la porte OU.

Cette fois, comme la porte ET, elle possède 2 entrées, mais une seule sortie.

On symbolise cette porte comme ceci :

Image utilisateur

Cette porte est définie par la table de vérité suivante :

Entrée 1

Entrée 2

Sortie

0

0

0

0

1

1

1

0

1

1

1

1

Cette porte logique met sa sortie à 1 quand au moins une de ses entrées vaut 1.

Porte NOR

La porte NOR est l'exact inverse de la sortie d'une porte OU. Elle est équivalente à une porte OU suivie d'une porte NON.

Sa table de vérité est :

Entrée 1

Entrée 2

Sortie

0

0

1

0

1

0

1

0

0

1

1

0

On peut recréer les portes ET, OU et NON, et donc n'importe quel circuits électronique, en utilisant des montages composés uniquement de portes NOR. Comme quoi, la porte NAND n'est pas la seule à avoir ce privilège. Cela a une conséquence : on peut concevoir un circuits en n'utilisant que des portes 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 uniquement avec des portes NOR.

A titre d'exercice, vous pouvez essayez de recréer les portes ET, OU et NON à partir de portes NOR. Si vous en avez envie, hein ! :-°

On la symbolise avec le schéma qui suit.

Image utilisateur
Câblage

Implémenter une porte NOR avec des transistors CMOS ressemble à ce qu'on a fait pour la prote NAND.

Image utilisateur

Ce schéma peut s'expliquer très simplement. Encore une fois, on va voir chacune des deux parties (celle du haut et celle du bas) l'une après l'autre, en commençant par celle du haut.

Image utilisateur

Celle-ci sert à connecter la sortie sur la tension d'alimentation du circuit. Nos deux transistors sur de type PMOS : ils se ferment quand on leur met un 0 sur la grille. Nos transistors sont reliés les uns à la suite des autres : il faut que les deux soient fermés pour que la masse soit connectée à la sortie. les deux entrées doivent être à zéro pour que l'on ait un 1 en sortie.

Passons maintenant à l'autre bloc de transistors.

Image utilisateur

Les transistors sont des transistors NMOS : ils se ferment quand on leur met un 1 sur leur grille. Cette fois, les transistors sont mis en parallèle : si un seul de ces deux transistors est fermé, la tension d'alimentation sera reliée à la sortie et elle passera à 0. Donc, si une seule des deux entrées est à 1, on se retrouve avec un 1 en sortie. Au final on obtient bien une porte NOR.

Porte XOR

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 table de vérité est :

Entrée 1

Entrée 2

Sortie

0

0

0

0

1

1

1

0

1

1

1

0

On remarque que sa sortie est à 1 quand les deux bits placés sur ses entrées sont différents, et valent 0 sinon.

On la symbolise comme ceci :

Image utilisateur
Porte NXOR

La porte XOR posséde une petite soueur : la NXOR.

Sa table de vérité est :

Entrée 1

Entrée 2

Sortie

0

0

1

0

1

0

1

0

0

1

1

1

On remarque que sa sortie est à 1 quand les deux bits placés sur ses entrées sont différents, et valent 0 sinon. Cette porte est équivalente à une porte XOR suivie d'une porte NON.

On la symbolise comme ceci :

Image utilisateur

Transistors Créons nos circuits !

Créons nos circuits !

Portes logiques Circuits combinatoires

Bon, c'est bien beau d'avoir quelques portes logiques, mais si je veux créer un circuit, je fais comment ?

Il faut avouer qu'on irait pas loin en sachant uniquement ce que sont les ET, NAND, et autres. Ce qu'il faudrait, c'est pouvoir créer de vrais circuits. Et bien que vos vœux soient exaucés (enfin presque) : nous allons enfin voir comment sont réalisés les circuits de nos ordinateurs. Du moins, nous allons voir comment créer des circuits simples, mais qui sont à la base des circuits de notre ordinateur.

Circuits combinatoires

Créons nos circuits ! Circuits séquentiels

Circuits combinatoires

Pour commencer, nous allons parler d'une classe de circuits assez simples : les circuits combinatoires. Ces circuits font comme tous les autres circuits : ils prennent des données sur leurs entrées, et fournissent un résultat en sortie. Le truc, c'est que ce qui est fourni en sortie ne dépend que du résultat sur les entrées, et de rien d'autre ! Cela peut sembler être évident, mais on verra que ce n'est pas le cas pour tous les circuits.

Pour donner quelques exemples de circuits combinatoires, on peut citer les circuits qui effectuent des additions, des multiplications, ou d'autres opérations arithmétiques du genre. Par exemple, le résultat d'une addition ne dépend que des nombres à additionner et rien d'autre. Pareil pour la division, la soustraction, la multiplication, etc. Notre ordinateur contient de nombreux circuits de ce genre. Toutefois, nous ne verrons pas tout de suite les circuits capables d'effectuer ces calculs : ceux-ci sont un peu plus compliqués que ce qu'on va voir ici et on va donc les laisser pour plus tard, dans la partie sur le processeur.

Tables de vérité

Bref, poursuivons. J'ai promis de vous apprendre à concevoir des circuits, de façon "simple". Pour commencer, il va falloir décrire ce que notre circuit fait. Pour un circuit combinatoire, la tâche est très simple, vu que ce qu'on trouve sur ses sorties ne dépend que de ce qu'on a sur les entrées. Pour décrire intégralement le comportement de notre circuit, il suffit donc de lister la valeur de chaque sortie pour toute valeur possible en entrée. Cela peut se faire simplement en écrivant ce qu'on appelle la table de vérité du circuit. Pour créer cette table de vérité, il faut commencer par lister toutes les valeurs possibles des entrées dans un tableau, et écrire à coté les valeurs des sorties qui correspondent à ces entrées. Cela peut être assez long : pour un circuit ayant n entrées, ce tableau aura 2^n lignes.

Bit de parité

Pour donner un exemple, on va prendre l'exemple d'un circuit calculant la le bit de parité d'un nombre.

Le quoi ? :euh:

Ah oui, pardon !

Ce bit de parité est une technique qui permet de détecter des erreurs de transmission ou d’éventuelles corruptions de données qui modifient un nombre impair de bits. Si un, trois, cinq, ou un nombre impair de bits voient leur valeur s'inverser (un 1 devient un 0, ou inversement), l'utilisation d'un bit de parité permettra de détecter cette erreur. Par contre, il sera impossible de la corriger.

Le principe caché derrière un bit de parité est simple : il suffit d'ajouter un bit supplémentaire aux bits à stocker. Le but d'un bit de parité est de faire en sorte que le nombre de bits à 1 dans le nombre à stocker, bit de parité inclut, soit toujours un nombre pair.
Ce bit, le bit de parité vaudra :

Détecter une erreur est simple : on compte le nombre de bits à 1 dans le nombre à stocker, bit de parité inclut, et on regarde s'il est pair. S'il est impair, on sait qu'au moins un bit à été modifié.

Table de vérité du circuit

Dans notre cas, on va créer un circuit qui calcule le bit de parité d'un nombre de 3 bits. Celui-ci dispose donc de 3 entrées, et d'une sortie sur laquelle on retrouvera notre bit de parité. Notre tableau possédera donc 2^3 lignes : cela fait 8 lignes. Voici donc le tableau de ce circuit, réalisé ci-dessous.

Entrée e2

Entrée e1

Entrée e0

Sortie s0

0

0

0

0

0

0

1

1

0

1

0

1

0

1

1

0

1

0

0

1

1

0

1

0

1

1

0

0

1

1

1

1

Équations logiques

Une fois qu'on a la table de vérité, une bonne partie du travail à déjà été fait. Il ne nous reste plus qu'à transformer notre table en ce qu'on appelle des équations logiques.

Mais que viennent faire les équations ici ? o_O

Attention : il ne s'agit pas des équations auxquelles vous êtes habitués. Ces équations logiques ne font que travailler avec des 1 et des 0, et n'effectuent pas d'opérations arithmétiques mais seulement des ET, des OU, et des NON. Ces équations vont ainsi avoir des bits pour inconnues.

Chacune de ces équations logiques correspondra à un circuit, et vice-versa : à un circuit sera associé une équation qui permettra de décrire le circuit. Par exemple, prenons le circuit vu dans le QCM de la question précédente.

Image utilisateur

Ce circuit a pour équation logique ( \overline {a}. b ) + ( a . \overline {b} )

Syntaxe

Pour pouvoir commencer à écrire ces équations, il va falloir faire un petit point de syntaxe. Voici résumé dans ce tableau les différentes opérations, ainsi que leur notation. Dans ce tableau, a et b sont des bits.

Opération logique

Symbole

NON a

\overline{a}

a ET b

a.b

a OU b

a+b

a XOR b

a \oplus b

Voilà, avec ce petit tableau, vous savez comment écrire une équation logique...enfin presque, il ne faut pas oublier le plus important : les parenthèses ! Et oui, il faudra bien éviter quelques ambiguïtés dans nos équations. C'est un peu comme avec des équations normales : ( a imes b ) + c donne un résultat différent de a imes ( b + c ). Avec nos équations logiques, on peut trouver des situations similaires : par exemple, (a . b) + c est différent de a . (b + c). On est alors obligé d'utiliser des parenthèses.

Méthode des Minterms

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 circuits. Et on fera cela pour toutes les sorties du circuit que l'on veut concevoir.

Pour cela, on peut utiliser ce qu'on appelle la méthode des minterms. Cette méthode permet de découper un circuit en quelques étapes simples :

Il ne reste plus qu'à faire cela pour toutes les sorties du circuit, et le tour est joué. Pour illustrer le tout, on va reprendre notre exemple avec le bit de parité.

Première étape

La première étape consiste donc à lister les lignes de la table de vérité dont la sortie est à 1.

Entrée e2

Entrée e1

Entrée e0

Sortie s0

0

0

0

0

0

0

1

1

0

1

0

1

0

1

1

0

1

0

0

1

1

0

1

0

1

1

0

0

1

1

1

1

Deuxième étape

Ensuite, on doit écrire l'équation logique de chacune des lignes sélectionnées à l'étape d'avant.

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

Par exemple, prenons la première ligne dont la sortie vaut 1, à savoir la deuxième.

Entrée e2

Entrée e1

Entrée e0

0

0

1

L'équation logique de cette ligne sera donc : \overline{e2} . \overline{e1} . e0.

Il faut ensuite faire cela pour toutes les lignes dont la sortie vaut 1.

Seconde ligne :

Entrée e2

Entrée e1

Entrée e0

0

1

0

L'équation logique de cette ligne sera donc : \overline{e2} . . e1 \overline{e0}.

Troisième ligne :

Entrée e2

Entrée e1

Entrée e0

1

0

0

L'équation logique de cette ligne sera donc : e2 . \overline{e1} . \overline{e0}.

Quatrième ligne :

Entrée e2

Entrée e1

Entrée e0

1

1

1

L'équation logique de cette ligne sera donc : e2 . e1 . e0.

Troisième étape

On a alors obtenu nos équations logiques. Reste à faire un bon gros OU entre toutes ces équations, et le tour est joué ! On obtient alors l'équation logique suivante : (\overline{e2} . \overline{e1} . e0) + (\overline{e2} . e1 . \overline{e0}) + (e2 . \overline{e1} . \overline{e0}) + (e2 . e1 . e0)

A ce stade, vous pourriez traduire cette équation directement en circuit, mais il y a un petit inconvénient...

Simplifications du circuit

Comme on l'a vu, on fini par obtenir une équation logique qui permet de décrire notre circuit. Mais quelle équation : on se retrouve avec un gros paquet de ET et de OU un peu partout ! Autant dire qu'il serait sympathique de pouvoir simplifier cette équation. Bien sûr, on peut vouloir simplifier cette équation juste pour se simplifier la vie lors de la traduction de cette équation en circuit, mais cela sert aussi à autre chose : cela permet d'obtenir un circuit plus rapide et/ou utilisant moins de portes logiques. Autant vous dire qu'apprendre à simplifier ces équations est quelque chose de crucial, particulièrement si vous voulez concevoir des circuits un tant soit peu rapides.

Pour donner un exemple, sachez que la grosse équation logique obtenue auparavant : (\overline{e2} . \overline{e1} . e0) + (\overline{e2} . e1 . \overline{e0}) + (e2 . \overline{e1} . \overline{e0}) + (e2 . e1 . e0) ; peut se simplifier en : e2 \oplus e1 \oplus e0 avec les règles de simplifications vues au-dessus. Dans cet exemple, on passe donc de 17 portes logiques à seulement 3 !

Pour simplifier notre équation, on peut utiliser certaines propriétés mathématiques simples de ces équations. Ces propriétés forment ce qu'on appelle l’algèbre de Boole, du nom du mathématicien qui les a découvertes/inventées.

Règle

Description

Commutativité

a+b = b+a

a.b = b.a

a \oplus b = b \oplus a

Associativité

(a+b)+c = a+(b+c)

(a.b).c = a.(b.c)

( a \oplus b ) \oplus c = a \oplus ( b \oplus c )

Distributivité

(a+b).c = (c.b)+(c.a)

(a.b)+c = (c+b).(c+a)

Idempotence

a.a = a

a+a=a

Element nul

a.0 = 0

a+1 = 1

Element Neutre

a.1 = a

a+0 = a

Loi de De Morgan

\overline {a} + \overline{b} = \overline{a.b} ;

\overline {a} . \overline{b} = \overline{a+b} .

Complémentarité

\overline{\overline{a}} = a

a + \overline{a} = 1

a . \overline{a} = 0

On peut aussi rajouter que la porte XOR a ses propres règles.

Regle

Description

XOR

a \oplus b = ( \overline{a} . b ) + ( a . \overline{b} )

a \oplus 0 = a

a \oplus 1 = \overline{a}

a \oplus a = 0

a \oplus \overline{a} = 1

En utilisant ces règles algébriques, on peut arriver à simplifier une équation assez rapidement. On peut ainsi factoriser ou développer certaines expressions, comme on le ferait avec une équation normale, afin de simplifier notre équation logique. Le tout est de bien faire ces simplifications en appliquant correctement ces règles. Pour cela, il n'y a pas de recette miracle : vous devez sortir votre cerveau, et réfléchir !

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 ces méthodes, qui sont assez complexes et n'apporteraient rien dans ce tutoriel. Il faut dire que ces méthodes risquent de ne pas vraiment nous servir : elles possèdent quelques défauts qui nous empêchent de créer de très gros circuits avec. Pour le dire franchement, elles sont trop longues à utiliser quand le nombre d'entrée du circuit dépasse 5 ou 6.

Mais

Un des problèmes des approches mentionnées plus haut est qu'elles nécessitent de créer une table de vérité. Et plus on a d'entrées, plus la table devient longue, et cela prend du temps pour la remplir. Cela ne pose aucun problèmes pour créer des circuits de moins de 5 ou 6 variables, mais au-delà, il y a de quoi rendre les armes assez rapidement. Et si vous ne me croyez pas, essayez de remplir la table de vérité d'un circuit qui additionne deux nombres de 32 bits, vous verrez : cela vous donnera une table de vérité de 4 294 967 296 lignes. Je ne sais pas si quelqu'un a déjà essayé de créer une telle table et d'en déduire le circuit correspondant, mais si c'est le cas, j'aurais de sérieuses craintes sur sa santé mentale. Pour compenser, on doit donc ruser.

Pour cela, il n'y a qu'une seule solution : on doit découper notre circuit en circuits plus petits qu'on relie ensemble. Il suffit de continuer ce découpage tant qu'on ne peut pas appliquer les techniques vues plus haut.


Créons nos circuits ! Circuits séquentiels

Circuits séquentiels

Circuits combinatoires Tic, Tac, Tic, Tac : Le signal d'horloge

Circuits séquentiels

Avec le premier chapitre, on sait coder de l’information. Avec le second chapitre et la partie sur les circuits combinatoires, on sait traiter et manipuler de l’information. Il nous manque encore une chose : savoir comment faire pour mémoriser de l'information. Les circuits combinatoires n’ont malheureusement pas cette possibilité et ne peuvent pas stocker de l'information pour l'utiliser quand on en a besoin. La valeur de la sortie de ces circuits ne dépend que de l'entrée, et pas de ce qui s'est passé auparavant : les circuits combinatoires n'ont pas de mémoire. Ils ne peuvent qu'effectuer un traitement sur des données immédiatement disponibles. On n'irait pas loin en se contentant de ce genre de circuits : il serait totalement impossible de créer un ordinateur.

Comment donner de la mémoire à nos circuits ?

Mais rassurez-vous, tout n'est pas perdu ! Il existe des circuits qui possèdent une telle capacité de mémorisation : ce sont les circuits séquentiels. Ces circuits sont donc capables de mémoriser des informations, et peuvent les utiliser pour déterminer quoi mettre sur leurs sorties. L'ensemble de ces informations mémorisées dans notre circuit forme ce qu'on appelle l'état de notre circuit.

Pour mémoriser des informations (un état), notre circuit doit posséder des circuits spéciaux, chacun d'entre eux pouvant stocker un ou plusieurs bits, qu'on appelle des mémoires. On verra dans la suite de ce tutoriel comment les mémoires actuelles font pour stocker des bits : elles peuvent utiliser aussi bien un support magnétique (disques durs), optique (CD-ROM, DVD-ROM, etc), que des transistors (mémoires RAM, FLASH, ROM, etc), etc.

Reste que cet état peut changer au cours du fonctionnement de notre circuit. Rien n’empêche de vouloir modifier les informations mémorisées dans un circuit. On peut faire passer notre circuit séquentiel d'un état à un autre sans trop de problèmes. Ce passage d'un état à un autre s'appelle une transition.

Un circuit séquentiel peut être intégralement décrit par les états qu'il peut prendre, ainsi que par les transitions possibles entre états. Si vous voulez concevoir un circuit séquentiel, tout ce que vous avez à faire est de lister tous les états possibles, et quelles sont les transitions possibles. Pour ce faire, on utilise souvent une représentation graphique, dans laquelle on représente les états possibles du circuit par des cercles, et les transitions possibles par des flèches.

Image utilisateur

La transition effectuée entre deux états dépend souvent de ce qu'on met sur l'entrée du circuit. Aussi bien l'état du circuit (ce qu'il a mémorisé) que les valeurs présentes sur ses entrées, vont déterminer ce qu'on trouve sur la sortie. Par exemple, la valeur présente sur l'entrée peut servir à mettre à jour l'état ou donner un ordre au circuit pour lui dire : change d'état de tel ou tel façon. Dans la suite du tutoriel, vous verrez que certains composants de notre ordinateur fonctionnent sur ce principe : je pense notamment au processeur, qui contient des mémoires internes décrivant son état (des registres), et que l'on fait changer d'état via des instructions fournies en entrée.

Pour rendre possible les transitions, on doit mettre à jour l'état de notre circuit avec un circuit combinatoire qui décide quel sera le nouvel état de notre circuit en fonction de l'ancien état et des valeurs des entrées. Un circuit séquentiel peut donc (sans que ce soit une obligation) être découpé en deux morceaux : une ou plusieurs mémoires qui stockent l'état de notre circuit, et un ou plusieurs circuits combinatoires chargés de mettre à jour l'état du circuit, et éventuellement sa sortie.

Pour la culture générale, il existe principalement deux types de circuits séquentiels :

Automates de Moore

Avec les automates de Moore, ce qu'on trouve en sortie ne dépend que de l'état de l'automate. On peut donc simplement placer un circuit combinatoire qui se chargera de lire l'état de l'automate et qui fournira un résultat sur la sortie directement.

Pour mettre à jour l'état, on place un circuit combinatoire qui va prendre les entrées du circuit, ainsi que l'état actuel du circuit (fourni sur la sortie), et qui déduira le nouvel état, les nouvelles données à mémoriser.

Image utilisateur
Automates de Mealy

Autre forme de circuits séquentiels : les automates de Mealy. Avec ceux-ci, la sortie dépend non seulement de l'état du circuit, mais aussi de ce qu'on trouve sur les entrées.

Image utilisateur

Ces automates ont tendance à utiliser moins de portes logiques que les automates de Moore.

Bascules

On a vu plus haut que la logique séquentielle se base sur des circuits combinatoires, auxquels on a ajouté 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 : avec nos portes logiques, on peut créer des circuits capables de mémoriser un bit. Ces circuits sont ce qu'on appelle des bascules.

En assemblant plusieurs de ces bascules ensembles, on peut créer ce qu'on appelle des registres, des espèces de mémoires assez rapides qu'on retrouve un peu partout dans nos ordinateurs : presque tous les circuits présents dans notre ordinateur contiennent des registres, que ce soit le processeur, la mémoire, les périphériques, etc.

Principe

Une solution pour créer une bascule consiste à boucler la sortie d'un circuit sur son entrée, de façon à ce que la sortie rafraîchisse le contenu de l'entrée en permanence et que le tout forme une boucle qui s'auto-entretienne. Une bonne partie des circuits séquentiels contiennent des boucles quelque part, avec une entrée reliée sur une sortie. Ce qui est tout le contraire des circuits combinatoires, qui ne contiennent jamais la moindre boucle !

Bien sur, cela ne marche pas avec tous les circuits : dans certains cas, cela ne marche pas, ou du moins cela ne suffit pas pour mémoriser des informations. Par exemple, si je relie la sortie d'une porte NON à son entrée, le montage obtenu ne sera pas capable de mémoriser quoique ce soit.

Et si on essayait avec deux portes NON ?

Ah, c'est plutôt bien vu !

En effet, en utilisant deux portes NON, et en les reliant comme indiqué sur les schéma juste en dessous, on peut mémoriser un bit.

Image utilisateur

Si on place l'entrée de la première porte NON à zéro, la sortie de celle-ci passera à 1. Cette sortie sera reliée à l'entrée de l'autre porte NON, qui inversera ce 1, donnant un zéro. Zéro qui sera alors ré-envoyé sur l'entrée initiale. L'ensemble sera stable : on peut déconnecter l'entrée du premier inverseur, celle-ci sera alors rafraichie en permanence par l'autre inverseur, avec sa valeur précédente. Le même raisonnement fonctionne si on met un 1 en sortie.

Image utilisateur
Bascule RS à NOR

Le seul problème, c'est qu'il faut bien mettre à jour l'état de ce bit de temps en temps. Il faut donc ruser. Pour mettre à jour l'état de notre circuit, on va simplement rajouter une entrée à notre circuit qui servira à le mettre à jour, et remplacer notre porte NON par une porte logique qui se comportera comme un inverseur dans certaines conditions. Le tout est de trouver une porte logique qui inverse le bit venant de l'autre inverseur si l'autre entrée est à zéro (ou à 1, suivant la bascule). Des portes NOR font très bien l'affaire.

Image utilisateur

On obtient alors ce qu'on appelle des bascules RS. Celles-ci sont des bascules qui comportent deux entrées R et S, et une sortie Q, sur laquelle on peut lire le bit stocké.

Image utilisateur

Le principe de ces bascules est assez simple :

Pour vous rappeler de ceci, 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). Petite remarque : si on met un 1 sur les deux entrées, le circuit ne répond plus de rien. On ne sait pas ce qui arrivera sur ses sorties. C'est bête, mais c'est comme ça !

Entrée Reset

Entrée Set

Sortie Q

0

0

Bit mémorisé par la bascule

0

1

1

1

0

0

1

1

Interdit

Bascules RS à NAND

On peut aussi utiliser des portes NAND pour créer une bascule.

Image utilisateur

En utilisant des portes NAND, le circuit change un peu. Celles-ci sont des bascules qui comportent deux entrées \overline{R} et \overline{S}, et une sortie Q, sur laquelle on peut lire le bit stocké.

Image utilisateur

Ces bascules fonctionnent différemment de la bascule précédente :

Entrée Reset

Entrée Set

Sortie Q

0

0

Interdit

0

1

0

1

0

1

1

1

Bit mémorisé par la bascule

Bascule D

Comme vous le voyez, notre bascule RS est un peu problématique : il y a une combinaison d'entrées pour laquelle on ne sait pas ce que va faire notre circuit. On va devoir résoudre ce léger défaut.

Tout d'abord, il faut remarquer que la configuration problématique survient quand on cherche à mettre R et S à 1 en même temps. Or, le bit R permet de mettre à zéro notre bascule, tandis que le bit S va la mettre à 1. Pas étonnant que cela ne marche pas. Pour résoudre ce problème, il suffit simplement de remarquer que le bit R est censé être l'exact opposé du bit S : quand on veut mettre un bit à 1, on ne le met pas zéro, et réciproquement. Donc, on peut se contenter d'un bit, et ajouter une porte NON pour obtenir l'autre bit.

Dans ce qui suit, on va choisir de garder le bit S. Pour une raison très simple : en faisant cela, placer un 0 sur l'entrée S fera mémoriser un zéro à la bascule, tandis qu'y placer un 1 mémorisera un 1. En clair, l'entrée S contiendra le bit à mémoriser.

Image utilisateur

Mais, il y a un petit problème. Si on regarde la table de vérité de ce nouveau circuit, on s’aperçoit qu'il ne mémorise rien ! Si on place un 1 sur l'entrée R, la bascule sera mise à 1, et si on met un zéro, elle sera mise à zéro. Pour régler ce petit problème, on va rajouter une entrée, qui permettra de dire à notre bascule : ne prend pas en compte ce que tu trouve sur ton entrée S. Cette entrée, on va l'appeler l'entrée de validation d'écriture. Elle servira à autoriser l'écriture dans la bascule.

Reste à savoir quoi rajouter dans notre circuit pour ajouter cette entrée. En réfléchissant bien, on se souvient que notre bascule RS effectuait une mémorisation quand ses bits R et S étaient tous les deux à 0. Ce qu'il faut rajouter, ce sont des portes, reliées à ce qui était autrefois les entrées R et S, reliées à notre nouvelle entrée. Il suffit que ces portes envoient un zéro sur leur sortie quand l'entrée de validation d'écriture est à zéro, et recopie son autre entrée sur sa sortie dans le cas contraire. Ce qu'on vient de décrire est exactement le fonctionnement d'une porte ET. On obtient alors le circuit suivant.

Image utilisateur

On peut aussi faire la même chose, mais avec la bascule RS à NAND.

Image utilisateur

Ce qu'on vient de fabriquer s'appelle une bascule D.

Image utilisateur
Mémoires

A partir de ces petites mémoires de 1 bit, on peut créer des mémoires un peu plus conséquentes. Grâce à cela, on saura maintenant créer des circuits séquentiels ! Pour commencer, il faut remarquer que la mémoire d'un circuit séquentiel forme un tout : on ne peut pas en modifier un morceau : lors d'une transition, c'est toute la mémoire de l'automate qui est modifié. Donc, on doit faire en sorte que la mise de nos mémoire se fasse en même temps. Rien de plus simple : il suffit de prendre plusieurs bascules D pour créer notre mémoire, et de relier ensemble leurs entrées de validation d'écriture.

Image utilisateur

C'est ainsi que son créer les mémoires qui sont internes à nos circuits séquentiels. Vous verrez que beaucoup des circuits d'un ordinateur sont des circuits séquentiels, et que ceux-ci contiennent toujours des petites mémoires, fabriquées à l'aide de bascules. Ces petites mémoires, que l'on vient de créer, sont appelées des registres.


Circuits combinatoires Tic, Tac, Tic, Tac : Le signal d'horloge

Tic, Tac, Tic, Tac : Le signal d'horloge

Circuits séquentiels C'est quoi un ordinateur ?

Tic, Tac, Tic, Tac : Le signal d'horloge

Visiblement, il ne manque rien : on sait fabriquer des mémoires et des circuits combinatoires, rien ne peut nous arrêter dans notre marche vers la conception d'un ordinateur. Tout semble aller pour le mieux dans le meilleur des mondes. Sauf que non, on a oublié de parler d'un léger détail à propos de nos circuits.

Temps de propagation

Tout circuit, quel qu'il soit, va mettre un petit peu de temps avant de réagir. Ce temps mit par le circuit pour s'apercevoir qu'il s'est passé quelque chose sur son entrée et modifier sa sortie en conséquence s'appelle le temps de propagation. Pour faire simple, c'est le temps que met un circuit à faire ce qu'on lui demande. Pour en donner une définition plus complète, on peut dire que c'est le temps entre le moment pendant lequel on modifie la tension sur une entrée d'un circuit logique et le moment où cette modification se répercute sur les sorties.

Ce temps de propagation dépend fortement du circuit et peut dépendre de pas mal de paramètres. Mais il y a trois raisons principales, qui sont à l'origine de ce temps de propagation. Il va de soit que plus ce temps de propagation est élevé, plus notre circuit risque d'être lent, et savoir sur quoi jouer pour le diminuer n'est pas un luxe. Voyons donc ce qu'il en est.

Critical Path

Le plus important de ces paramètres est ce qu'on appelle le Critical Path. Il s'agit du 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 suivant.

Image utilisateur

Pour ce circuit, le Critical Path est le chemin dessiné en rouge. En suivant ce chemin, on va traverser 3 portes logiques, contre deux ou une dans les autres chemins. Pour information, tous les chemins possibles ne sont pas présentés sur le schéma, mais ceux qui ne sont pas représentés passent par moins de 3 portes logiques.

De plus, on doit préciser que nos portes n'ont pas toute le même temps de propagation : une porte NON aura tendance à être plus rapide qu'une porte NAND, par exemple.

Fan Out

Autre facteur qui joue beaucoup sur ce temps de propagation : le nombre de composants reliés sur la sortie d'une porte logique. Plus on connecte de portes logiques sur un fil, plus il faudra du temps pour la tension à l'entrée de ces portes change pour atteindre sa bonne valeur.

Wire Delay

Autre facteur qui joue dans le temps de propagation : le temps mis par notre tension pour se propager dans les "fils" et les interconnexions qui relient les portes logiques entre elles. Ce temps dépend notamment de la résistance (celle de la loi d'Ohm, que vous avez surement déjà vue il y a un moment) et de ce qu'on appelle la capacité des interconnexions. Ce temps perdu dans les fils devient de plus en plus important au fil du temps, les transistors et portes logiques devenant de plus en plus rapides à force des les miniaturiser. Pour donner un exemple, sachez que si vous comptez créer des circuits travaillant sur des entrées de 256 à 512 bits qui soient rapides, il vaut mieux modifier votre circuit de façon à minimiser le temps perdu dans les interconnexions au lieu de diminuer le Critical Path.

Circuits synchrones

Ce temps de propagation doit être pris en compte quand on crée un circuit séquentiel. Sans cela on ne sait pas quand mettre à jour la mémoire intégrée dans notre circuit séquentiel. Si on le fait trop tôt, le circuit ne se comportera pas comme il faut : on peut parfaitement sauter des états. De plus, les différents circuits d'un ordinateur 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 composant lent peut donc rater deux ou trois ordres successifs envoyées par un composant un peu trop rapide.

Comment éviter les ennuis dus à l'existence de ce temps de propagation ?

Il existe diverses solutions. On peut notamment faire en sorte que les entrées et le circuit combinatoire prévienne la mémoire quand ils veulent la mettre à jour. Quand l'entrée et le circuit combinatoire sont prêts, on autorise l'écriture dans la mémoire. C'est ce qui est fait dans les circuits asynchrones. Mais ce n'est pas cette solution qui est utilisée dans nos ordinateur.

La majorité des circuits de nos ordinateur gèrent les temps de propagation différemment. Ce sont ce qu'on appelle des circuits synchrones. Pour simplifier, ces circuits vont mettre à jour leurs mémoires à intervalles réguliers. La durée entre deux mises à jour est constante et doit être plus grande que le pire temps de propagation possible du circuit. Les concepteurs d'un circuit doivent estimer le pire temps de propagation possible pour le circuit et ajouter une marge de sureté.

L'horloge

Pour mettre à jour nos circuits à intervalles réguliers, ceux-ci sont commandés par une tension qui varie de façon cyclique : le signal d'horloge. Celle-ci passe de façon cyclique de 1 à 0. Cette tension effectue un cycle plusieurs fois par seconde. Le temps que met la tension pour effectuer un cycle est ce qu'on appelle la période. Le nombre de cycle, de périodes, en une seconde est appelé la fréquence. Cette fréquence se mesure dans une unité : le hertz.

Image utilisateur

On voit sur ce schéma que la tension ne peut pas varier instantanément : la tension met un certain temps pour passer de 0 à 1 et de 1 à 0. On appelle cela un front. La passage de 0 à 1 est appelé un front montant et le passage de 1 à 0 un front descendant.

Les circuits

Cette horloge est reliée aux entrées d'autorisation d'écriture des bascules du circuit. Pour cela, on doit rajouter une entrée sur notre circuit, sur laquelle on enverra l'horloge.

Image utilisateur

En faisant cela, notre circuit logique va "lire" les entrées et en déduire une sortie uniquement lorsqu'il voit un front montant (ou descendant) sur son entrée d'horloge ! Entre deux fronts montants (ou descendants), notre circuit est complétement autiste du point de vue des entrées : on peut faire varier autant de fois qu'on veut nos entrées, il faudra attendre le prochain front montant pour notre circuit réagisse.

Dans le cas où notre circuit est composé de plusieurs sous-circuits devant être synchronisés via l’horloge, celle-ci est distribuée à tous les sous-circuits à travers un réseau de connections électriques qu'on appelle l'arbre d'horloge.

Et dans nos PC ?

Dans la pratique, une bonne partie des composants d'un ordinateur sont synchronisés par des horloges. Oui, j'ai bien dit DES horloges. Par exemple, notre processeur fonctionne avec une horloge différente de l'horloge de la mémoire ! La présence de plusieurs horloges est justifiée par un fait très simple : certains composants informatiques sont plus lents que d'autres et ne sont pas capables de fonctionner avec des horloges rapides. Par exemple, le processeur a souvent une horloge très rapide comparée à l'horloge des autres composants. Généralement, plus un composant utilise une fréquence élevée, plus celui-ci est rapide. Cela n'est toutefois pas un élément déterminant : un processeur de 4 gigahertz peut être bien plus rapide qu'un processeur de 200 gigahertz, pour des raisons techniques qu'on verra plus tard dans ce tutoriel. De nos jours, c'est plus la façon dont notre processeur va faire ses opérations qui sera déterminante : ne vous faites pas avoir par le Megahertz Myth !

En fait, il existe une horloge de base qui est "transformée" en plusieurs horloges dans notre ordinateur. On peut parfaitement transformer un signal d'horloge en un autre, ayant une période deux fois plus grande ou plus petite, grâce à des montages électroniques spécialisés. Cela peut se faire avec des composants appelés des PLL ou encore avec des montages à portes logiques un peu particuliers, qu'on n'abordera pas ici.

Les premiers processeurs avaient une fréquence assez faible et étaient peu rapides. Au fil du temps, avec l’amélioration des méthodes de conception des processeurs, la fréquence de ceux-ci a commencée a augmenter. Ces processeurs sont devenus plus rapides, plus efficaces. Pour donner un ordre de grandeur, le premier microprocesseur avait une fréquence de 740 kilohertz (740 000 hertz). De nos jours, les processeurs peuvent monter jusqu'à plusieurs gigahertz : plusieurs milliards de fronts par secondes ! :waw: Quoiqu'il en soit, cette montée en fréquence est aujourd'hui terminée : de nos jours, les concepteurs de processeurs sont face à un mur et ne peuvent plus trop augmenter la fréquence de nos processeurs aussi simplement qu'avant.

Et pourquoi les concepteurs de processeurs ont-ils arrêtés d'augmenter la fréquence de nos processeurs ?

Augmenter la fréquence a tendance à vraiment faire chauffer le processeur plus que de raison : difficile de monter en fréquence dans ces conditions. Une grande part de cette dissipation thermique a lieu dans l'arbre d'horloge : environ 20% à 35%. Cela vient du fait que les composants reliés à l'arbre horloge doivent continuer à changer d'état tant que l'horloge est présente, et ce même quand ils sont inutilisés. C'est la première limite à la montée en puissance : la dissipation thermique est tellement importante qu'elle limite grandement les améliorations possibles et la montée en fréquence de nos processeurs.

Auparavant, un processeur était refroidi par un simple radiateur. Aujourd'hui, on est obligé d'utiliser un radiateur et un ventilateur, avec une pâte thermique de qualité tellement nos processeurs chauffent. Pour limiter la catastrophe, tous les fabricants de CPU cherchent au maximum à diminuer la température de nos processeurs. Pour cela, ils ont inventé diverses techniques permettant de diminuer la consommation énergétique et la dissipation thermique d'un processeur. Mais ces techniques ne suffisent plus désormais. C'est ce qui est appelé le Heat Wall.

Et voilà, avec un cerveau en parfait état de marche, et beaucoup de temps devant vous, vous pouvez construire n'importe quel circuit imaginable et fabriquer un ordinateur. Du moins, en théorie : n'essayez pas chez vous. :diable: Bon, blague à part, avec ce chapitre, vous avez tout de même le niveau pour créer certains circuits présents dans notre ordinateur comme l'ALU. Sympa, non ?


Circuits séquentiels C'est quoi un ordinateur ?

C'est quoi un ordinateur ?

Tic, Tac, Tic, Tac : Le signal d'horloge Numérique versus analogique

Nos ordinateurs servent à manipuler de l'information. Cette information peut être une température, une image, un signal sonore, etc. Bref, du moment que ça se mesure, c'est de l'information. Cette information, ils vont devoir la transformer en quelque chose d'exploitable et de facilement manipulable, que ce soit aussi bien du son, que de la vidéo, du texte et pleins d'autres choses. Pour cela, on va utiliser l'astuce vue au chapitre précédent : on code chaque information grâce à un nombre.

Une fois l’information codée correctement sous la forme de nombres, il suffira d'utiliser une machine à calculer pour pouvoir effectuer des manipulations sur ces nombres, et donc sur l’information codée : une simple machine à calculer devient alors une machine à traiter de l'information. Un ordinateur est donc un calculateur. Mais par contre, tout calculateur n'est pas un ordinateur : par exemple, certains calculateurs ne comptent même pas en binaire. Mais alors, qu'est-ce qu'un ordinateur ?

Numérique versus analogique

C'est quoi un ordinateur ? Architecture de base

Numérique versus analogique

Pour pouvoir traiter de l'information, la première étape est d'abord de coder celle-ci. On a vu dans le chapitre sur le binaire comment représenter des informations simples en utilisant le binaire. Mais ce codage, cette transformation d’information en nombre, peut être fait de plusieurs façons différentes, et coder des informations en binaire n'est pas le seul moyen.

Analogique versus numérique

Dans les grandes lignes, on peut identifier deux grands types de codage :

Le codage analogique

Celui-ci utilise des nombres réels : il code l’information avec des grandeurs physiques (des trucs qu'on peut mesurer par un nombre) comprises dans un intervalle.

Un codage analogique a une précision théoriquement infinie : on peut par exemple utiliser toutes les valeurs entre 0 et 5 pour coder une information. Celle-ci peut alors prendre une valeur comme 1 , 2.2345646, ou pire...

Le codage numérique

Celui-ci utilise uniquement des suites de symboles (qu'on peut assimiler à des chiffres), assimilables à des nombres entiers pour coder les informations. Pour simplifier, le codage numérique va coder des informations en utilisant des nombres entiers codés dans une base, qui peut être 2, 3, 4, 16, etc. Les fameux symboles dont je viens de parler sont simplement les chiffres de cette base.

Le codage numérique n'utilise qu'un nombre fini de valeurs, contrairement au codage analogique. Un code numérique a une précision figée et ne pourra pas prendre un grand nombre de valeurs (comparé à l'infini). :p Cela donnera des valeurs du style : 0, 0.12 , 0.24 , 0.36, 0.48... jusqu'à 2 volts.

Un calculateur analogique peut donc faire des calculs avec une précision très fine, et peut même faire certains calculs avec une précision impossible à atteindre avec un calculateur numérique : des dérivées, des intégrations, etc. Un calculateur numérique peut bien sûr effectuer des intégrations et dérivations, mais ne donnera jamais un résultat exact et se contentera de donner une approximation du résultat. Un calculateur analogique pourra donner un résultat exact, du moins en théorie : un calculateur analogique insensible aux perturbations extérieures et n'ayant aucune imperfection n'a pas encore été inventé.

Pour les calculateurs numériques, les nombres manipulés sont codés par des suites de symboles (des "chiffres", si vous préférez), et un calculateur numérique ne fera que transformer des suites de symboles en d'autres suites de symboles. Vous pouvez par exemple identifier chacun de ces symboles en un chiffre dans une base entière quelconque (pas forcément la base 10 ou 2).

Dans un ordinateur, les symboles utilisés ne peuvent prendre que deux valeurs : 0 ou 1. De tels symboles ne sont rien d'autre que les fameux bits du chapitre précédent, ce qui fait que notre ordinateur ne manipule donc que des bits : vous comprenez maintenant l'utilité du premier chapitre. ^^

L'immunité au bruit

Vu ce qui a été dit précédemment, nos calculateurs numériques ne semblent pas vraiment très intéressants. Et pourtant, la grande majorité de nos composants et appareils électroniques (dont nos ordinateurs) sont des machines numériques ! C'est du au fait que les calculateurs analogiques ont un gros problème : ils ont une faible immunité au bruit.

Explication : un signal analogique peut facilement subir des perturbations qui vont changer sa valeur, de façon parfois assez drastique. Autant vous dire que si une de ces perturbations un peu violente arrive, le résultat qui arrive en sortie n'est vraiment pas celui attendu. Si un système est peu sensible à ces perturbations, on dit qu'il a une meilleure immunité au bruit.

Un signal numérique n'a pas trop ce problème : les perturbations ou parasites vont moins perturber le signal numérique et vont éviter de trop modifier le signal original : l'erreur sera beaucoup plus faible qu'avec un signal analogique.

Mais pourquoi avoir choisi la base 2 dans nos ordinateurs ?

La question est parfaitement légitime : on aurait tout aussi bien pu prendre la base 10 ou n'importe quelle autre base. Il aurait été bien plus facile pour les humains qui doivent programmer ces machines d'utiliser la base 10. D'ailleurs, il existe de nombreuses machines qui manipulent des données numériques en base 10, en base 3, etc. Et on a déjà inventé des ordinateurs qui comptaient en base 3 : l'ordinateur SETUN, par exemple, fabriqué et conçu pour l'université de Moscou. Et rien n’empêche de créer des ordinateurs qui compteraient en base 10, 16, ou tout autre base. Mais il y a plusieurs raisons qui font que le binaire a été choisi comme base pour le codage de l'information dans un ordinateur.

La plus importante de toutes, c'est qu'une perturbation n'aura pas le même effet sur un nombre codé en base 2 et sur un nombre codé en base 10.

En effet, supposons que nous utilisions, par exemple, une tension comprise entre 0 et 9 volts, qui code un chiffre/symbole allant de 0 à 9 (on utilise donc la base 10). Le moindre parasite peut changer la valeur du chiffre codé par cette tension.

Image utilisateur

Avec cette tension qui code seulement un 0 ou un 1 (de 0volts pour un 0 et 10 pour un 1), un parasite de 1 volt aura nettement moins de chance de modifier la valeur du bit codé ainsi.

Image utilisateur

Le parasite aura donc un effet nettement plus faible : la résistance aux perturbations électromagnétiques extérieure est meilleure.


C'est quoi un ordinateur ? Architecture de base

Architecture de base

Numérique versus analogique Ordinateurs

Architecture de base

Une fois notre information codée, il faut ensuite pouvoir la manipuler et la stocker. Ce traitement de notre information peut être fait de différentes façons. Pour transformer cette information et en faire quelque chose, il va falloir effectuer une série d'étapes. La première étape, c'est de coder cette information sous une forme utilisable. Mais ça ne fait pas tout, il faut encore traiter cette information.

I/O et traitement

Pour cela, on va donc devoir :

Toute machine traitant de l'information est donc composéé par :

Image utilisateur

Notre ordinateur contient pas mal d'entrées et de sorties. Par exemple, votre écran est une sortie : il reçoit des informations, et les transforme en image affichée à l'écran. On pourrait aussi citer des dispositifs comme des imprimantes, ou des haut-parleurs. Comme entrée, vous avez votre clavier, votre souris, pour ne citer qu'eux.

Automates

Cette unité de traitement peut très bien consister en un vulgaire circuit combinatoire, ou tout autre mécanisme, sans mémoire. Mais d'autres unités de traitement ont une certaine capacité de mémorisation, comme les circuits séquentiels. Tout système dont l'unité de traitement possède cette capacité de mémorisation, et fonctionne comme un circuit électronique séquentiel, est appelé un automate.

Principe

Celui-ci contiendra donc des mémoires internes en plus de l’unité de traitement, qui représenteront l'état de l’automate (les informations qu'il a mémorisées). Bien sûr, cet état peut être mis à jour, et on peut changer l'état de notre automate pour modifier ces informations, les manipuler, etc. Notre unité de traitement pourra donc manipuler directement le contenu de nos mémoires. Notre automate passera donc d'états en états, via des suites d'étapes qui transformeront un état en un autre : on modifiera les informations contenues dans notre automate étapes par étapes jusqu'à arriver au résultat voulu. Ces changements d'état sont bien sur gouvernés par l'unité de traitement.

Image utilisateur

Attention : ce schéma est un schéma de principe. Il existe des automates pour lesquels il n'y a pas de séparation nette entre mémoire et circuits de traitement. Il est possible de créer des circuits dans lesquels la mémorisation des informations est entremêlée avec les circuits chargés de traiter l'information. Toutefois, dans nos ordinateurs, les deux sont relativement bien séparés, même si ce n'est pas totalement le cas. D'ailleurs, nos ordinateurs sont des automates spéciaux, composés à partir de composants plus petits qui sont eux-même des automates.

Pour information, on peut très bien créer des automates avec un peu n'importe quoi. Du genre, des dispositifs hydrauliques, ou électriques, magnétiques, voire à air comprimé. Pour citer un exemple, on peut citer le calculateur hydraulique MONIAC. Quant à nos ordinateurs, ils sont fabriqués avec des dispositifs électroniques, comme des portes logiques ou des montages à base de transistors et de condensateurs. Évidement, il existe des automates numériques, et des automates analogiques, voire des automates hybrides mélangeant des circuits analogiques et des circuits numériques.

Automate numérique

Dans un automate numérique (un ordinateur par exemple), l’information placée sur l'entrée est codée sous la forme d'une suite de symboles avant d'être envoyée à l'unité de traitement ou en mémoire. Nos informations seront codées par des suites de symboles, des nombres codés dans une certaine base, et seront stockées ainsi en mémoire. Les suites de symboles manipulées sont appelées des données. Dans nos ordinateurs, les symboles utilisés étant des zéros et des uns, nos données sont donc de simples suites de bits.

Reste que ces données seront manipulées par notre automate, par son unité de traitement. Tout ce que peut faire la partie traitement d'un automate numérique, c'est modifier l'état de l'automate, à savoir modifier le contenu des mémoires de l'automate. Cela peut permettre de transformer une (ou plusieurs) donnée en une (ou plusieurs) autre(s), ou de configurer l'automate pour qu'il fonctionne correctement. Ces transformations élémentaires qui modifient l'état de l'automate sont appelées des instructions. Un automate numérique est donc une machine qui va simplement appliquer une suite d'instructions dans un ordre bien précis sur les données. C’est cette suite d'instructions qui va définir le traitement fait par notre automate numérique, et donc ce à quoi il peut servir.

Programme

Dans certains automates, la suite d'instructions effectuée est toujours la même. Une fois conçus, ceux-ci ne peuvent faire que ce pourquoi ils ont été conçus. Ils ne sont pas programmables. C'est notamment le cas pour les calculateurs analogiques : une fois qu'on a câblé un automate analogique, il est impossible de lui faire faire autre chose que ce pour quoi il a été conçu sans modifier son câblage. A la rigueur, on peut le reconfigurer et faire varier certains paramètres via des interrupteurs ou des boutons, mais cela s’arrête là. D'autres automates numériques ont le même problème : la suite d'instruction qu'ils exécutent est impossible à changer sans modifier les circuits de l'automate lui-même. Et cela pose un problème : à chaque problème qu'on veut résoudre en utilisant un automate, on doit recréer un nouvel automate. Autant dire que ça peut devenir assez embêtant !

Mais il existe une solution : créer des automates dont on peut remplacer la suite d'instructions qu'ils effectuent par une autre sans avoir à modifier leur câblage. On peut donc faire ce que l'on veut de ces automates : ceux-ci sont réutilisables à volonté et il est possible de modifier leur fonction du jour au lendemain et leur faire faire un traitement différent. On dit qu'ils sont programmables. Ainsi, pour programmer notre ordinateur, il suffira de créer une suite d'instructions qui va faire ce que l'on souhaite. Et c'est bien plus rapide que de créer un automate complet de zéro. Cette suite d'instruction sera alors appelée le programme de l'automate.

La solution utilisée pour rendre nos automates programmables consiste à stocker le programme dans une mémoire, qui sera modifiable à loisir. C'est ainsi que notre ordinateur est rendu programmable : on peut parfaitement modifier le contenu de cette mémoire (ou la changer, au pire), et donc changer le programme exécuté par notre ordinateur sans trop de problèmes. Mine de rien, cette idée d'automate stockant son programme en mémoire est ce qui a fait que l’informatique est ce qu'elle est aujourd’hui. C'est la définition même d'ordinateur : automate programmable qui stocke son programme dans sa mémoire.


Numérique versus analogique Ordinateurs

Ordinateurs

Architecture de base La gestion de la mémoire

Ordinateurs

Tous nos ordinateurs sont plus ou moins organisés sur un même modèle de base, une organisation commune. Notre ordinateur est ainsi découpé en composants bien distincts, qui ont chacun une utilité particulière. Dans ce découpage en composant, on retrouve plus ou moins l'organisation qu'on a vue au-dessus, avec son entrée, sa sortie, son unité de traitement, sa mémoire, etc.

Organisation

Notre ordinateur contient donc :

Image utilisateur

Cela ressemble fortement à l'organisation vue plus haut, avec son entrée, sa sortie, son unité de traitement et sa mémoire. Rien d'étonnant à cela, notre ordinateur est un automate comme un autre, et il n'est pas étonnant qu'il reprenne une organisation commune à pas mal d'automates (mais pas à tous : certains fusionnent la mémoire et l'unité de traitement dans un seul gros circuit, ce que ne font pas nos ordinateurs). Rien n’empêche à notre ordinateur (ou à tout autre automate d'ailleurs) d'utiliser plusieurs processeurs, plusieurs mémoires, plusieurs bus, plusieurs entrées ou plusieurs sorties.

Périphériques

Cet ensemble de composants, ainsi que la façon dont ils communiquent entre eux est la structure minimum que tout ordinateur possède, le minimum syndical. 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. On peut donner comme exemple le clavier, la souris, l'écran, la carte son, etc.

Microcontroleurs

Parfois, on décide de regrouper la mémoire, les bus, le CPU et les ports d'entrée-sortie dans un seul boîtier, histoire de rassembler tout cela dans un seul composant électronique nommé microcontrôleur. Dans certains cas, qui sont plus la règle que l'exception, certains périphériques sont carrément inclus dans le microcontrôleur ! On peut ainsi trouver dans ces microcontrôleurs, des compteurs, des générateurs de signaux, des convertisseurs numériques-analogiques... On trouve des microcontrôleurs dans les disques durs, les baladeurs mp3, dans les automobiles, et tous les systèmes embarqués en général. Nombreux sont les périphériques ou les composants internes à un ordinateur qui contiennent des microcontrôleurs.

Maintenant qu'on connait un peu mieux l'organisation de base, voyons plus en détail ces différents composants.

Mémoire

La mémoire est, je me répète, le composant qui se chargera de stocker notre programme à éxecuter, ainsi que les données qu'il va manipuler. Son rôle est donc de retenir que des données ou des instructions stockées sous la forme de suites de bits, afin qu'on puisse les récupérer et les traiter.

ROM et RWM

Pour simplifier grandement, on peut grossièrement classer nos mémoire en deux types : les Read Only Memory, et les Read Write Memory.

Pour les mémoires ROM (les Read Only Memory), on ne peut pas modifier leur contenu. On peut récupérer une donnée ou une instruction dans notre mémoire : on dit qu'on y accède en lecture. Mais on ne peut pas modifier les données qu'elles contiennent. On utilise de telles mémoires pour stocker des programmes ou pour stocker des données qui ne peuvent pas varier. Par exemple, votre ordinateur contient une mémoire ROM spéciale qu'on appelle le BIOS, qui permet de démarrer votre ordinateur, le configurer à l'allumage, et démarrer votre système d'exploitation.

Quand aux mémoire RWM (les Read Write Memory), on peut accéder à celle-ci en lecture, et donc récupérer une donnée stockée en mémoire, mais on peut aussi y accéder en écriture : on stocke une donnée dans la mémoire, ou on modifie une donnée existante. Ces mémoires RWM sont déjà plus intéressantes, et on peut les utiliser pour stocker des données. On va donc forcément trouver au moins une mémoire RWM dans notre ordinateur.

Pour l'anecdote, il n'existe pas de Write Only Memory. ^^

Adressage

Pour utiliser cette mémoire, le processeur va pouvoir rapatrier des données depuis celle-ci. Pour éviter de s’emmêler les pinceaux, et confondre une donnée avec une autre, le processeur va devoir utiliser un moyen pour retrouver une donnée dans notre mémoire. Il existe plusieurs solutions, mais une de ces solutions est utilisée dans la grosse majorité des cas.

Dans la majorité des cas, notre mémoire est découpée en plusieurs cases mémoires, des blocs de mémoire qui contiennent chacun un nombre fini et constant de bits. Chaque case mémoire se voit attribuer un nombre binaire unique, l'adresse, qui va permettre de la 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. Ben les adresses mémoires, c'est pareil !

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

Image utilisateur

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

Anatomie

Une mémoire est un composant assez simple. Dans les grandes lignes, une mémoire est composée de deux à trois grands circuits. Le premier circuit contient toutes les cases mémoires : il s'agit du plan mémoire. C'est la mémoire proprement dite, là où sont stockées les données/instructions. Il existe différentes façons pour concevoir des cases mémoires. Pour information, dans le chapitre précédent, on avait vu comment créer des registres à partir de bascules D : on avait alors crée une case mémoire d'une mémoire RWM.

Ces cases mémoires ne nous servent à rien si l'on ne peut pas les sélectionner. Heureusement, les mémoires actuelles sont adressables, et on peut préciser quelle case mémoire lire ou écrire en précisant son adresse. Cette sélection d'une case à partie de son adresse ne se fait pas toute seule : on a besoin de circuits supplémentaires pour gérer l'adressage. Ce rôle est assuré par un circuit spécialisé qu'on appelle le contrôleur mémoire.

Et enfin, on doit relier notre mémoire au reste de l'ordinateur via un bus. On a donc besoin de connexions avec le bus. Ces connexions nous permettent aussi de savoir dans quel sens transférer les données (pour une mémoire RWM).

Image utilisateur
Bus de communication

Maintenant qu'on a une mémoire ainsi que nos entrées-sorties, il va bien falloir que notre processeur puisse les utiliser. Pour cela, le processeur est relié à la mémoire ainsi qu'aux entrées-sorties par un ou plusieurs bus. Ce bus n'est rien d'autre qu'un ensemble de fils électriques sur lesquels on envoie des zéros ou des uns. Ce bus relie le processeur, la mémoire, les entrées et les sorties ; et leur permet d’échanger des données ou des instructions.

Pour permettre au processeur (ou aux périphériques) de communiquer avec la mémoire, il y a trois prérequis que ce bus doit respecter :

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. Ceux-ci relieront les différents composants comme indiqué dans le schéma qui suit.

Image utilisateur

Vous l'avez surement déjà deviné grâce à leur nom, mais je vais quand même expliquer à quoi servent ces différents bus.

Bus

Utilité

Bus d'adresse

Le bus d'adresse 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.

Bus de donnée

Le bus de donnée est un ensemble de fils par lequel s'échangent les données (et parfois les instructions) entre le processeur et le reste de la machine.

Bus de commande

Ce bus de commande va permettre de gérer l'intégralité des transferts entre la mémoire et le reste de l'ordinateur. Il peut transférer au moins un bit précisant si on veut lire ou écrire dans la mémoire. Généralement, on considère par convention que ce bit vaut :

  • 1 si on veut faire une lecture,

  • 0 si c'est pour une écriture.

Processeur

C'est un composant qui va prendre en entrée une ou plusieurs données et éxecuter des instructions. Ces instructions peuvent être des additions, des multiplications, par exemple, mais qui peuvent aussi faire des choses un peu plus utiles. Ce processeur est aussi appelé Central Processing Unit, abrévié en CPU.

Un processeur ne peut qu'effectuer une suite d'instructions dans un ordre bien précis. C'est cette propriété qui fait que notre ordinateur est un automate particulier, programmable : on lui permet de faire des instructions indépendantes, et on peut organiser ces instructions dans l'ordre que l'on souhaite : en clair, créer un programme. Pour vous donner une idée de ce que peut être une instruction, on va en citer quelques-unes.

Instructions arithmétiques

Les instructions les plus communes sont des instructions arithmétiques et logiques, qui font simplement des calculs sur des nombres. On peut citer par exemple :

Ces instructions sont des instructions dont le résultat ne dépend que des données à traiter. Elle sont généralement prises en charge par un circuit combinatoire indépendant, qui s'occupe exclusivement du calcul de ces instructions : l'unité de calcul.

Registres

Pour pouvoir fonctionner, tout processeur va devoir stocker 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, qu'il manipule certaines données, etc. Pour cela, il contient des registres. Ces registres sont de petites mémoires ultra-rapides fabriquées avec des bascules.

Ces registres 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. Bref, suivant le processeur, ces registres peuvent servir à tout et n'importe quoi.

Instructions d'accès mémoire

Pour faire ces calculs, et exécuter nos instructions arithmétiques et logiques, notre processeur doit aller chercher les données à manipuler dans la mémoire RAM ou dans ses registres. Après tout, les données manipulées par nos instructions ne sortent pas de nulle part. Certaines d'entre elles peuvent être stockées dans les registres du processeur, mais d'autres sont stockées dans la mémoire principale : il faut bien y aller les chercher.

Pour cela, notre processeur va devoir échanger des données entre les registres et la mémoire, copier une donnée d'un endroit de la mémoire à un autre, copier le contenu d'un registre dans un autre, modifier directement le contenu de la mémoire, effectuer des lectures ou écriture en mémoire principale, etc.

Comme vous l'avez surement deviné, les accès mémoires ne sont pas pris en charge par les unités de calcul. Pour gérer ces communications avec la mémoire, le processeur devra être relié à la mémoire et devra décider quoi lui envoyer comme ordre sur les différents bus (bus de commande, de donnée, d'adresse). Il pourra ainsi lui envoyer des ordres du style : "Je veux récupérer le contenu de l'adresse X", ou "Enregistre moi la donnée que je t'envoie à l'adresse Y". Ces ordres seront transmis via le bus. L'intérieur de notre processeur ressemble donc à ceci, pour le moment :

Image utilisateur
Program Counter

Il est évident que pour exécuter une suite d'instructions dans le bon ordre, notre ordinateur doit savoir quelle est la prochaine instruction à exécuter. Il faut donc que notre processeur se souvienne de cette information quelque part : notre processeur 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.

Ce registre stocke l'adresse de la prochaine instruction à exécuter. Cette adresse permet de localiser l'instruction suivante en mémoire. Cette adresse ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples qu'on verra dans la suite de ce tutoriel.

Ce calcul peut être fait assez simplement. Généralement, on profite du fait que ces instructions sont exécutées dans un ordre bien précis, les unes après les autres. Sur la grosse majorité des ordinateur, celles-ci sont placées les unes à la suite des autres dans l'ordre où elles doivent être exécutées. L'ordre en question est décidé par le programmeur. Un programme informatique n'est donc qu'une vulgaire suite d'instructions stockée quelque part dans la mémoire de notre ordinateur.

Adresse

Instruction

0

Charger le contenu de l'adresse 0F05

1

Charger le contenu de l'adresse 0555

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

En faisant ainsi, on peut calculer facilement l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction juste chargée (le nombre de case mémoire qu'elle occupe) au contenu du registre d'adresse d'instruction. Dans ce cas, l'adresse de la prochaine instruction est calculée par un petit circuit combinatoire couplé à notre registre d'adresse d'instruction, qu'on appelle le compteur ordinal.

L'intérieur de notre processeur ressemble donc plus à ce qui est indiqué dans le schéma du dessous.

Image utilisateur

Mais certains processeurs n'utilisent pas cette méthode. Sur de tels processeurs, chaque instruction va devoir préciser quelle est la prochaine instruction. Pour ce faire, une partie de la suite de bit représentant notre instruction à exécuter va stocker cette adresse. Dans ce cas, ces processeurs utilisent toujours un registre pour stocker cette adresse, mais ne possèdent pas de compteur ordinal, et n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau.

Prise de décision

Notre processeur peut donc exécuter des instructions les unes à la suite des autres grâce à notre registre d'adresse d'instruction (le Program Counter). C'est bien, mais on ne pas bien loin avec ce genre de choses. Il serait évidemment mieux si notre processeur pouvait faire des choses plus évoluées et s'il pouvait plus ou moins s'adapter aux circonstances au lieu de réagir machinalement. Par exemple, on peut souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie et ne l’exécute pas sinon. 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.

Pour ce faire, on a crée des instructions un peu spéciales, qui permettent de "sauter" directement à une instruction dans notre programme, et poursuivre l'exécution à partir de cette instruction. Cela permet au programme de passer directement à une instruction située plus loin dans le déroulement normal du programme, voir de revenir à une instruction antérieure. Ces instructions sont ce qu'on appelle des branchements.

Pour ce faire, elles modifient le contenu du registre d'adresse d'instruction, et y place l'adresse de l'instruction à laquelle on veut sauter. Ces instructions sont appelées des branchements. Elles sont très utiles pour créer nos programmes informatiques, et il serait vraiment difficle, voire impossible de vous passer d'elles. Tout programmeur utilise des branchements quand il programme : il ne s'en rend pas compte, mais ces branchements sont souvent cachés derrière des fonctionnalités basiques de nos langages de programmation usuels (les if, tests, boucles, et fonctions sont fabriquées avec des branchements).

Séquenceur

Quoiqu'il en soit, toutes nos instructions sont stockées en mémoire sous la forme de suites de bits. A telle instruction correspondra telle suite de bit. Notre processeur devra donc décider quoi faire de ces suites de bits, et les interpréter, en déduire quoi faire. Par exemple, est-ce que la suite de bit que je viens de lire me demande de charger une donnée depuis la mémoire, est-ce qu'elle me demande de faire une instruction arithmétique, etc. Une fois cela fait, il faut ensuite aller configurer la mémoire pour gérer les instructions d'accès mémoire (lire la bonne adresse, préciser le sens de transferts, etc), ou commander l'unité de calcul afin qu'elle fasse une addition et pas une multiplication, ou mettre à jour le registre d'adresse d'instruction si c'est un branchement, etc.

Pour ce faire, notre processeur va contenir un circuit séquentiel spécial, qui déduit quoi faire de la suite d'instruction chargée, et qui commandera les circuits du processeur. Ce circuit spécialisé s'appelle le séquenceur.

Image utilisateur

Architecture de base La gestion de la mémoire

La gestion de la mémoire

Ordinateurs Deux mémoires pour le prix d'une

On a vu que notre programme était stocké dans la mémoire de notre ordinateur. Les instructions du programme exécuté par le processeur sont donc stockées comme toutes les autres données : sous la forme de suites de bits dans notre mémoire, tout comme les données qu'il va manipuler. Dans ces conditions, difficile de faire la différence entre donnée et instruction. Mais rassurez-vous : le processeur intègre souvent des fonctionnalités qui empêchent de confondre une donnée avec une instruction quand il va chercher une information en mémoire.

Ces fonctionnalités ne sont pas totalement fiables, et il arrive assez rarement que le processeur puisse confondre une instruction ou une donnée, mais cela est rare. Cela peut même être un effet recherché : par exemple, on peut créer des programmes qui modifient leurs propres instructions : cela s'appelle du self modifying code, ce qui se traduit par code automodifiant en français. 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), pouvait aussi permettre de rendre nos programmes plus rapides, servait à compresser un programme, ou pire : permettait de cacher un programme et le rendre indétectable dans la mémoire (les virus informatiques utilisent beaucoup de genre de procédés). Mais passons !

Deux mémoires pour le prix d'une

La gestion de la mémoire L'organisation de la mémoire et la pile

Deux mémoires pour le prix d'une

La plus importante de ces astuces évitant la confusion entre données et instructions est très simple : les instructions et les données sont stockées dans deux portions de mémoire bien séparées. Sur de nombreux ordinateurs, la mémoire est séparée en deux gros blocs de mémoires bien spécialisés :

Ces blocs de mémoire vont donc stocker des contenus différents :

Portion de la mémoire

Mémoire programme

Mémoire de travail

Contenu du bloc

  • le programme informatique à exécuter

  • et parfois les constantes : ce sont des données qui peuvent être lues mais ne sont jamais accédées en écriture durant l'exécution du programme. Elle ne sont donc jamais modifiées et gardent la même valeur quoi qu'il se passe lors de l'exécution du programme.

les variables du programme à exécuter, qui sont des données que le programme va manipuler.

Il faut toutefois préciser que ce découpage en mémoire programme et mémoire de travail n'est pas une obligation. En effet, certains ordinateurs s'en passent complétement : je pense notamment aux architectures dataflow, une classe d’ordinateur assez spéciale, qui ne sera pas traitée dans ce tutoriel, mais qui est néanmoins abordées dans un article assez compliqué sur ce site. Mais remettons cela à plus tard, pour quand vous aurez un meilleur niveau.

Le processeur ne traitera pas de la même façon les instructions en mémoire programme et les données présentes en mémoire de travail, afin de ne pas faire de confusions. Nos instructions sont en effet interprétées par le séquenceur, tandis que nos données sont manipulées par l'unité de calcul. Et tout cela, c'est grâce à l'existence du Program Counter, le fameux registre d'adresse d'instruction vu précédemment. En regroupant nos instructions dans un seul bloc de mémoire, et en plaçant nos instructions les unes à la suite des autres, on est sur que le registre d'adresse d'instruction passera d'une instruction à l'autre en restant dans un bloc de mémoire ne contenant que des instructions. Sauf s'il déborde de ce bloc, ou qu'un branchement renvoie notre processeur n'importe où dans la mémoire, mais passons.

Quoiqu'il en soit, ce découpage entre mémoire programme et mémoire de travail est quelque chose d'assez abstrait qui peut être mis en pratique de différentes manières. Sur certains ordinateurs, on utilise deux mémoires séparées : une pour le programme, et une pour les données. Sur d'autres, on utilisera une seule mémoire, dont une portion stockera notre programme, et l'autre servira de mémoire de travail. Il faut bien faire la différence entre le découpage de notre mémoire en mémoires de programmes et de travail, purement "conceptuelles" ; et les différentes mémoires qu'on trouvera dans nos ordinateurs. Rassurez-vous, vous allez comprendre en lisant la suite. Dans ce qui suit, on va voir comment des deux mémoires sont organisées dans nos ordinateurs.

Séparation matérielle des mémoires

Sur les ordinateurs très simples, La mémoire programme et la mémoire travail sont souvent placées dans deux mémoires séparées. Il y a deux composants électroniques, chacun dans un boîtier séparé : un pour la mémoire programme et un autre pour la mémoire travail.

Avec cette séparation dans deux mémoires séparées, la mémoire programme est généralement une mémoire de type ROM, c'est à dire accessible uniquement en lecture : on peut récupérer les informations conservées dans la mémoire (on dit qu'on effectue une lecture), mais on ne peut pas les modifier. Par contre, la mémoire travail est une mémoire RWM : on peut lire les informations conservées, mais on peut aussi modifier les données qu'elle contient (écriture). On peut ainsi effectuer de nombreuses manipulations sur le contenu de cette mémoire : supprimer des données, en rajouter, les remplacer, les modifier, etc.

Image utilisateur
Architectures Harvard et Von Neumann

On a vu que le processeur est relié à la mémoire par un ensemble de fils qui connectent ces deux composants, le bus. Dans le cas où la mémoire programme et la mémoire travail sont séparées dans deux composants électroniques matériellement différents, il y a deux façon de relier ces deux mémoires au processeur par un bus :

Le premier cas s'appelle l'architecture Von Neumann.

Image utilisateur

Le second s'appelle l'architecture Harvard.

Image utilisateur

Chacune possède quelques avantages et inconvénients:

Architecture Von neumann

Architecture Harvard

Avantages

  • Accès à la mémoire facile : un seul bus à gérer ;

  • Un seul bus à câbler : simplicité de conception.

  • Permet de charger une instruction et une donnée simultanément : on charge la donnée sur le bus qui relie la mémoire de travail au processeur, et l'instruction sur le bus qui relie processeur et mémoire programme. Les deux bus étant séparés, on peut le faire simultanément. On se retrouve donc avec un gain de vitesse

Inconvénients

  • Ne peut pas charger une donnée simultanément avec une instruction : on doit charger la donnée, puis l'instruction, vu que tout passe par un seul bus. Ce genre d'architecture est donc plus lente.

  • Deux bus à câbler et à gérer ;

  • Accès à la mémoire plus compliqué à gérer.

Architecture modifiée

Sur d'autres, on a besoin de modifier certains paramètres du programmes pour qu'il s'adapte à certaines circonstances. Pour ce faire, il faut donc modifier certaines parties de la mémoire programme. On ne peut donc stocker ces paramètres en ROM, et on préfère plutôt les stocker dans une RWM : la mémoire programme est donc composée d'une ROM et d'une partie de la RWM.

Image utilisateur

Avec cette organisation, une partie ou la totalité du programme est stocké dans une mémoire censée stocker des données. Rien de choquant à cela : programme et données sont tous les deux stockés sous la forme de suites de bits dans la mémoire. Rien n'empêche de copier l'intégralité du programme de la mémoire ROM vers la mémoire RWM, mais ce cas est assez rare.

Mettre les programmes sur un périphérique

On peut même aller plus loin : on peut utiliser une mémoire ROM, contenant un programme de base, et charger directement nos programmes dans la mémoire RWM, depuis un périphérique connecté sur une entrée-sortie : un disque dur, par exemple. Dans ce cas, la mémoire programme n'est pas intégralement stockée dans une ROM : le programme est en effet placé sur un périphérique et chargé en mémoire RWM pour être exécuté. Mais il y a toujours dans tous les ordinateurs, une petite mémoire ROM. Cette ROM contient un petit programme qui va charger le programme stocké sur le périphérique dans la mémoire de travail.
On aura donc le système d'exploitation et nos programmes qui seront donc copiés en mémoire RWM :

L'avantage, c'est qu'on peut modifier le contenu d'un périphérique 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 sur notre périphérique, en rajouter, en modifier, les mettre à jour sans que cela ne pose problème. C'est cette solution qui est utilisée dans nos PC actuels, et la petite mémoire ROM en question s'appelle le BIOS.


La gestion de la mémoire L'organisation de la mémoire et la pile

L'organisation de la mémoire et la pile

Deux mémoires pour le prix d'une Machines à pile et successeurs

L'organisation de la mémoire et la pile

Reste que notre mémoire de travail peut-être organisée de différentes façons, et que celle-ci est elle-même subdivisée en plusieurs morceaux de taille et d'utilité différentes. Suivant le programme que vous utilisez, ou votre système d'exploitation, la mémoire est généralement organisée plus ou moins différemment : votre système d'exploitation ou le programme exécuté peut ainsi réserver certains morceau de programme pour telle ou telle fonctionnalité, ou pour stocker des données particulières. Mais certaines particularités reviennent souvent.

Pile, Tas et Mémoire Statique

Généralement, la mémoire d'un ordinateur est segmentée en quatre parties. On retrouve la mémoire programme, contenant le programme. Par contre, notre mémoire de travail est découpée en trois portions, qui ont des utilités différentes :

Image utilisateur

La mémoire de travail statique est une partie de la mémoire de travail dans laquelle on stocke des données définitivement. En clair, on ne peut pas supprimer l'espace mémoire utilisé par une donnée dont on n'a plus besoin pour l'utiliser pour stocker une autre donnée. On peut donc lire ou modifier la valeur d'une donnée, mais pas la supprimer. Et c'est pareil pour la mémoire programme : on ne peut pas supprimer tout un morceau de programme en cours d’exécution (sauf dans quelques cas particuliers vraiment tordus).

A l'inverse, on peut utiliser le reste de la mémoire pour stocker temporairement des données et les effacer lorsqu'elles deviennent inutiles. Cela permet de limiter l'utilisation de la mémoire. Cette partie de la mémoire utilisable au besoin peut être utilisée de deux façon :

La différence principale entre le tas et la pile est la façon dont sont organisées les données dedans. Une autre différence est leur utilisation : le tas est intégralement géré par le logiciel (par le programme en cours d’exécution et éventuellement le système d'exploitation), tandis que la pile est en partie, voire totalement, gérée par le matériel de notre ordinateur. Dans ce qui va suivre, on va parler de la pile. Pourquoi ? Et bien parce que celle-ci est en partie gérée par notre matériel, et que certains processeurs l'utilisent abondamment. Il existe même des processeurs qui utilisent systématiquement cette pile pour stocker les données que notre processeur doit manipuler. Ces processeurs sont appelés des machines à pile, ou stack machines.

La pile

Comme je l'ai dit plus haut, la pile est une partie de la mémoire de travail. Mais cette portion de la RAM a une particularité : on stocke les données à l'intérieur d'une certaine façon. Les données sont regroupées dans la pile dans ce qu'on appelle des stack frame ou cadres de pile. Ces stack frames regroupent plusieurs cases mémoires contiguës (placées les unes à la suite des autres). On peut voir ces stack frames comme des espèces de blocs de mémoire.

Image utilisateur

Sur les stack machines, ces stack frames stockent généralement un nombre entier, des adresses, des caractères, ou un nombre flottant ; mais ne contiennent guère plus. Mais sur d'autres processeurs un peu plus évolués, on utilise la pile pour stocker autre chose, et il est alors nécessaire d'avoir des stack frame pouvant stocker des données plus évoluées, voire stocker plusieurs données hétérogènes dans une seule stack frame. Ce genre de choses est nécessaire pour implémenter certaines fonctionnalités de certains langages de haut niveau.

Last Input First Output

Mais ce qui différencie une pile d'une simple collection de morceaux de mémoire, c'est la façon dont les stack frames sont gérées.

Comme on peut le voir facilement, les stack frame sont crées une par unes, ce qui fait qu'elles sont placées les unes à la suite des autres dans la mémoire : on crée une stack frame immédiatement après la précédente. C'est une première contrainte : on ne peut pas créer de stack frames n'importe où dans la mémoire. On peut comparer l'organisation des stack frames dans la pile à une pile d'assiette : on peut parfaitement rajouter une assiette au sommet de la pile d'assiette, ou enlever celle qui est au sommet, mais on ne peut pas toucher aux autres assiettes. Sur la pile de notre ordinateur, c'est la même chose : on ne peut accéder qu'à la donnée située au sommet de la pile. Comme pour une pile d'assiette, on peut rajouter ou enlever une stack frame au sommet de la pile, mais pas toucher aux stack frame en dessous, ni les manipuler.

Le nombre de manipulations possibles sur cette pile se résume donc à trois manipulations de base qu'on peut combiner pour créer des manipulations plus complexes.

On peut ainsi :

Image utilisateur

Source de l'image : Wikipédia

Si vous regardez bien, vous remarquerez que la donnée au sommet de la pile est la dernière donnée à avoir été ajoutée (empilée) sur la pile. Ce sera aussi la prochaine donnée à être dépilée (si on n'empile pas de données au dessus). Ainsi, on sait que dans cette pile, les données sont dépilées dans l'ordre inverse d'empilement. Ainsi, la donnée au sommet de la pile est celle qui a été ajoutée le plus récemment.

Au fait, la pile peut contenir un nombre maximal de stack frames, ce qui peut poser certains problèmes. Si l'on souhaite utiliser plus de stack frames que possible, il se produit un stack overflow, appelé en français débordement de pile. En clair, l'ordinateur plante !


Deux mémoires pour le prix d'une Machines à pile et successeurs

Machines à pile et successeurs

L'organisation de la mémoire et la pile Langage machine et assembleur

Machines à pile et successeurs

De ce qu'on vient de voir, on peut grosso-modo classer nos ordinateurs en deux grandes catégories : les machines à pile, et les machines à accès aléatoire.

Machines à pile

Les machines à pile, aussi appelées stack machines en anglais, utilisent la pile pour stocker les données manipulées par leurs instructions. Sur ces machines, les cadres de pile ne peuvent contenir que des données simples. Par données simples, il faut comprendre données manipulables de base par le processeur, comme des nombres, ou des caractères. Leur taille est donc facile à déterminer : elle est de la taille de la donnée à manipuler.

Exemple avec des entiers de 4 octets.

Image utilisateur

Ces machines ont besoin d'un registre pour fonctionner : il faut bien stocker l'adresse du sommet de la pile. Je vous présente donc le Stack Pointer, qui n'est autre que ce fameux registre qui stocke l'adresse du sommet de la pile. Ce registre seul suffit : nos cadres de pile ayant une taille bien précise, on peut se passer de registre pour stocker leur taille ou leur adresse de début/fin en se débrouillant bien.

Sur certaines machines à pile très simples, la pile n'est pas tout à fait stockée dans une portion de la mémoire : elle est stockée directement dans le processeur. Le processeur contient ainsi un grand nombre de registres, qui seront utilisés comme une pile. Ces registres étant plus rapides que la mémoire principale de l'ordinateur, les opérations manipulant uniquement la pile et ne devant pas manipuler la mémoire seront donc beaucoup plus rapides (les autres instructions étant aussi accélérées, mais moins).

Push Et Pop

Bien évidemment, les données à traiter ne s'empilent pas toutes seules au sommet de la pile. Pour empiler une donnée au sommet de la pile, notre processeur fourni une instruction spécialement dédiée. Cette instruction s'appelle souvent Push. Elle permet de copier une donnée vers le sommet de la pile. Cette donnée peut être aussi bien dans la mémoire statique que dans le tas, peu importe. Cette instruction va prendre l'adresse de la donnée à empiler, et va la stocker sur la pile. Bien évidemment, le contenu du Stack Pointer doit être mis à jour : on doit additionner (ou soustraire, si on fait partir la pile de la fin de la mémoire) la taille de la donnée qu'on vient d'empiler.

Image utilisateur

Bien évidemment, on peut aussi ranger la donnée lacée au sommet de la pile dans la mémoire, à une certaine adresse. Dans ce cas, on utilise l'instruction Pop, qui dépile la donnée au sommet de la pile et la stocke à l'adresse indiquée dans l'instruction. Encore une fois, le Stack Pointer est mis à jour lors de cette opération, en soustrayant (ou additionnant si on fait partir la pile de la fin de la mémoire) la taille de la donnée qu'on vient d'enlever de la pile.

Image utilisateur
Instructions de traitement de données

Sur une machine à pile, les seules données manipulables par une instruction sont celles qui sont placées au sommet de la pile. Pour exécuter une instruction, il faut donc empiler les opérandes une par une, et exécuter l'instruction une fois que les opérandes sont empilées. Le résultat de l'instruction sera sauvegardé au sommet de la pile.

Chose importante : l'instruction dépile automatiquement les opérandes qu'elle utilise. Elle est un peu obligée, sans quoi la gestion de la pile serait horriblement compliquée, et de nombreuses données s'accumuleraient dans la pile durant un bon moment, faute de pouvoir être dépilées rapidement (vu qu'on empile au-dessus). Ce qui signifie qu'on ne peut pas réutiliser plusieurs fois de suite une donnée placée sur la pile : on doit recharger cette donnée à chaque fois. Ceci dit, certaines instructions ont étés inventées pour limiter la casse. On peut notamment citer l'instruction dup, qui copie le sommet de la pile en deux exemplaires.

Image utilisateur

Pour faciliter la vie des programmeurs, le processeur peut aussi fournir d'autres instructions qui peuvent permettre de manipuler la pile ou de modifier son organisation. On peut par exemple citer l'instruction swap, qui échange deux données dans la pile.

Image utilisateur
Avantages et désavantages

Avec une telle architecture, les programmes utilisent peu de mémoire. Les instructions sont très petites : on n'a pas besoin d'utiliser de bits pour indiquer la localisation des données dans la mémoire, sauf pour Pop et Push. Vu que les programmes crées pour les machines à pile sont souvent très petits, on dit que la code density (la densité du code) est bonne. Les machines à pile furent les premières à être inventées et utilisées : dans les débuts de l’informatique, la mémoire était rare et chère, et l'économiser était important. Ces machines à pile permettaient d'économiser de la mémoire facilement, et étaient donc bien vues.

Ces machines n'ont pas besoin d'utiliser beaucoup de registres pour stocker leur état : un Stack Pointer et un Program Counter suffisent. A peine deux registres (avec éventuellement d'autres registres supplémentaires pour faciliter la conception du processeur).

Par contre, une bonne partie des instructions de notre programmes seront des instructions Pop et Push qui ne servent qu'à déplacer des données dans la mémoire de notre ordinateur. Une bonne partie des instructions ne sert donc qu'à manipuler la mémoire, et pas à faire des calculs. Sans compter que notre programme comprendra beaucoup d'instructions comparé aux autres types de processeurs.

Machines à accès aléatoire

Ah ben tient, vu qu'on parle de ces autres types de processeurs, voyons ce qu'ils peuvent bien être ! Déjà, pourquoi avoir inventé autre chose que des machines à pile ? Et bien tout simplement pour supprimer les défauts vus plus haut : l'impossibilité de réutiliser une donnée placée sur la pile, et beaucoup de copies ou recopies de données inutiles en mémoire. Pour éviter cela, les concepteurs de processeurs ont inventé des processeurs plus élaborés, qu'on appelle des machines à accès aléatoire.

Sur ces ordinateurs, les données qu'une instruction de calcul (une instruction ne faisant pas que lire, écrire, ou déplacer des données dans la mémoire) doit manipuler ne sont pas implicitement placée au sommet d'une pile. Avec les machines à pile, on sait où sont placées ces données, implicitement : le Stack Pointer se souvient du sommet de la pile, et on sait alors où sont ces données. Ce n'est plus le cas sur les machines à accès aléatoire : on doit préciser où sont les instructions à manipuler dans la mémoire.

Une instruction doit ainsi fournir ce qu'on appelle une référence, qui va permettre de localiser la donnée à manipuler dans la mémoire. Cette référence pourra ainsi préciser plus ou moins explicitement dans quel registre, à quelle adresse mémoire, à quel endroit sur le disque dur, etc ; se situe la donnée à manipuler. Ces références sont souvent stockées directement dans les instructions qui les utilisent, mais on verra cela en temps voulu dans le chapitre sur le langage machine et l'assembleur.

Cela permet d'éviter d'avoir à copier des données dans une pile, les empiler, et les déplacer avant de les manipuler. Le nombre d'accès à la mémoire est plus faible comparé à une machine à pile. Et cela a son importance : il faut savoir qu'il est difficile de créer des mémoires rapides. Et cela devient de plus en plus problématique : de nos jours, le processeur est beaucoup plus rapide que la mémoire. Il n'est donc pas rare que le processeur doive attendre des données en provenance de la mémoire : c'est ce qu'on appelle le "Von Neumann Bottleneck". C'est pour cela que nos ordinateurs actuels sont des machines à accès aléatoire : pour limiter les accès à la mémoire principale.

On peut aussi signaler que quelques anciennes machines et prototypes de recherche ne sont ni des machines à pile, ni des machines à accès aléatoire. Elles fonctionnent autrement, avec des mémoires spéciales : des content adressables memory. Mais passons : ces architectures sont un peu compliquées, alors autant les passer sur le tapis pour le moment.

Machines à registres

Certaines machines à accès aléatoire assez anciennes ne faisaient que manipuler la mémoire RAM. Les fameuses références mentionnées plus haut étaient donc des adresses mémoires, qui permettaient de préciser la localisation de la donnée à manipuler dans la mémoire principale (la ROM ou la RWM). Pour diminuer encore plus les accès à cette mémoire, les concepteurs d'ordinateurs ont inventés les machines à registres.

Ces machines peuvent stocker des données dans des registres intégrés dans le processeur, au lieu de devoir travailler en mémoire. Pour simplifier, ces registres stockent des données comme la pile le faisait sur les machines à pile. Ces registres vont remplacer la pile, mais d'une manière un peu plus souple : on peut accéder à chacun de ces registres individuellement, alors qu'on ne pouvait qu’accéder à une seule donnée avec la pile (celle qui était au sommet de la pile).

Reste à savoir comment charger nos données à manipuler dans ces registres. Après tout, pour la pile, on disposait des instructions Push et Pop, qui permettaient d'échanger des données entre la pile et la mémoire statique. Sur certains processeurs, on utilise une instruction à tout faire : le mov. Sur d'autres, on utilise des instructions séparées suivant le sens de transfert et la localisation des données (dans un registre ou dans la mémoire). Par exemple, on peut avoir des instructions différentes selon qu'on veuille copier une donnée présente en mémoire dans un registre, copier le contenu d'un registre dans un autre, copier le contenu d'un registre dans la mémoire RAM, etc.

Avantages et inconvénients

L'utilisation de registres est plus souple que l'utilisation d'une pile. Par exemple, une fois qu'une donnée est chargée dans un registre, on peut la réutiliser autant de fois qu'on veut tant qu'on ne l'a pas effacée. Avec une pile, cette donnée aurait automatiquement effacée, dépilée, après utilisation : on aurait du la recharger plusieurs fois de suite. De manière générale, le nombre total d'accès à la mémoire diminue fortement comparé aux machines à pile.

Et on retrouve les mêmes avantages pour les machines à accès aléatoires n'ayant pas de registres, même si c'est dans une moindre mesure. Il faut dire que nos registres sont souvent des mémoires très rapides, bien plus rapides que la mémoire principale. Utiliser des registres est donc une bonne manière de gagner en performances. C'est pour ces raisons que nos ordinateurs actuels sont souvent des machines à accès aléatoires utilisant des registres.

Le seul problème, c'est qu'il faut bien faire de la place pour stocker les références. Comme je l'ai dit, ces références sont placées dans les instructions : elles doivent préciser où sont stockées les données à manipuler. Et cela prend de la place : des bits sont utilisés pour ces références. La code density est donc moins bonne. De nos jours, cela ne pose pas vraiment de problèmes : la taille des programmes n'est pas vraiment un sujet de préoccupation majeur, et on peut s'en accommoder facilement.

Les hybrides

De nos jours, on pourrait croire que les machines à accès aléatoire l'ont emporté. Mais la réalité est plus complexe que çà : nos ordinateurs actuels sont certes des machines à accès aléatoire, mais ils possèdent de quoi gérer une pile. Le seul truc, c'est que cette pile n'est pas une pile simple comme celle qui est utilisée sur une machine à pile : la pile de nos ordinateurs utilise des cadres de pile de taille variable. On peut ainsi mettre ce qu'on veut dans ces cadres de pile, et y mélanger des tas de données hétérogènes.

Image utilisateur

Pour localiser une donnée dans cette Stack Frame, il suffit de la repérer en utilisant un décalage par rapport au début ou la fin de celle-ci. Ainsi, on pourra dire : la donnée que je veux manipuler est placée 8 adresses après le début de la Stack Frame, ou 16 adresses après la fin de celle-ci. On peut donc calculer l'adresse de la donnée à manipuler en additionnant ce décalage avec le contenu du Stack Pointer ou du Frame Pointer. Une fois cette adresse connue, nos instructions vont pouvoir manipuler notre donnée en fournissant comme référence cette fameuse adresse calculée.

Utiliser des piles aussi compliquées a une utilité : sans cela, certaines fonctionnalités de nos langages de programmation actuels n'existeraient pas ! Pour les connaisseurs, cela signifierait qu'on ne pourrait pas utiliser de fonctions réentrantes ou de fonctions récursives. Mais je n'en dis pas plus : vous verrez ce que cela veut dire d'ici quelques chapitres.

Bon, c'est bien beau, mais ces cadres de pile de taille variables, on les délimite comment ?

Pour cela, on a besoin de sauvegarder deux choses : l'adresse à laquelle commence notre Stack Frame en mémoire, et de quoi connaitre l'adresse de fin. Et il existe diverses façons de faire.

Frame Pointer

Pour ce faire, on peut rajouter un registre en plus du Stack Pointer, afin de pouvoir gérer ces cadres de pile. Ce registre s'appelle le Frame Pointer, et sert souvent à dire à quelle adresse commence (ou termine, si on fait grandir notre pile de la fin de la mémoire) la Stack Frame qui est au sommet de la pile. La création d'une Stack Frame se base sur des manipulations de ces deux registres: le Stack Pointer, et le Frame Pointer.

Image utilisateur

Certains processeurs possèdent un registre spécialisé qui sert de Frame Pointer uniquement : on ne peut pas l'utiliser pour autre chose. Si ce n'est pas le cas, on est obligé de stocker ces informations dans deux registres normaux, et se débrouiller avec les registres restants.

Stack Pointer Only

D'autres processeurs arrivent à se passer de Frame Pointer. Ceux-ci n'utilisent pas de registres pour stocker l'adresse de la base de la Stack Frame, mais préfèrent calculer cette adresse à partir de l'adresse de fin de la Stack Frame, et de sa longueur. Pour info, le calcul est une simple addition/soustraction entre la longueur et le contenu du Stack Pointer.

Cette longueur peut être stockée directement dans certaines instructions censées manipuler la pile : si la Stack Frame a toujours la même taille, cette solution est clairement la meilleure. Mais il arrive que notre Stack Frame aie une taille qui ne soit pas constante : dans ce cas, on a deux solutions : soit stocker cette taille dans un registres, soit la stocker dans les instructions qui manipulent la pile, soit utiliser du Self Modifying Code.

Voilà, les bases sont clairement posées : vous avez maintenant un bon aperçu de ce qu'on trouve dans nos ordinateurs. Vous savez ce qu'est un processeur, une mémoire, des bus, et savez plus ou moins comment tout cela est organisé. Vous savez de plus avec quoi sont crées les circuits de notre ordinateurs, surtout pour ce qui est des mémoires. Vous êtes prêt pour la suite.

Dans les chapitres suivants, on va approfondir ces connaissances superficielles, et on va aborder chaque composant (mémoire, processeur, entrées-sorties, bus, etc) uns par uns. Vous saurez comment créer des mémoires complètes, ce qu'il y a dans un processeur, et aurez aussi un aperçu des dernières évolutions technologiques. On peut considérer que c'est maintenant que les choses sérieuses commencent.


L'organisation de la mémoire et la pile Langage machine et assembleur

Langage machine et assembleur

Machines à pile et successeurs Instructions

Dans ce chapitre, on va aborder le langage machine d'un processeur. Le langage machine d'un processeur définit toutes les opérations qu'un programmeur peut effectuer sur notre processeur.
Celui-ci définit notamment :

Au fait : la majorité des concepts qui seront vus dans ce chapitre ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. De plus, ce chapitre sera suivi par un chapitre spécialement dédié aux bases théoriques de la programmation en assembleur : boucles, sous-programmes, et autres. C'est sympa, non ? ^^

Instructions

Langage machine et assembleur Jeux d'instruction

Instructions

Pour rappel, le rôle d'un processeur est d’exécuter des programmes. Un programme informatique est une suite d'instructions à exécuter dans l'ordre. Celles-ci sont placées dans la mémoire programme les unes à la suite des autres dans l'ordre dans lequel elles doivent être exécutées.

C'est quoi une instruction ?

Il existe plusieurs types d'instructions dont voici les principaux :

Instruction

Utilité

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

Les instructions logiques

Elles travaillent sur des bits ou des groupes de bits.
On peut citer :

  • Le ET logique.

  • Le OU logique.

  • Le XOR.

  • Le NON , qui inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. Pour rappel, cela permet de calculer le complément à 1 d'un nombre (rappelez-vous le chapitre sur le binaire)

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

Les instructions de manipulation de chaines de caractères

Certains processeurs intègrent des instructions capables de manipuler ces chaines de caractères directement. Mais autant être franc : ceux-ci sont très rares.

Dans notre ordinateur, une lettre est stockée sous la forme d'un nombre souvent codé sur 1 octet (rappelez-vous le premier chapitre sur la table ASCII). Pour stocker du texte, on utilise souvent ce que l'on appelle des chaines de caractères : ce ne sont rien de plus que des suites 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.

Les instructions de test

Elles peuvent comparer deux nombres entre eux pour savoir si une condition est remplie ou pas.
Pour citer quelques exemples, il existe certaines instructions qui peuvent vérifier si :

  • deux nombres sont égaux ;

  • si deux nombres sont différents ;

  • si un nombre est supérieur à un autre ;

  • si un nombre est inférieur à un autre.

Les instructions de contrôle

Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elle 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.

Les 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 instructions de gestion de l'énergie

Elles permettent de modifier la consommation en électricité de l'ordinateur (instructions de mise en veille du PC, par exemple).

Les inclassables

Il existe une grande quantité d'autres instructions, qui sont fournies par certains processeurs pour des besoins spécifiques.

  • Ainsi, certains processeurs ont des instructions spécialement adaptés aux besoins des OS modernes.

  • Il arrive aussi qu'on puisse trouver des instructions qui permettent à des programmes de partager des données, d'échanger des informations (via Message Passing), etc. 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.

  • Certains processeurs sont aussi capables d'effectuer des instructions sur des structures de données assez complexes, comme des listes chainées ou des arbres.

  • Et on peut trouver bien d'autres exemples...

Ces types d'instructions ne sont pas les seuls : on peut parfaitement trouver d'autres instructions différentes, pour faciliter la création de systèmes d'exploitation, pour manipuler des structures de données plus complexes comme des arbres ou des matrices, etc.

Type des données et instructions

Petite remarque sur les instructions manipulant des nombres (comme les instructions arithmétiques, les décalages, et les tests) : ces instructions dépendent de la représentation utilisée pour ces nombres. La raison est simple : on ne manipule pas de la même façon des nombres signés, des nombres codés en complément à 1, des flottants simple précision, des flottants double précision, etc.

Par exemple, quand on veut faire une addition, on ne traite pas de la même façon un entier ou un flottant. Si vous ne me croyez pas, prenez deux flottants simple précision et additionnez-les comme vous le feriez avec des entiers codés en complément à deux : vous obtiendrez n'importe quoi ! Et c'est pareil pour de nombreuses autres instructions (multiplications, division, etc). On peut se retrouver avec d'autres cas de ce genre, pour lequel le "type" de la donnée sur laquelle on va instructionner est important.

Dans ce cas, le processeur dispose souvent d'une instruction par type à manipuler. On se retrouve donc avec des instructions différentes pour effectuer la même opération mathématique, chacune de ces instructions étant adaptée à une représentation particulière : on peut avoir une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à un, une autre pour les entiers codés en Binary Coded Decimal, etc.

Sur d'anciennes machines, on stockait le type de la donnée (est-ce un flottant, un entier codé en BCD, etc...) dans la mémoire. Chaque nombre, chaque donnée naturellement manipulée par le processeur incorporait un tag, une petite suite de bit qui permettait de préciser son type. Le processeur ne possédait pas d'instruction en plusieurs exemplaires pour faire la même chose, et utilisait le tag pour déduire quoi faire comme manipulation sur notre donnée.

Par exemple, ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Le traitement effectué par cette instruction dépendait du tag incorporé dans la donnée. Des processeurs de ce type s'appellent des Tagged Architectures. De nos jours, ces processeurs n'existent plus que dans quelques muséums : ils ont faits leur temps, laissons-les reposer en paix.

Longueur des données à traiter

La taille des données à manipuler peut elle aussi dépendre de l'instruction. Ainsi, un processeur peut avoir des instructions pour traiter des nombres entiers de 8 bits, et d'autres instructions pour traiter des nombres entiers de 32 bits, par exemple. On peut aussi citer le cas des flottants : il faut bien faire la différence entre flottants simple précision et double précision !

Les tous premiers ordinateurs pouvaient manipuler des données de taille arbitraire : en clair, ils pouvaient manipuler des données aussi grandes qu'on le souhaite sans aucun problème. Alors certes, ces processeurs utilisaient des ruses : ils n'utilisait pas vraiment le binaire qu'on a vu au premier chapitre.

A la place, ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en Binary Coded Decimal (une méthode de représentation des entiers assez proche du décimal), et utilisaient des instructions pouvant manipuler de tels tableaux. Mais de nos jours, cela tend à disparaitre, et les processeurs ne disposent plus d'instructions de ce genre.


Langage machine et assembleur Jeux d'instruction

Jeux d'instruction

Instructions Registres architecturaux

Jeux d'instruction

Au fait, on va mettre les choses au clair tout de suite : certains processeurs peuvent faire des instructions que d'autres ne peuvent pas faire. Ainsi, les instructions exécutables par un processeur dépendent fortement du processeur utilisé. La liste de toute les instructions qu'un processeur peut exécuter s'appelle son jeu d'instruction. Ce jeu d'instruction va définir quelles sont les instructions supportées, ainsi que les suites de bits correspondant à chaque instruction.

RISC vs CISC

Il existe différents jeux d'instructions : le X86 , le PPC, etc. Et tout ces jeux d'instructions ont leurs particularités. Pour s'y retrouver, on a grossièrement classé ces jeux d'instructions en plusieurs catégories. La première classification se base sur le nombre d'instructions et classe nos processeurs en deux catégories :

CISC

CISC est l'acronyme de Complex Instruction Set Computer. Traduit de l'anglais cela signifie Ordinateur à jeu d'instruction complexe. Les processeurs CISC ont un jeu d'instruction étoffé, avec beaucoup d'instructions. De plus, certaines de ces instructions sont assez complexes et permettent de faire des opérations assez évoluées.

Par exemple, ces processeurs peuvent :

Ces jeux d'instructions sont les plus anciens : ils étaient à la mode jusqu'à la fin des années 1980. A cette époque, on programmait rarement avec des langages de haut niveau et beaucoup de programmeurs devaient utiliser l'assembleur. Avoir un jeu d'instruction complexe, avec des instructions de "haut niveau" qu'on ne devait pas refaire à partir d'instructions plus simples, était un gros avantage : cela facilitait la vie des programmeurs.

Cette complexité des jeux d'instructions n'a pas que des avantages "humains", mais a aussi quelques avantages techniques. Il n'est pas rare qu'une grosse instruction complexe puisse remplacer une suite d'instructions plus élémentaires.

Cela a quelques effets plutôt bénéfiques :

Vu qu'un programme écrit pour des processeurs CISC utilise moins d'instructions, il prendra donc moins de place en mémoire programme. A l'époque des processeurs CISC, la mémoire était rare et chère, ce qui faisait que les ordinateurs n'avaient pas plusieurs gigaoctets de mémoire : économiser celle-ci était crucial.

Mais ces avantages ne sont pas sans contreparties :

L'agence tout RISC

Au fil du temps, on s'est demandé si les instructions complexes des processeurs CISC étaient vraiment utiles. Pour le programmeur qui écrit ses programmes en assembleur, elle le sont. Mais avec l'invention des langages de haut niveau, la roue a commencée à tourner. Diverses analyses ont alors étés effectuées par IBM, DEC et quelques chercheurs, visant à évaluer les instructions réellement utilisées par les compilateurs. Et à l'époque, les compilateurs n'utilisaient pas la totalité des instructions fournies par un processeur. Nombre de ces instructions n'étaient utilisées que dans de rares cas, voire jamais. Autant dire que beaucoup de transistors étaient gâchés à rien !

L'idée de créer des processeurs possédant des jeux d'instructions simples et contenant un nombre limité d'instructions très rapides commença à germer. Ces processeurs sont de nos jours appelés des processeurs RISC. RISC est l'acronyme de Reduced Instruction Set Computer. Traduit de l'anglais cela signifie Ordinateur à jeu d'instruction réduit.

Mais de tels processeurs RISC, complètement opposés aux processeurs CISC, durent attendre un peu avant de percer. Par exemple, IBM décida de créer un processeur possédant un jeu d'instruction plus sobre, l'IBM 801, qui fût un véritable échec commercial. Mais la relève ne se fit pas attendre. C'est dans les années 1980 que les processeurs possédant un jeu d'instruction simple devinrent à la mode. Cette année là, un scientifique de l'université de Berkeley décida de créer un processeur possédant un jeu d'instruction contenant seulement un nombre réduit d'instructions simples, possédant une architecture particulière. Ce processeur était assez novateur et incorporait de nombreuses améliorations qu'on retrouve encore dans nos processeurs haute performances actuels, ce qui fit son succès : les processeurs RISC étaient nés.

Comme ce qui a été dit plus haut, un processeur RISC n'a pas besoin de cabler beaucoup d'instructions, ce qui a certains effets assez bénéfiques :

Mais par contre, cela a aussi quelques désavantages :

Qui est le vainqueur ?

Durant longtemps, les CISC et les RISC eurent chacun leurs admirateurs et leurs détracteurs. De longs et interminables débats eurent lieu pour savoir si les CISC étaient meilleurs que les RISC, similaires aux "Windows versus Linux", ou "C versus C++", qu'on trouve sur n'importe quel forum digne de ce nom. Au final, on ne peut pas dire qu'un processeur CISC sera meilleur qu'un RISC ou l'inverse : chacun a des avantages et des inconvénients, qui rendent le RISC/CISC adapté ou pas selon la situation.

Par exemple, on mettra souvent un processeur RISC dans un système embarqué, devant consommer très peu. Par contre, le CISC semble mieux adapté dans certaines conditions, en raison de la taille plus faible des programmes, ou quand les programmes peuvent faire un bon usage des instructions complexes du processeur.

Au final, tout dépend d'un tas de paramètres :

Tout ces paramètres jouent beaucoup dans la façon dont on pourra tirer au mieux parti d'un processeur RISC ou CISC, et ils sont bien plus importants que le fait que le processeur soit un RISC ou un CISC.

De plus, de nos jours, les différences entre CISC et RISC commencent à s'estomper. Les processeurs actuels sont de plus en plus difficiles à ranger dans des catégories précises. Les processeurs actuels sont conçus d'une façon plus pragmatiques : au lieu de respecter à la lettre les principes du RISC et du CISC, on préfère intégrer les techniques et instructions qui fonctionnent, peut importe qu'elles viennent de processeurs purement RISC ou CISC. Les anciens processeurs RISC se sont ainsi garnis d'instructions et techniques de plus en plus complexes et les processeurs CISC ont intégré des techniques provenant des processeurs RISC (pipeline, etc). Au final, cette guerre RISC ou CISC n'a plus vraiment de sens de nos jours.

Jeux d'instructions spécialisés

En parallèle de ces architectures CISC et RISC, qui sont en quelques sorte la base de tous les jeux d'instructions, d'autres classes de jeux d'instructions sont apparus, assez différents des jeux d’instructions RISC et CISC. On peut par exemple citer le Very Long Instruction Word, qui sera abordé dans les chapitre à la fin du tutoriel. La plupart de ces jeux d'instructions sont implantés dans des processeurs spécialisés, qu'on fabrique pour une utilisation particulière. Ce peut être pour un langage de programmation particulier, pour des applications destinées à un marche de niche comme les supercalculateurs, etc.

Les DSP

Parmi ces jeux d'instructions spécialisés, on peut citer les fameux jeux d'instructions Digital Signal Processor, aussi appelés des DSP. Ces DSP sont des processeurs chargés de faire des calculs sur de la vidéo, du son, ou tout autre signal. Dès que vous avez besoin de traiter du son ou de la vidéo, vous avez un DSP quelque part, que ce soit une carte son ou une platine DVD.

Ces DSP ont souvent un jeu d'instruction similaire aux jeux d'instructions RISC, avec peu d'instructions, toutes spécialisées pour faire du traitement de signal. On peut par exemple citer l'instruction phare de ces DSP, l'instruction MAD (qui multiplie deux nombres et additionne un 3éme au résultat de la multiplication). De nombreux algorithmes de traitement du signal (filtres FIR, transformées de Fourier) utilisent massivement cette opération. Ces DSP possèdent aussi des instructions permettant de faire répéter rapidement une suite d'instruction (pour les connaisseurs, ces instructions permettent de créer des boucles), ou des instructions capables de traiter plusieurs données en parallèle (en même temps).

Ces instructions manipulent le plus souvent des nombres entiers, et parfois (plus rarement) des nombres flottants. Ceci dit, ces DSP utilisent souvent des nombres flottants assez particuliers qui n'ont rien à voir avec les nombres flottants que l'on a vu dans le premier chapitre. Il supportent aussi des formats de nombre entiers assez exotiques, même si c'est assez rare.

Ces DSP ont souvent une architecture de type Harvard. Pour rappel, cela signifie qu'ils sont connectés à deux bus de données : un pour les instructions du programme, et un autre relié à la mémoire de travail, pour les données. Certains DSP vont même plus loin : ils sont reliés à plusieurs bus mémoire. Au bout de ces bus mémoire, on retrouve souvent plusieurs mémoires séparées. Nos DSP sont donc capables de lire et/ou d'écrire plusieurs données simultanément : une par bus mémoire relié au DSP.

Il y a pire

On peut aussi citer les jeux d'instructions de certains processeurs particulièrement adaptés à un système d'exploitation en particulier. Un exemple serait les processeurs multics, spécialement dédiés au système d'exploitation du même nom. Il faut avouer que ces processeurs sont assez rares et dédiés à des marchés de niche.

Dans le même genre, certains processeurs sont spécialement conçus pour un langage en particulier. Il existe ainsi des processeurs possédant des instructions permettant d’accélérer le traitement des opérations de base fournies par un langage de programmation, ou encore d'implémenter celle-ci directement dans le jeu d'instruction du processeur, transformant ainsi ce langage de haut niveau en assembleur. On appelle de tels processeurs, des processeurs dédiés.

Historiquement, les premiers processeurs de ce type étaient des processeurs dédiés au langage LISP, un vieux langage fonctionnel autrefois utilisé, mais aujourd'hui peu usité. De tels processeurs datent des années 1970 et étaient utilisés dans ce qu'on appelait des machines LISP. Ces machines LISP é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. D'autres langages fonctionnels ont aussi eu droit à leurs processeurs dédiés : le prolog en est un bel exemple.

Autre langage qui a eu l'honneur d'avoir ses propres processeurs dédiés : le FORTH, un des premiers langages à pile de haut niveau. Ce langage 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.

En regardant dans les langages de programmation un peu plus connus, on peut aussi citer des processeurs spécialisés pour JAVA, qui intègrent une machine virtuelle JAVA directement dans leurs circuits : de quoi exécuter nativement du bytecode ! Certains processeurs ARM, qu'on trouve dans des système embarqués, sont de ce type.

Et pour nos ordinateurs ?

Le jeu d'instruction de nos PC qui fonctionnent sous Windows est appelé le x86. C'est un jeu d'instructions particulièrement ancien, apparu certainement avant votre naissance : 1978. Depuis, de plus en plus d'instructions ont été ajoutées et rajoutées : ces instructions sont ce qu'on appelle des extensions x86. On peut citer par exemple les extensions MMX, SSE, SSE2, voir 3dnow!. Le résultat, c'est que les processeurs x86 sont de type CISC, avec tous les inconvénients que cela apporte.

Les anciens macintoshs (la génération de macintosh produits entre 1994 et 2006) utilisaient un jeu d'instruction différent : le PowerPC. Celui-ci était un jeu d'instruction de type RISC. Depuis 2006, les macintoshs utilisent un processeur X86.

Mais les architectures x86 et Power PC ne sont pas les seules au monde : il existe d'autres types d'architectures qui sont très utilisées dans le monde de l’informatique embarquée et dans tout ce qui est tablettes et téléphones portables derniers cris. On peut citer notamment l'architecture ARM, qui domine ce marché. Et n'oublions pas leurs consœurs MIPS et SPARC.


Instructions Registres architecturaux

Registres architecturaux

Jeux d'instruction Représentation en binaire

Registres architecturaux

Nos instructions manipulent donc des données, qui sont forcément stockées quelques part dans la mémoire de notre ordinateur. En plus d'avoir accès aux données placées dans la mémoire RAM, le processeur possède plusieurs mémoires internes très rapides qui peuvent stocker très peu de données : des registres. Ces registres servent à stocker temporairement des informations dont le processeur peut avoir besoin, aussi bien instructions, adresses ou données. Il s'agit bien des registres vus dans les chapitres précédents, fabriqués avec des bascules.

Mais pourquoi utiliser des registres pour stocker des données alors que l'on a déjà une mémoire RAM ?

C'est très simple : la mémoire RAM est une mémoire assez lente, et y accéder en permanence rendrait notre ordinateur vraiment trop lent pour être utilisable. Par contre, les registres sont des mémoires vraiment très rapides. En stockant temporairement des données dans ces registres, le processeur pourra alors manipuler celle-ci très rapidement, sans avoir à attendre une mémoire RAM à la lenteur pachydermique. Typiquement, dès qu'une donnée doit être lue ou modifiée plusieurs fois de suite, on a tout à gagner à la mettre dans un registre.

A quoi servent ces registres ?

On peut se demander à quoi servent ces registres. Tout cela dépend du processeur, et tous nos processeurs ne gèrent pas ces registres de la même façon.

Registres spécialisés

Certains processeurs disposent de registres spécialisés, qui ont une utilité bien précise. Leur fonction est ainsi prédéterminée une bonne fois pour toute. Le contenu de nos registres est aussi fixé une bonne fois pour toute : un registre est conçu pour stocker soit des nombres entiers, des flottants, des adresses, etc; mais pas autre chose. Pour donner quelques exemples, voici quelques registres spécialisés qu'on peut trouver sur pas mal de processeurs.

Registre

Utilité

Le registre d'adresse d'instruction

Pour rappel, un processeur doit effectuer une suite d'instructions dans un ordre bien précis. Dans ces conditions, il est évident que notre processeur doit se souvenir où il est dans le programme, quelle est la prochaine instruction à exécuter : notre processeur doit donc contenir une mémoire qui stocke cette information. C'est le rôle du registre d'adresse d'instruction.

Ce registre stocke l'adresse de la prochaine instruction à exécuter. Cette adresse permet de localiser l'instruction suivante en mémoire. Cette adresse ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples qu'on verra dans la suite de ce tutoriel. Cela peut aller d'une simple addition à quelque chose d'un tout petit peu plus complexe. Quoiqu'il en soit, elle est calculée par un petit circuit combinatoire couplé à notre registre d'adresse d'instruction, qu'on appelle le compteur ordinal.

Ce registre d'adresse d'instruction est souvent appelé le Program Counter. Retenez bien ce terme, et ne l'oubliez pas si vous voulez lire des documentations en anglais.

Le registre d'état

Le registre d'état contient plusieurs bits qui ont chacun une utilité particulière. Ce registre est très différent suivant les processeurs, mais certains bits reviennent souvent :

  • divers bits utilisés lors d'opérations de comparaisons ou de tests qui servent à donner le résultat de celles-ci ;

  • le bit d'overflow, qui prévient quand le résultat d'une instruction est trop grand pour tenir dans un registre ;

  • le bit null : précise que le résultat d'une instruction est nul (vaut zéro) ;

  • le bit de retenue, utile pour les additions ;

  • le bit de signe, qui permet de dire si le résultat d'une instruction est un nombres négatif ou positif.

Le Stack Pointer, et éventuellement le Frame Pointer

Ces deux registres sont utilisés pour gérer une pile, si le processeur en possède une. Pour ceux qui auraient oubliés ce qu'est la pile, le chapitre 5 est là pour vous.

Pour rappel, le Stack Pointer stocke l'adresse du sommet de la pile. Tout processeur qui possède une pile en possède un. Par contre, le Frame Pointer est optionnel : il n'est présent que sur les processeurs qui gèrent des Stack Frames de taille variable. Ce registre stocke l'adresse à laquelle commence la Stack Frame située au sommet de la pile.

Registres entiers

Certains registres sont spécialement conçus pour stocker des nombres entiers. On peut ainsi effectuer des instructions de calculs, des opérations logiques dessus.

Registres flottants

Certains registres sont spécialement conçus pour stocker des nombres flottants. L’intérêt de placer les nombres flottants à part des nombres entiers, dans des registres différents peut se justifier par une remarque très simple : on ne calcule pas de la même façon avec des nombres flottants et avec des nombres entiers. La façon de gérer les nombres flottants par nos instructions étant différente de celle des entiers, certains processeurs placent les nombres flottants à part, dans des registres séparés.
On peut ainsi effectuer des instructions de calculs, des opérations logiques dessus.

Registres de constante

Ces registres de constante contiennent des constantes assez souvent utilisées. Par exemple, certains processeurs possèdent des registres initialisés à zéro pour accélérer la comparaison avec zéro ou l'initialisation d'une variable à zéro. On peut aussi citer certains registres flottants qui stockent des nombres comme \pi, ou e pour faciliter l'implémentation des calculs trigonométriques).

Registres d'Index

Autrefois, nos processeurs possédaient des registres d'Index, qui servait à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ces registres d'Index étaient utilisés pour effectuer des manipulations arithmétiques sur des adresses. Sans eux, accéder à des données placées à des adresses mémoires consécutives nécessitait souvent d'utiliser du self-modifying code : le programme devait être conçu pour se modifier lui-même en partie, ce qui n'était pas forcément idéal pour le programmeur.

Registres généraux

Malheureusement, fournir des registres très spécialisés n'est pas très flexible. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers, de 4 registres d'Index pour calculer des adresses, et de 4 registres flottants. Si jamais j’exécute un morceau de programme qui manipule beaucoup de nombres entiers, mais qui ne manipule pas d'adresses ou de nombre flottants, j'utiliserais juste les 4 registres entiers. Une partie des registres du processeur sera inutilisé : tous les registres flottants et d'Index. Le problème vient juste du fait que ces registres ont une fonction bien fixée.

Pourtant, en réfléchissant, un registre est un registre, et il ne fait que stocker une suite de bits. Il peut tout stocker : adresses, flottants, entiers, etc. Pour plus de flexibilité, certains processeurs ne fournissent pas de registres spécialisés comme des registres entiers ou flottants, mais fournissent à la place des Les registres généraux utilisables pour tout et n'importe quoi. Ce sont des registres qui n'ont pas d'utilité particulière et qui peuvent stocker toute sorte d’information codée en binaire. Pour reprendre notre exemple du dessus, un processeur avec des registres généraux fournira un Program Counter et 12 registres généraux, qu'on peut utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 12 entiers, 10 entiers et 2 flottants, 7 adresses et 5 entiers, etc. Ce qui sera plus flexible et permettra de mieux utiliser les registres.

Dans la réalité, nos processeurs utilisent souvent un espèce de mélange entre les deux solutions. Généralement, une bonne partie des registres du processeur sont des registres généraux, à part quelques registres spécialisés, accessibles seulement à travers quelques instructions bien choisies. C'est le cas du registre d'adresse d'instruction, qui est manipulé automatiquement par le processeur et par les instructions de branchement.

La catastrophe

Ceci dit, certains processeurs sont très laxistes : tous les registres sont des registres généraux, même le Program Counter. Sur ces processeurs, on peut parfaitement lire ou écrire dans le Program Counter sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur notre Program Counter, on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans notre 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.

Registres architecturaux

Un programmeur (ou un compilateur) qui souhaite programmer en langage machine peut manipuler ces registres. A ce stade, il faut faire une petite remarque : tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il existe ainsi deux types de registres : les registres architecturaux, manipulables par des instructions, et d'autres registres internes au processeurs. Ces registres peuvent servir à simplifier la conception du processeur ou à permettre l'implémentation d'optimisations permettant de rendre notre ordinateur plus rapide.

Le nombre de registres architecturaux varie suivant le processeur. Généralement, les processeurs RISC et les DSP possèdent un grand nombre de registres. Sur les processeurs CISC, c'est l'inverse : il est rare d'avoir un grand nombre de registres architecturaux manipulables par un programme. Quoiqu'il en soit, tous les registres cités plus haut sont des registres architecturaux.

Ça doit être du sport pour se retrouver dans un processeur avec tout ces registres ! Comment notre programmeur fait-il pour sélectionner un registre parmi tous les autres ?

Et bien rassurez-vous, les concepteurs de processeurs ont trouvé des solutions.

Registres non référencables

Certains registres n'ont pas besoin d'être sélectionnées. On les manipule implicitement avec certaines instructions. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. C'est le cas pour le Program Counter : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé implicitement par les instructions de comparaisons et de test, et certaines opérations arithmétiques.

Noms de registres

Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse bien précise. Et bien pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder.

Cet identifiant est ce qu'on appelle un nom de registre. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Ce numéro, ou nom de registre, permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur : ce nom de registre, ce numéro, ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire.

Image utilisateur

Toutefois, tous les registres n'ont pas forcément besoin d'avoir un nom. Par exemple, les registres chargés de gérer la pile n'ont pas forcément besoin d'un nom : la gestion de la pile se fait alors via des instructions Push et Pop qui sont les seules à pouvoir manipuler ces registres. Toute manipulation du Frame Pointer et du Stack Pointer se faisant grâce à ces instructions, on n'a pas besoin de leur fournir un identifiant pour pouvoir les sélectionner. C'est aussi le cas du registre d'adresse d'instruction : sur certains processeurs, il est manipulé automatiquement par le processeur et par les instructions de branchement. 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. Quand on effectue un branchement, le processeur sait qu'il doit modifier le Program Counter : pas besoin de lui dire. Pareil pour les instructions de gestion de la pile.

Ces noms de registres posent un petit problème. Quand une instruction voudra manipuler des données, elle devra fatalement donner une adresse ou un nom de registres qui indiquera la position de la donnée en mémoire. Ces adresses et noms de registres sont codés sous la forme de suites de bits, incorporées dans l'instruction. Mais rien ne ressemble plus à une suite de bits qu'une autre suite de bits : notre processeur devra éviter de confondre suite de bits représentant une adresse, et suite de bits représentant un nom de registre. Pour éviter les confusions, chaque instruction devra préciser à quoi correspondra la suite de bits précisant la localisation des données à manipuler : est-ce un registres ou une adresse, ou autre chose encore. Cette précision (cet-ce une adresse ou un nom de registres) sera indiquée par ce qu'on appelle un mode d'adressage. Nous reviendront dessus tout à l'heure.

Registres adressables

Mais il existe une autre solution, assez peu utilisée. Sur certains processeurs assez rares, on peut adresser les registres via une adresse mémoire. Il est vrai que c'est assez rare, et qu'à part quelques vielles architectures ou quelques micro-contrôleurs, je n'ai pas d'exemples à donner. Mais c'est tout à fait possible ! C'est le cas du PDP-10.

Image utilisateur
8, 16, 32, 64 bits : une histoire de taille des registres

Vous avez déjà entendu parler de processeurs 32 ou 64 bits ?

Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple. Il s'agit de la quantité de bits qui peuvent être stockés dans chaque registre généraux.

Attention : on parle bien des registres généraux, et pas forcément des autres registres. Notre processeur contient pas mal de registres et certains peuvent contenir plus de bits que d'autres. Par exemple, dans certains processeurs, les registres généraux sont séparés des registres stockant des flottants et ces deux types de registres peuvent avoir une taille différente. Exemple : dans les processeurs x86, il existe des registres spécialement dédiés aux nombres flottants et d'autres spécialement dédiés aux nombres entiers (ce sont les registres généraux qui servent pour les entiers). Les registres pour nombres entiers n'ont pas la même taille que les registres dédiés aux nombres flottants. Un registre pour les nombres entiers contient environ 32 bits tandis qu'un registre pour nombres flottants contient 80 bits.

Ce nombre de bits que peut contenir un registre est parfois différent du nombre de bits qui peuvent transiter en même temps sur le bus de donnée de votre ordinateur. Cette quantité peut varier suivant l'ordinateur. On l'appelle la largeur du bus de données. Exemple : sur les processeurs x 86 - 32 bits, un registre stockant un entier fait 32bits. Un registre pour les flottants en fait généralement 64. Le bus de donnée de ce genre d'ordinateur peut contenir 64 bits en même temps. Cela a une petite incidence sur la façon dont une donnée est transférée entre la mémoire et un registre. On peut donc se retrouver dans deux situations différentes :

Situation

Conséquence

Le bus de données a une largeur égale à la taille d'un registre

Le bus de donnée peut charger en une seule fois le nombre de bits que peut contenir un registre.

La largeur du bus de donnée est plus petite que la taille d'un registre

On ne peut pas charger le contenu d'un registre en une fois, et on doit charger ce contenu morceau par morceau.


Jeux d'instruction Représentation en binaire

Représentation en binaire

Registres architecturaux Classes d'architectures

Représentation en binaire

On peut de demander comment notre ordinateur fait pour stocker ces instructions dans sa mémoire. On a déjà vu il y a quelques chapitres que les instructions sont stockées dans la mémoire programme de l'ordinateur sous la forme de suites de bits.

Exemple : ici, les valeurs binaires sont complètement fictives.

Instruction

Valeur Binaire

Ne rien faire durant un cycle d'horloge : NOP

1001 0000

Mise en veille : HALT

0110 1111

Addition : ADD

0000 0000, ou 0000 0001, ou 1000 0000, etc...

Écriture en mémoire : STORE

1111 1100, ou 1111 1101, ou 1111 1110, etc...

Mais j'ai volontairement passé sous silence quelque chose : cette suite de bits n'est pas organisée n'importe comment.

Opcode

La suite de bits de notre instruction contient une portion qui permet d'identifier l'instruction en question. Cette partie permet ainsi de dire s'il s'agit d'une instruction d'addition, de soustraction, d'un branchement inconditionnel, d'un appel de fonction, d'une lecture en mémoire, etc. Cette portion de mémoire s'appelle l'opcode.

Image utilisateur

Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilité. Ce qui fait que pour chaque processeur, ses fabricants donnent une liste qui recense l'intégralité des instructions et de leur opcode : l'opcode map.

Petit détail : il existe certains processeurs qui utilisent une seule et unique instruction. Ces processeurs peuvent donc se passer d'opcode : avec une seule instruction possible, pas besoin d'avoir un opcode pour préciser quelle instruction exécuter. Mais autant prévenir : ces processeurs sont totalement tordus et sont vraiment très rares. Inutile de s'attarder plus longtemps sur ces processeurs.

Opérandes

Il arrive que certaines instructions soient composées d'un Opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. Mais certaines instructions ne se contentent pas d'un opcode : elles utilisent une partie variable. Cette partie variable peut permettre de donner des informations au processeur sur l'instruction, sur ses données, ou permettre d’autres choses encore. Mais le plus fréquemment, cette partie variable permet de préciser quelles sont les données à manipuler. Sans cela, rien ne marche !

Quand je dis "préciser quelles sont les données à manipuler", cela veut vouloir dire plusieurs choses. On peut parfois mettre la donnée directement dans l'instruction : si la donnée est une constante, on peut la placer directement dans l'instruction. Mais dans les autres cas, notre instruction va devoir préciser la localisation des données à manipuler : est-ce que la donnée à manipuler est dans un registre (et si oui, lequel), dans la mémoire (et à quelle adresse ?). De même, où enregistrer le résultat ? Bref, cette partie variable est bien remplie.

Modes d'adressage

Reste à savoir comment interpréter cette partie variable : après tout, c'est une simple suite de bits qui peut représenter une adresse, un nombre, un nom de registre, etc. Il existe diverses façons pour cela : chacune de ces façon va permettre d’interpréter le contenu de la partie variable comme étant une adresse, une constante, un nom de registre, etc, ce qui nous permettra de localiser la ou les donnée de notre instruction. Ces diverses manières d’interpréter notre partie variable pour en exploiter son contenu s'appellent des modes d'adressage. Pour résumer, ce mode d'adressage est une sorte de recette de cuisine capable de dire où se trouve la ou les données nécessaires pour exécuter une instruction. De plus, notre mode d'adressage peut aussi préciser où stocker le résultat de l'instruction.

Ces modes d'adressage dépendent fortement de l'instruction qu'on veut faire exécuter et du processeur. Certaines instructions supportent certains modes d'adressage et pas d'autres, voir mixent plusieurs modes d'adressages : les instructions manipulant plusieurs données peuvent parfois utiliser un mode d'adressage différent pour chaque donnée. Dans de tels cas, tout se passe comme si l'instruction avait plusieurs parties variables, nommées opérandes, contenant chacune soit une adresse, une donnée ou un registre. Pour comprendre un peu mieux ce qu'est un mode d'adressage, voyons quelques exemples de modes d'adressages assez communs et qui reviennent souvent.

Je vais donc parler des modes d'adressages suivants :

Adressage implicite

Avec l'adressage implicite, la partie variable n'existe pas ! Il peut y avoir plusieurs raisons à cela. Il se peut que l'instruction n'aie pas besoin de données : une instruction de mise en veille de l'ordinateur, par exemple. Ensuite, certaines instructions n'ont pas besoin qu'on leur donne la localisation des données d'entrée et "savent" où est la ou les donnée(s). Comme exemple, on pourrait citer une instruction qui met tous les bits du registre d'état à zéro. Certaines instructions manipulant la pile sont adressées de cette manière : on connait d'avance l'adresse de la base ou du sommet de la pile. Pour rappel, celle-ci est stockée dans quelques registres du processeur.

Adressage immédiat

Avec l'adressage immédiat, la partie variable est une constante. Celle-ci peut être un nombre, un caractère, un nombre flottant, etc. Avec ce mode d'adressage, notre donnée est chargée en même temps que l'instruction et est placée dans la partie variable.

Image utilisateur
Adressage direct

Passons maintenant à l'adressage absolu, aussi appelé adressage direct. Avec lui, la partie variable est l'adresse de la donnée à laquelle accéder.

Image utilisateur

Cela permet parfois de lire une donnée directement depuis la mémoire sans devoir la copier dans un registre.

Image utilisateur

Ce mode d'adressage ne sert que pour les données dont l'adresse est fixée une bonne fois pour toute. Les seules données qui respectent cette condition sont les données placées dans la mémoire statique (souvenez-vous du chapitre précédent : on avait parlé des mémoires programme, statique, de la pile et du tas). Pour les programmeurs, cela correspond aux variables globales et aux variables statiques, ainsi qu'à certaines constantes (les chaines de caractères constantes, par exemple). Bien peu de données sont stockées dans cette mémoire statique, ce qui fait que ce mode d'adressage a tendance à devenir de plus en plus marginal.

Adressage inhérent

Avec le mode d'adressage inhérent, la partie variable va identifier un registre qui contient la donnée voulue.

Image utilisateur

Mais identifier un registre peut se faire de différentes façons. On peut soit utiliser des noms de registres, ou encore identifier nos registres par des adresses mémoires. Le mode d'adressage inhérent n'utilise que des noms de registres.

Image utilisateur
Adressage indirect à registre

Dans certains cas, les registres généraux du processeur peuvent stocker des adresses mémoire. Après tout, une adresse n'est rien d'autre qu'un nombre entier ayant une signification spéciale, et utiliser un registre censé stocker des nombres entiers pour stocker une adresse n'a rien de choquant. Ces adresses sont alors manipulables comme des données, et on peut leur faire subir quelques manipulations arithmétiques, comme des soustractions et des additions.

On peut alors décider à un moment ou un autre d'accéder au contenu de l'adresse stockée dans un registre : c'est le rôle du mode d'adressage indirect à registre. Ici, la partie variable permet d'identifier un registre contenant l'adresse de la donnée voulue.

Image utilisateur

Si on regarde uniquement l'instruction telle qu'elle est en mémoire, on ne voit aucune différence avec le mode d'adressage inhérent vu juste au-dessus. La différence viendra de ce qu'on fait de ce nom de registre : le nom de registre n'est pas interprété de la même manière. Avec le mode d'adressage inhérent, le registre indiqué dans l'instruction contiendra la donnée à manipuler. Avec le mode d'adressage indirect à registre, la donnée sera placée en mémoire, et le registre contiendra l'adresse de la donnée.

Image utilisateur

Le mode d'adressage indirect à registre permet d'implémenter de façon simple ce qu'on appelle les pointeurs. Au début de l'informatique, les processeurs ne possédaient pas d'instructions ou de modes d'adressages pour gérer les pointeurs. On pouvait quand même gérer ceux-ci, en utilisant l'adressage direct. Mais dans certains cas, forçait l'utilisation de self-modifying code, c'est à dire que le programme devait contenir des instructions qui devaient modifier certaines instructions avant de les exécuter ! En clair, le programme devait se modifier tout seul pour faire ce qu'il faut. L'invention de ce mode d'adressage a permit de faciliter le tout : plus besoin de self-modifying code.

Pour donner un exemple, on peut citer l'exemple des tableaux. Un tableau est un ensemble de données de même taille rangées les unes à la suite des autres en mémoire.

Image utilisateur

Exemple avec un tableau d'entiers prenant chacun 8 octets.

Stocker des données dans un tableau ne sert à rien si on ne peut pas les manipuler : le processeur doit connaitre l'adresse de l’élément qu'on veut lire ou écrire pour y accéder. Cette adresse peut se calculer assez simplement, en connaissant l'adresse du début du tableau, la longueur de chaque élément, ainsi que du numéro de l’élément dans notre tableau. Le seul problème, c'est qu'une fois calculée, notre adresse se retrouve dans un registre, et qu'il faut trouver un moyen pour y accéder. Et c'est là que le mode d'adressage indirect à registre intervient : une fois que l'adresse est calculée, elle est forcément stockée dans un registre : le mode d'adressage indirect à registre permet d’accéder à cette adresse directement. Sans ce mode d'adressage, on serait obligé d'utiliser une instruction utilisant le mode d'adressage direct, et de modifier l'adresse incorporée dans l'instruction avec du self-modifying code. Imaginez l'horreur.

Register Indirect Autoincrement/Autodecrement

Ce mode d'adressage existe aussi avec une variante : l'instruction peut automatiquement augmenter ou diminuer le contenu du registre d'une valeur fixe. Cela permet de passer directement à l’élément suivant ou précédent dans un tableau. Ce mode d'adressage a été inventé afin de faciliter le parcourt des tableaux. Il n'est pas rare qu'un programmeur aie besoin de traiter tous les éléments d'un tableau. Pour cela, il utilise une suite d'instructions qu'il répète sur tous les éléments : il commence par traiter le premier, passe au suivant, et continue ainsi de suite jusqu’au dernier. Ces modes d'adressage permettent d’accélérer ces parcourt en incrémentant ou décrémentant l'adresse lors de l'accès à notre élément.

Image utilisateur
Indexed Absolute

D'autres modes d'adressage permettent de faciliter la manipulations des tableaux. Ces modes d'adressage permettent de faciliter le calcul de l'adresse d'un élément du tableau. Reste à savoir comment ce calcul d'adresse est fait. Sachez que pour cela, chaque élément d'un tableau reçoit un nombre, un indice, qui détermine sa place dans le tableau : l’élément d'indice 0 est celui qui est placé au début du tableau, celui d'indice 1 est celui qui le suit immédiatement après dans la mémoire, etc. On doit donc calculer son adresse à partir de l'indice et d'autres informations. Pour cela, on utilise le fait que les éléments d'un tableau ont une taille fixe et sont rangés dans des adresses mémoires consécutives.

Prenons un exemple : un tableau d'entiers, prenant chacun 4 octets. Le premier élément d'indice zéro est placé à l'adresse A : c'est l'adresse à laquelle commence le tableau en mémoire. Le second élément est placé 4 octets après (vu que le premier élément prend 4 octets) : son adresse est donc A+4. Le second élément est placé 4 octets après le premier élément, ce qui donne l'adresse (A+4) + 4.

Si vous continuez ce petit jeu pour quelques valeurs, on obtiendrait quelque chose dans le genre :

Indice i

Adresse de l'élèment

0

A

1

A+4

2

A+8

3

A+12

4

A+16

5

A+20

...

...

Vous remarquerez surement quelque chose sur l'adresse de l'élément d'indice i, si vous vous souvenez que l'entier de notre exemple fait 4 octets.

Indice i

Adresse de l'élèment

0

A + (0 * 4)

1

A + (1 * 4)

2

A + (2 * 4)

3

A + (3 * 4)

4

A + (4 * 4)

5

A + (5 * 4)

...

...

On peut formaliser cette remarque mathématiquement en posant L la longueur d'un élément du tableau, i l'indice de cet élément, et A l'adresse de début du tableau (l'adresse de l’élément d'indice zéro).

l'adresse de l’élément d'indice i vaut toujours A + L imes i.

Pour éviter d'avoir à calculer les adresses à la main avec le mode d'adressage register indirect, on a inventé un mode d'adressage pour combler ce manque : le mode d'adressage Indexed Absolute.

Celui-ci fournit l'adresse de base du tableau, et un registre qui contient l'indice.

Image utilisateur

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

Image utilisateur
Base plus index

Le mode d'adressage Indexed Absolute vu plus haut ne marche que pour des tableaux dont l'adresse est fixée une bonne fois pour toute. Ces tableaux sont assez rare : ils correspondent aux tableaux de taille fixe, déclarée dans la mémoire statique (souvenez-vous du chapitre précédent). Et croyez moi, ces tableaux ne forment pas la majorité de l’espèce. La majorité des tableaux sont des tableaux dont l'adresse n'est pas connue lors de la création du programme : ils sont déclarés sur la pile ou dans le tas, et leur adresse varie à chaque exécution du programme. On peut certes régler ce problème en utilisant du self-modifying code, mais ce serait vendre son âme au diable !

Pour contourner les limitations du mode d'adressage Indexed Absolute, on a inventé le mode d'adressage Base plus index. Avec ce dernier, l'adresse du début du tableau n'est pas stockée dans l'instruction elle-même, mais dans un registre. Elle peut donc varier autant qu'on veut.

Ce mode d'adressage spécifie deux registres dans sa partie variable : un registre qui contient l'adresse de départ du tableau en mémoire : le registre de base ; et un qui contient l'indice : le registre d'index.

Image utilisateur

Le processeur calcule alors l'adresse de l’élément voulu à partir du contenu de ces deux registres, et accède à notre élément. En clair : notre instruction ne fait pas que calculer l'adresse de l’élément : elle va aussi le lire ou l'écrire.

Image utilisateur

Ce mode d'adressage possède une variante qui permet de vérifier qu'on ne "déborde pas" du tableau, en calculant par erreur une adresse en dehors du tableau, à cause d'un indice erroné, par exemple. Accéder à l’élément 25 d'un tableau de seulement 5 élément 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 constantes ou placées dans deux registres). Si cette variante n'est pas supportée, on doit faire ces vérifications à la main. Parfois, certains processeurs implémentent des instructions capables de vérifier si les indices des tableaux sont corrects. Ces instructions sont capables de vérifier si un entier (l'indice) dépasse d'indice maximal autorisé, et qui effectuent un branchement automatique si l'indice n'est pas correct. L'instruction BOUND sur le jeu d'instruction x86 en est un exemple.

Base + Offset

Les tableaux ne sont pas les seuls regroupements de données utilisés par les programmeurs. Nos programmeurs utilisent souvent ce qu'on appelle des structures. Ces structures servent à créer des données plus complexe que celles que le processeur peut supporter. Comme je l'ai dit plus haut, notre processeur ne gère que des données simples : des entiers, des flottants ou des caractères. Pour créer des types de données plus complexe, on est obligé de regrouper des données de ce genre dans un seul bloc de mémoire : on crée ainsi une structure.

Par exemple, voici ce que donnerais une structure composée d'un entier, d'un flottant simple précision, et d'un caractère :

Octet 1

Octet 2

Octet 3

Octet 4

Octet 5

Octet 6

Octet 7

Octet 8

Octet 9

Adresse A

Adresse A + 1

Adresse A + 2

Adresse A + 3

Adresse A + 4

Adresse A + 5

Adresse A + 6

Adresse A + 7

Adresse A + 8

Entier 32 bits

Entier 32 bits

Entier 32 bits

Entier 32 bits

Flottant simple précision

Flottant simple précision

Flottant simple précision

Flottant simple précision

Caractère 8 bits

Mais le processeur ne peut pas manipuler ces structures : il est obligé de manipuler les données élémentaires qui la constituent unes par unes. Pour cela, il doit calculer leur adresse. Ce qui n'est pas très compliqué : une donnée a une place prédéterminée dans une structure. Elle est donc a une distance fixe du début de la structure.

Calculer l'adresse d'un élément de notre structure se fait donc en ajoutant une constante à l'adresse de départ de la structure. Et c'est ce que fait le mode d'adressage Base + Offset. Celui-ci spécifie un registre qui contient l'adresse du début de la structure, et une constante.

Image utilisateur

Ce mode d'adressage va non seulement effectuer ce calcul, mais il va aussi aller lire (ou écrire) la donnée adressée.

Image utilisateur
Base + Index + offset

Certains processeurs vont encore plus loin : ils sont capables de gérer des tableaux de structures ! Ce genre de prouesse est possible grâce au mode d'adressage Base + Index + offset. Avec ce mode d'adressage, on peut calculer l'adresse d'une donnée placée dans un tableau de structure assez simplement : on calcule d'abord l'adresse du début de la structure avec le mode d'adressage Base + Index, et ensuite on ajoute une constante pour repérer la donnée dans la structure. Et le tout, en un seul mode d'adressage. Autant vous dire que ce mode d'adressage est particulièrement complexe, et qu'on n'en parlera pas plus que cela.

Autres

D'autres modes d'adressages existent, et en faire une liste exhaustive serait assez long. Ce serait de plus inutile, vu que la plupart sont de toute façon obsolètes. Des modes d'adressage comme le Memory indirect ne servent plus à grand chose de nos jours.

Encodage du mode d'adressage

Dans le paragraphe du dessus, on a vu les divers modes d'adressages les plus utilisés. Mais nous n'avons pas encore parlé de quelque chose de fondamental : comment préciser quel mode d'adressage notre instruction utilise ? Et bien sachez que cela se fait de diverses manières suivant les instructions.

Explicite

Nous allons voir un premier cas : celui des instructions pouvant gérer plusieurs modes d'adressages par opérandes. Prenons un exemple : je dispose d'une instruction d'addition. Les deux opérandes de mon instruction peuvent être soit des registres, soit un registre et une adresse, soit un registre et une constante. La donnée à utiliser sera alors chargée depuis la mémoire ou depuis un registre, ou prise directement dans l'instruction, suivant l'opérande utilisée. Dans un cas pareil, je suis obligé de préciser quel est le mode d'adressage utiliser. Sans cela, je n'ai aucun moyen de savoir si la seconde opérande est un registre, une constante, ou une adresse. Autant je peux le savoir pour la première opérande : c'est un registre, autant le mode d'adressage de la seconde m'est inconnu.

On est dans un cas dans lequel certaines opérandes ont plusieurs modes d'adressage. Pour ces instructions, le mode d’adressage doit être précisé dans notre instruction. Quelques bits de l'instruction doivent servir à préciser le mode d'adressage. Ces bits peuvent être placés dans l'opcode, ou dans quelques bits à part, séparés de l'opcode et des opérandes (généralement, ces bits sont intercalés entre l'opcode et les opérandes).

Image utilisateur
Implicite

Dans le second cas, notre instruction ne peut gérer qu'un seul mode d'adressage par opérande, toujours le même. Prenons un exemple : j'ai un processeur RISC dont toutes les instructions arithmétiques ne peuvent manipuler que des registres. Pas de mode d'adressage immédiat, ni absolu ni quoique ce soit : les opérandes des instructions arithmétiques utilisent toutes le mode d'adressage à registre.

Prenons un autre exemple : l'instruction Load. Cette instruction va lire le contenu d'une adresse mémoire et stocker celui-ci dans un registre. Cette instruction a deux opérandes prédéfinies : un registre, et une adresse mémoire. Notre instruction utilise donc le mode d'adressage absolu pour la source de la donnée à lire, et un nom de registre pour la destination du résultat. Et cela ne change jamais : notre instruction a ses modes d'adressages prédéfinis, sans aucune possibilité de changement.

Dans un cas pareil, si chaque opérande a un mode d'adressage prédéterminé, pas besoin de le préciser vu que celui-ci ne change jamais. Celui-ci peut être déduit automatiquement en connaissant l'instruction : il est plus ou moins implicite. On n'a pas besoin d'utiliser des bits de notre instruction pour préciser le mode d'adressage, qui peut être déduit à partir de l'Opcode.

Image utilisateur
Jeux d'instructions et modes d'adressages

Le nombre de mode d'adressages différents gérés par un processeur dépend fortement de son jeu d'instruction. Les processeurs CISC ont souvent beaucoup de modes d'adressages. C'est tout le contraire des processeurs RISC, qui ont très peu de modes d'adressage : cela permet de simplifier la conception du processeur au maximum. Et cela rend difficile la programmation en assembleur : certains modes d'adressages facilitent vraiment la vie (le mode d'adressage indirect à registre, notamment).

Sur certains processeurs, chaque instruction définie dans le jeu d'instruction peut utiliser tous les modes d'adressages supportés par le processeur : on dit que le jeu d'instruction du processeur est orthogonal. Les jeu d'instructions orthogonaux sont une caractéristique des processeurs CISC, et sont très rares chez les processeurs RISC.

Longueur d'une instruction

Une instruction va prendre un certain nombre de bits en mémoire. On dit aussi qu'elle a une certaine longueur. Et cette longueur dépend de l'instruction et de ses opérandes. Les opérandes d'une instruction n'ont pas la même taille. Ce qui fait que nos instructions auront des tailles différentes, si elles utilisent des opérandes différentes. Par exemple, une opérande contenant une adresse mémoire (adressage direct) prendra plus de place qu'une opérande spécifiant un registre : pour un processeur de 64 registres, il suffira d'encoder de quoi spécifier 64 registres. Par contre, une adresse permet souvent de préciser bien plus que 64 cases mémoires et prend donc plus de place. Généralement, l'adressage par registre et l'adressage indirect à registre permettent d'avoir des opérandes petites comparé aux modes d'adressage direct et immédiat. Mais le mode d'adressage implicite est celui qui permet de se passer complètement de partie variable et est donc le plus économe en mémoire.

Quoiqu'il en soit, on pourrait croire que la taille d'une instruction est égale à celle de ses opérandes + celle de son opcode. Mais c'est faux. C'est vrai sur certains processeurs, mais pas sur tous. Certains processeurs ont des instructions de taille fixe, peut importe la taille de leurs opérandes. D'autres processeurs utilisent des instructions de taille variable, pour éviter de gaspiller et prendre juste ce qu'il faut de mémoire pour stocker l'opcode et les opérandes.

Longueur variable

Sur certains processeurs, cette longueur est variable : toutes les instructions n'ont pas la même taille. Ainsi, une instruction d'addition prendra moins de bits qu'une instruction de branchement, par exemple. Cela permet de gagner un peu de mémoire : avoir des instructions qui font entre 2 et 3 octets est plus avantageux que de tout mettre sur 3 octets. En contrepartie, calculer l'adresse de la prochaine instruction est assez compliqué : la mise à jour du Program Counter nécéssite pas mal de travail.

Les processeurs qui utilisent ces instructions de longueur variable sont souvent des processeurs CISC. Il faut dire que les processeurs CISC ont beaucoup d'instructions, ce qui fait que l'opcode de chaque instruction est assez long et prend de la mémoire. Avoir des instructions de longueur variable permet de limiter fortement la casse, voir même d'inverser la tendance. La taille de l'instruction dépend aussi du mode d'adressage : la taille d'une opérande varie suivant sa nature (une adresse, une constante, quelques bits pour spécifier un registre, voire rien).

Longueur fixe

Sur d'autres processeurs, cette longueur est généralement fixe. 2videmment, cela gâche un peu de mémoire comparé à des instructions de longueur variable. Mais cela permet au processeur de calculer plus facilement l’adresse de l'instruction suivante en faisant une simple addition. Et cela a d'autres avantages, dont on ne parlera pas ici.

Les instructions de longueur fixe sont surtout utilisées sur les processeurs RISC. Sur les processeurs RISC, l'opcode prend peu de place : il y a peu d'instructions différentes à coder, donc l'opcode est plus court, et donc on préfère simplifier le tout et utiliser des instructions de taille fixe.


Registres architecturaux Classes d'architectures

Classes d'architectures

Représentation en binaire Un peu de programmation !

Classes d'architectures

Tous ces modes d'adressage ne sont pas supportés par tous les processeurs ! En fait, il existe plusieurs types d'architectures, définies par leurs modes d'adressages possibles. Certaines ne supportent pas certains modes d'adressage. Et pour s'y repérer, on a décider de classifier un peu tout ça.

Il existe donc 5 classes d'architectures :

A accès mémoire strict

Dans cette architecture ci, il n'y a pas de registres généraux : les instructions n'accèdent qu'à la mémoire principale. Néanmoins, les registres d'instruction et pointeur d'instruction existent toujours. Les seules opérandes possibles pour ces processeurs sont des adresses mémoire, ce qui fait qu'un mode d'adressage est très utilisé : l'adressage absolu. Ce genre d'architectures est aujourd'hui tombé en désuétude depuis que la mémoire est devenue très lente comparé au processeur.

A pile

Dans les architectures à pile, il n'y a pas de registres stockant de données : les instructions n'accèdent qu'à la mémoire principale, exactement comme pour les architectures à accès mémoire strict. Néanmoins, ces machines fonctionnent différemment. Ces processeurs implémentent une pile, et écrivent donc tous leurs résultats en mémoire RAM. Et oui, vous ne vous êtes pas trompés : il s'agit bien de nos bonnes vielles machines à pile, vues il y a quelques chapitres.

Push et Pop

Ces architectures ont besoin d'instructions pour transférer des données entre la pile et le reste de la mémoire. Pour cela, ces processeurs disposent d'instructions spécialisées pour pouvoir empiler une donnée au sommet de la pile : push ; et une instruction pour dépiler la donnée au sommet de la pile et la sauvegarder en mémoire : pop.

Les instructions push et pop vont aller lire ou écrire à une adresse mémoire bien précise. Cette adresse spécifie l'adresse de la donnée à charger pour push et l'adresse à laquelle sauvegarder le sommet de la pile pour pop. Cette adresse peut être précisée via différents modes d'adressages : absolus, Base + Index, etc. L'instruction push peut éventuellement empiler une constante, et utilise dans ce cas le mode d'adressage immédiat.

Instructions arithmétiques

Toutes les instructions arithmétiques et logiques vont aller chercher leurs opérandes sur le sommet de la pile. Ces instructions vont donc dépiler un certain nombre d'opérandes (1 ,2 voire 3), et vont stocker le résultat au sommet de la pile. Le sommet de la pile est adressé de façon implicite : le sommet de la pile est toujours connu (son adresse est stockée dans un registre dédié et on n'a donc pas besoin de la préciser par un mode d'adressage).

Notre instructions arithmétiques et logiques se contentent d'un opcode, vu que les opérandes sont adressées implicitement. C'est pour cela que sur ces processeurs, la mémoire utilisée par le programme est très faible.

Sur un ordinateur qui n'est pas basé sur une architecture à pile, on aurait dû préciser la localisation des données en ajoutant une partie variable à l'opcode de l'instruction, augmentant ainsi la quantité de mémoire utilisée pour stocker celle-ci.

Machines à pile 1 et 2 adresses

Dans ce que je viens de dire au-dessus, les machine à pile que je viens de décrire ne pouvait pas manipuler autre chose que des données placées sur la pile : ces machines à pile sont ce qu'on appelle des machines zéro adresse. Toutefois, certaines machines à pile autorisent certaines instructions à pouvoir, si besoin est, préciser l'adresse mémoire d'une (voire plusieurs dans certains cas) de leurs opérandes. Ainsi, leurs instructions peuvent soit manipuler des données placées sur la pile, soit une donnée placée sur la pile et une donnée qui n'est pas sur la pile, mais dans la mémoire RAM. Ces machines sont appelées des machines à pile une adresse.

A accumulateur unique

Sur certains processeurs, les résultats d'une opération ne peuvent être enregistrés que dans un seul registre, prédéfini à l'avance : l'accumulateur. Cela ne signifie pas qu'il n'existe qu'un seul registre dans le processeur. Mais vu qu'une instruction ne peut pas modifier leur contenu, le seul moyen d'écrire dans ces registres est de lire une donnée depuis la mémoire et de stocker le résultat de la lecture dedans. Toute instruction va obligatoirement lire une donnée depuis cette accumulateur, et y écrire son résultat. Si l'instruction a besoin de plusieurs opérandes, elle va en stocker une dans cet accumulateur et aller chercher les autres dans la mémoire ou dans les autres registres.

Dans tous les cas, l'accumulateur est localisé grâce au mode d'adressage implicite. De plus, le résultat des instructions arithmétiques et logiques est stocké dans l'accumulateur, et on n'a pas besoin de préciser où stocker le résultat : pas de mode d'adressage pour le résultat.

Architectures 1-adresse

Historiquement, les premières architectures à accumulateur ne contenaient aucun autre registre : l'accumulateur était seul au monde. Pour faire ses calculs, notre processeur devait stocker une opérande dans l'accumulateur, et aller chercher les autres en mémoire. En conséquence, le processeur ne pouvait pas gérer certains modes d'adressages, comme le mode d'adressage à registre. Sur ces processeurs, les modes d'adressages supportés étaient les modes d'adressages implicite, absolus, et immédiat.

Ces architectures sont parfois appelées architectures 1-adresse. Cela vient du fait que la grosse majorité des instructions n'ont besoin que d'une opérande. Il faut dire que la majorité des instructions d'un processeur n'a besoin que de deux opérandes et ne fournissent qu'un résultat : pensez aux instructions d'addition, de multiplication, de division, etc. Pour ces opérations, le résultat ainsi qu'une des opérandes sont stockés dans l'accumulateur, et adressés de façon implicite. Il y a juste à préciser la seconde opérande à l'instruction, ce qui prend en tout une opérande.

Architectures à registres d'Index

Évidemment, avec ces seuls modes d'adressages, l'utilisation de tableaux ou de structures devenait un véritable calvaire. Pour améliorer la situation, ces processeurs à accumulateurs ont alors incorporés des registres d'Index, capables de stocker des indices de tableaux, ou des constantes permettant de localiser une donnée dans une structure. Ces registres permettaient de faciliter les calculs d'adresses mémoire.

Au départ, nos processeurs n'utilisaient qu'un seul registre d'Index, accessible et modifiable via des instructions spécialisées. Ce registre d'Index se comportait comme un second accumulateur, spécialisés dans les calculs d'adresses mémoire. Les modes d'adressages autorisés restaient les mêmes qu'avec une architecture à accumulateur normale. La seule différence, c'est que le processeur contenait de nouvelles instruction capables de lire ou d'écrire une donnée dans/depuis l'accumulateur, qui utilisaient ce registre d'Index de façon implicite.

Mais avec le temps, nos processeurs finirent par incorporer plusieurs de ces registres. Nos instructions de lecture ou d'écriture devaient alors préciser quel registre d'Index utiliser. Le mode d'adressage Indexed Absolute vit le jour. Les autres modes d'adressages, comme le mode d'adressage Base + Index ou indirects à registres étaient plutôt rares à l'époque et étaient difficiles à mettre en œuvre sur ce genre de machines.

Architectures 2,3-adresse

Ensuite, ces architectures s’améliorèrent un petit peu : on leur ajouta des registres capables de stocker des données. L’accumulateur n'était plus seul au monde. Mais attention : ces registres ne peuvent servir que d’opérande dans une instruction, et le résultat d'une instruction ira obligatoirement dans l'accumulateur. Ces architectures supportaient donc le mode d'adressage à registre.

Architectures registre-mémoire

C'est la même chose que l'architecture à accumulateur, mais cette fois, le processeur peut aussi contenir plusieurs autres registres généraux qui peuvent servir à stocker pleins de données diverses et variées. Le processeur peut donc utiliser plusieurs registres pour stocker des informations (généralement des résultats de calcul intermédiaires), au lieu d'aller charger ou stocker ces données dans la mémoire principale.

Ces architectures à registres généraux (ainsi que les architectures Load-store qu'on verra juste après) sont elles-même divisées en deux sous-classes bien distinctes : les architectures 2 adresses et les architectures 3 adresses. Cette distinction entre architecture 2 et 3 adresses permet de distinguer les modes d'adressages des opérations arithmétiques manipulant deux données : additions, multiplications, soustraction, division, etc. Ces instructions ont donc besoin d'adresser deux données différentes, et de stocker le résultat quelque part. Il leur faut donc préciser trois opérandes dans le résultat : la localisation des deux données à manipuler, et l'endroit où ranger le résultat.

Architectures 2 adresse

Sur les architectures deux adresses, l'instruction possède seulement deux opérandes pour les données à manipuler, l'endroit où ranger le résultat étant adressé implicitement. Plus précisément, le résultat sera stocké au même endroit que la première donnée qu'on manipule : cette donnée sera remplacée par le résultat de l'instruction.

Mnémonique/Opccode

Opérande 1

Opérande 2

DIV (Division)

Dividende / Résultat

Diviseur

Avec cette organisation, les instructions sont plus courtes. Mais elle est moins souple, vu que l'une des données utilisée est écrasée : si on a encore besoin de cette donnée après l’exécution de notre instruction, on est obligé de copier cette donnée dans un autre registre et faire travailler notre instruction sur une copie.

Architectures 3 adresse

Sur les architectures trois adresses, l'instruction possède trois opérandes : deux pour les données à manipuler, et une pour le résultat.

Mnémonique/Opccode

Opérande 1

Opérande 2

Opérande 3

DIV

Dividende

Diviseur

Résultat

Les instructions de ce genre sont assez longues, mais on peut préciser à quel endroit ranger le résultat. On n'est ainsi pas obligé d'écraser une des deux données manipulées dans certains cas, et stocker le résultat de l'instruction dans un registre inutilisé, préférer écraser une autre donnée qui ne sera pas réutilisée, etc. Ce genre d'architecture permet une meilleure utilisation des registres, ce qui est souvent un avantage. Mais par contre, les instructions deviennent très longues, ce qui peut devenir un vrai problème. Sans compter que devoir gérer trois modes d'adressages (un par opérande) au lieu de deux risque d'être assez couteux en circuits et en transistors : un circuit aussi complexe sera plus lent et coutera cher. Et ces désavantages sont souvent assez ennuyeux.

Load-store

Cette fois, la différence n'est pas au niveau du nombre de registres. Dans cette architecture, toutes les instructions arithmétiques et logiques ne peuvent aller chercher leurs données que dans des registres du processeurs.

Accès mémoires

Seules les instructions load et store peuvent accéder à la mémoire. load permet de copier le contenu d'une (ou plusieurs) adresse mémoire dans un registre, tandis que store copie en mémoire le contenu d'un registre. load et store sont des instructions qui prennent comme opérande le nom d'un registre et une adresse mémoire. Ces instructions peuvent aussi utiliser l'adressage indirect à registre ou tout autre mode d'adressage qui fournit une adresse mémoire.

Instructions arithmétiques et logiques

Toutes les autres instructions n'accèdent pas directement à la mémoire. En conséquence, ces instructions ne peuvent prendre que des noms de registres ou des constantes comme opérandes. Cela autorise les modes d'adressage immédiat et à registre. Il faut noter aussi que les architectures Load-store sont elles aussi classées en architectures à 2 ou 3 adresses. Tous les processeurs RISC inventés à ce jour sont basés sur une architecture Load-store.