Licence CC BY-NC

Vers la pratique: un gros paquet de portes

Armé de toutes ces connaissances, il est finalement temps de terminer notre voyage avec un petit aperçu de quelques circuits présents dans les processeurs modernes.

Multiplexeurs et démultiplexeurs

Multiplexeur (MUX)

Premier élément d’électronique utile pour la suite, le multiplexeur : plusieurs entrées, un sélecteur et une sortie. En fonction de la valeur du sélecteur, c’est une des entrées qui apparaît en sortie. Ainsi, on numérote les entrées et on donne au sélecteur la valeur de l’entrée qu’on veut voir en sortie.

Par exemple, prenons un multiplexeur simple, dit 2 vers 1 : on a deux entrées, e0e_0 et e1e_1 (pouvant chacune valoir 0 ou 1), un sélecteur, s0s_0 (valant 0 pour la sélection de e0e_0, 1 pour e1e_1) et une sortie qq (dont la valeur dépend de l’entrée sélectionnée et de la valeur de cette entrée).

ch5: mux
Représentation schématique d’un multiplexeur (noté MUX).

À quoi ressemble le circuit électrique correspondant à cet élément ? Pour le savoir, vous pouvez constater qu’on a ici 3 entrées et une sortie. Il s’agit donc d’une fonction booléenne de type q(e0,e1,s0)q(e_0,e_1,s_0). Le reste, c’est la création d’une table de vérité, puis la simplification par Karnaugh, comme on l’a vu ici. Vous essayez ?

s0s_0 e0e_0 e1e_1 qq
1 1 1 1
1 1 0 0
1 0 1 1
1 0 0 0
0 1 1 1
0 1 0 1
0 0 1 0
0 0 0 0

Vous devriez assez facilement tomber sur q(e0,e1,s0)=s0e0+s0e1q(e_0,e_1,s_0)=\overline{s_0}\cdot e_0+s_0\cdot e_1, ce qui est assez logique. Le circuit associé est donc:

ch5: mux 2-1 circuit
Circuit d’un MUX 2 vers 1.

Évidement, il existe des multiplexeurs 4 vers 1, 8 vers 1, 4 vers 2, etc. Néanmoins, le nombre de bits de sélection augmente avec le nombre d’entrée (4 entrées nécessitent 2 bits de sélections, 8 entrées nécessitent 3 bits de sélection, et ainsi de suite). Bien que ce soit ensuite un peu moins marrant à traiter avec Karnaugh, il est tout à fait possible d’arriver à des simplifications (en utilisant directement les règles de l’algèbre booléenne). Une autre manière de faire est de chaîner les multiplexeurs. Voici un exemple de multiplexeur 4 vers 1 réalisé à partir de multiplexeurs 2 vers 1.

ch5: mux 4-1
Représentation schématique d’un MUX 4 vers 1 utilisant 2 bits de sélection, s0s_0 et s1s_1 (gauche) et sa décomposition (droite), réalisé à partir de 3 MUX 2 vers 1, ce qui correspond à la fonction logique q(e0,e1,e2,e3,s0,s1)=s1(s0e0+s0e1)+s1(s0e2+s0e3)q(e_0,e_1,e_2,e_3,s_0,s_1)=\overline{s_1}\,(\overline{s_0}\cdot e_0+s_0\cdot e_1) + s_1\,(\overline{s_0}\cdot e_2+s_0\cdot e_3).

Ces composants sont très utiles pour piloter un composant électronique. En effet, comme on le verra ci-dessous, il permet de choisir entre différentes entrées en fonction de l’opération à réaliser.

Démultiplexeur (DEMUX)

À l’inverse, le démultiplexeur prend une entrée et la redirige vers la bonne sortie en fonction du sélecteur, ce qui donne l’équivalent électronique de l’aiguillage.

ch5: demux
Représentation schématique d’un démultiplexeur 1 vers 2 (DEMUX).

Cette fois, la fonction logique sera une série de « ET ». En effet, pour être active, une sortie donnée devra correspondre au sélecteur. Par exemple, dans le démultiplexeur donné plus haut, la fonction correspondante à la sortie « 0 » serait q0=s0eq_0=\overline{s_0}\cdot e tandis que q1=s0eq_1= s_0\cdot e. Dès lors, à quoi ressemblera le circuit logique ?

Sachant cela :

ch5: demux 1-2
Schéma électrique d’un DEMUX 1 vers 2.

Bien entendu, il existe des démultiplexeurs 1 vers 4, 8…

Couplés aux multiplexeurs, ces composés permettent de transmettre différentes informations par le même canal.

Transmission de plusieurs informations, ici des conversations téléphoniques, par le même canal (source: Wikipédia)

Unité arithmétique et logique (ALU)

Bon, maintenant qu’on a vu les (dé)multiplexeur, on peut s’attaquer à un composant de base d’un processeur, c’est l’ALU, pour arithmetic logic unit (unité arithmétique et logique, en bon français). Dans un processeur, c’est ce type de circuit qui réalise toutes les opérations de base.

ch5: ALU
Représentation schématique d’une ALU. Les flèches vertes représentent des bits de contrôles, qu’on aura l’occasion d’expliquer par la suite.

Une ALU prend un certain nombre de choses en entrée : deux mots, représentant des entiers et dont le nombre de bit dépend du processeur (32 ou 64 bits), AA et BB, ainsi que l’opération à réaliser par l’ALU, l'opcode (pour operation code). On retrouve alors le résultat de l’opération en sortie, YY (mot d’autant de bits que les entrées), ainsi qu’un certain nombre d’informations, regroupées sous le terme de statut (par exemple, pour citer quelque chose qu’on a déjà rencontré, l'overflow, mais aussi d’autres choses intéressantes).

En fonction de l’ALU, on peut réaliser un certain nombre d’opérations sur les entrées AA et BB, telles que :

  • Les mathématiques de base : addition, soustraction, complément à 2, éventuellement les multiplications et divisions, …
  • Les opérations logiques: « ET », « OU », …
  • Les comparaisons: ABA \neq B, A<BA < B, …

D’autres opérations sont parfois disponibles dans des ALU spécialisées, telles que le calcul flottant (nombres à virgule), les décalages et rotations de bits, des opérations mathématiques plus complexes (racines carrées, exponentielle, logarithme et autres joyeusetés), mais le principe reste le même. Évidement, plus on rajoute d’opérations, plus les circuits associés sont complexes, le but ne va donc pas être ici de les passer en revue. Néanmoins, on va voir deux ou trois morceaux intéressants.

L’additionneur binaire

On avait fini le chapitre précédent en montrant qu’on pouvait implémenter les 5 opérations mathématiques de base à l’aide de l’addition et de quelques astuces. Il nous faut donc un circuit permettant de réaliser l’addition de 2 bits en position ii de AA et BB, qu’on va noter aia_i et bib_i, mais en gérant le report entrant, noté cic_{i}, et pour stocker le tout dans le bit ii de YY, noté yiy_i, et en calculant s’il est nécessaire d’avoir un report sortant, ci+1c_{i+1}, pour la prochaine opération. Autrement dit, on est face à la table de vérité ci-dessous.

cic_{i} aia_i bib_i yi(ai,bi,ci)y_i(a_i,b_i,c_i) ci+1(ai,bi,ci)c_{i+1}(a_i,b_i,c_i)
0 0 0 0 0
0 0 1 1 0
0 1 0 1 0
0 1 1 0 1
1 0 0 1 0
1 0 1 0 1
1 1 0 0 1
1 1 1 1 1

Il n’y a plus qu’à regarder quelles fonctions logiques on obtient alors.

  • Pour ce qui est du résultat de l’addition, yi(ai,bi,ci)=ciaibi+ciaibi+ciaibi+ciaibiy_i(a_i,b_i,c_i)=\overline{c_{i}}\cdot\overline{ a_i}\cdot b_i + \overline{c_{i}}\cdot a_i\cdot\overline{b_i}+c_{i}\cdot \overline{a_i}\cdot \overline{b_i} + c_{i}\cdot a_i \cdot b_i, ce qui n’est pas simplifiable en utilisant Karnaugh (parce que ça donne un magnifique damier). Néanmoins, si on fait intervenir la porte XOR, pq=pˉq+pqˉp\oplus q = \bar p\cdot q + p\cdot\bar q, on peut obtenir une simplification en termes de nombre de portes logiques. Ainsi, cela revient à yi(ai,bi,ci)=ci(aibi)y_i(a_i,b_i,c_{i})=c_{i}\oplus (a_i\oplus b_i). Vous pouvez même le vérifier par vous-mêmes1. :pirate:
  • Pour le report sortant, on obtient cette fois quelque chose de simplifiable par Karnaugh. Une fois que c’est fait, on se rend compte que ci+1(ai,bi,ci)=aibi+ci(ai+bi)c_{i+1}(a_i,b_i,c_i) = a_i\cdot b_i+c_i\,(a_i+b_i) (on gagne une porte en mettant cic_i en évidence).

Ne reste plus qu’à faire le circuit correspondant, ce que je vous laisse essayer :

ch5: adder
Circuit électronique d’un additionneur binaire. Les deux portes XOR s’occupent de l’addition, tandis que les portes du dessous gèrent le report sortant. C’est un peu plus compressé que d’habitude, mais peu importe.

Pour la suite, on va simplifier les choses en représentant l’additionneur binaire comme suit :

ch5: adder simplifié
Représentation simplifiée de l’additionneur binaire, équivalent du circuit présenté ci-dessus. Les flèches sont là pour visualiser plus facilement ce qui rentre et ce qui sort.

Une petite ALU 1 bit

On entre clairement dans le domaine du spéculatif : peu de chance qu’un vrai circuit de processeur ressemble à ceux que je vais vous présenter. Mais conceptuellement, ça tient la route. :)

Jusqu’ici, on peut juste réaliser l’addition. Qu’à cela ne tienne, on a vu que pour réaliser la soustraction, ABA-B, il suffisait de placer c0c_0 à 1, et d’additionner avec bi\overline{b_i}. On a donc besoin d’un signal, qu’on va appeller invBinv_B, qui donne bib_i si invB=0inv_B={ 0} et bi\overline{b_i} si invB=1inv_B={ 1} autrement dit, invBbiinv_B \oplus b_i. Et le circuit permettant de réaliser la soustraction est donc simplement le suivant.

ch5: ALU 1 bit v1
ALU 1 bit (carré jaune) permettant de réaliser l’addition et la soustraction.

Bon, on aimerait aussi que notre ALU face un peu plus que l’addition et la soustraction, par exemple qu’elle face aussi le « ET » et le « OU ». Néanmoins, pour faire ça, on va avoir besoin d’un multiplexeur qui réalise le choix entre les différentes opérations à réaliser par l’ALU, qu’on va commander par le signal op. Tant qu’on est dedans, on va rajouter un signal invAinv_A qui va permettre d’obtenir « NON ET » (comme ai+bi\overline{a_i}+\overline{b_i}) et « NON OU » (comme aibi\overline{a_i}\cdot\overline{b_i}). Et l’histoire de préparer la suite, on va également rajouter une entrée pip_i, dont vous allez comprendre l’intérêt un peu plus bas.

ch5: ALU 1 bit v2
Une version beaucoup plus intéressante de l’ALU 1 bit, qui permet déjà de faire plus de choses. :pirate: Évidement, on pourrait lui ajouter d’autres fonctionnalités (comme une porte XOR, par exemple), mais on peut calculer déjà pas mal avec ça. Notez que les valeurs de op sont purement arbitraires.

Pour la suite, on va encore dézoomer d’un cran et représenter ce type d’ALU de la manière suivante.

ch5: ALU 1bit mini
Représentation schématique d’une ALU 1 bit capable de faire l’addition, la soustraction, et quelques opérations logiques.

Une « petite » ALU nn bits

Maitenant qu’on a une ALU 1 bit, on peut l’utiliser pour faire des calculs sur des nombres représentés sur plusieurs bits. Combien ? Comme ça a été dit dans le chapitre précédent, ça dépend du processeur, mais généralement 32 ou 64 bits, qui est la taille de la représentation des nombres entiers. De manière assez logique2, une telle ALU sera alors formée de nn ALU 1 bit mises bout à bout pour traiter chacun des nn bits de AA et de BB. Dès lors, on peut créer notre ALU comme suit.

ch5: ALU n bits
Et voici notre ALU nn bits. Et là je vous dois quelques explications. ;)

Donc, qu’est-ce qu’on a là ? Vous devez reconnaître le centre : c’est là que se font les différentes opérations, bit par bit, comme promis. Pour que ça fonctionne, il faut évidement donner à chacune de ces ALUs les différents signaux op, invAinv_A et invBinv_B.

Par contre, deux modifications ont été apportées afin que cette ALU soit capable de faire les comparaisons :

  • La première, c’est pour réaliser la comparaison A=BA=B. Pour ça, l’astuce est de calculer ABA-B, puisque si AA est égal à BB, le résultat sera de… zéro. Ensuite, pour vérifier que c’est bien le cas, on fait passer le résultat de chaque soustraction bit à bit dans des portes « NON OU « (à droite dans le schéma). En effet, xy=1a=b=0x \downarrow y = 1 \Leftrightarrow a=b=0 (voir le premier chapitre). Le résultat, c’est le signal zéro, qui vaut donc 1 si AB=0A-B=0, c’est-à-dire que A=BA=B.
  • La deuxième, basée sur la même astuce, c’est pour réaliser les comparaisons du type A<BA<B. En effet, si A<BA<B, ABA-B est un nombre négatif. À l’inverse si A>BA>B, ABA-B est un nombre positif. Or on a vu que si un nombre était négatif, son bit en première position (le bit de poids fort) valait 1. C’est à ça que va servir la fameuse sortie pp qu’on a rajoutée plus haut. Si cette sortie est sélectionnée, tous les bits seront mis à zéro, sauf le premier, dont la valeur sera égale au bit de poids fort résultant de la soustraction de AA et BB (setpset_p dans le schéma), ce qui explique en partie pourquoi l’ALUn-1 est différente des autres (la partie « grise » contient quelques circuits supplémentaires pour gérer cela3). Autrement dit, si A<BA<B, Y=000012Y=00\ldots001_2, 00000200\ldots000_2 sinon. Dans certains processeurs, c’est également un signal, comme zéro. À noter que le calcul dépend de si on compare des nombres signés ou pas (d’où le signal supplémentaire signe).

Et finalement, la dernière modification apportée est là pour permettre la détection de l'overflow, comme expliqué dans le chapitre précédent. Pour cela, il est nécessaire de connaître le report entrant et sortant (cn2c_{n-2} et cn1c_{n-1}), ainsi que le fait qu’on travaille avec des nombres signés ou non. De même, c’est l’ALUn-1 qui a été modifiée de manière à indiquer s’il y a dépassement ou non.

Et on a donc une ALU qui est capable de faire des opérations sur les nombres entiers signés ou pas, y compris des comparaisons. C’est rudimentaire, mais vous n’imaginez pas tout ce qu’on peut déjà faire avec ça.

Tout ça n’est toutefois pas très efficace. En effet, tout ça est très joli sur le papier, mais j’ai volontairement fait en sorte de ne pas complexifier la chose en omettant des aspects très importants qui reposent sur la petite note faite il y a deux chapitres : passer par une porte prend du temps ! Deux conséquences à cela :

  • On a créé des scénarios de course (race condition, que les développeurs parmi vous connaissent peut-être déjà, parce que ce n’est pas limité à l’électronique). Par exemple :

    CH5: scénario de course
    Scénario de course simple entre deux composants électroniques (inspiré de Wikipédia). On verra plus loin que ce scénario a ceci dit un intérêt.

    Imaginons qu’on change la valeur de aa. Le passage par la porte « NON » implique un délai Δt1\Delta{t_1} de temps, par rapport à la première entrée de la porte « ET », ce qui signifie que la valeur de sortie de cette porte « ET » n’est correcte qu’après un temps équivalent à Δt1+Δt2\Delta{t_1} + \Delta{t_2} (le passage par la porte « ET » impliquant un second délai Δt2\Delta{t_2}). Autrement dit, comme les changements ne sont pas instantanés, il faut attendre un certain temps pour que le système se « stabilise » avant de lire une valeur correcte. Pire, durant l’intervalle, la valeur en sortie de la porte « ET » est incorrecte. Il faut donc attendre un certain temps…

    Pour ceux que ça intéresse, ça se traite généralement en ajoutant de la redondance et/ou en simplifiant le circuit (comme c’est le cas ici).

  • La création d’un circuit comme celui présenté plus haut où les unités sont mises à la chaîne induit un énorme problème généré par le calcul du report. En effet, le calcul du dernier reste (cnc_n) requiert de passer par rien de moins que 3n3n portes, sans compter le temps qu’il faut pour réaliser les opérations à la suite : le calcul de yiy_i requiert le calcul de cic_i, qui requiert le calcul de ci1c_{i-1}, qui requiert celui de ci2c_{i-2}, qui… rends le temps très long ! Ça impacte donc le nombre d’opération qu’on peut réaliser par seconde, et ce n’est pas quelque chose de souhaitable. Il existe cependant des solutions, par exemple le carry lookahead adder (« additionneur à retenue/report anticipée »), une alternative assez intelligente pour régler le problème du report et qui est une fois encore une application assez jolie de l’algèbre de Boole.

Bonus : et ainsi, on en arrive presque à l’assembleur

Si on résume, on a besoin de différents bits de contrôle nous permettant de réaliser tout un tas d’opérations. Et c’est là qu’intervient l’Assembleur. C’est quoi? C’est le langage de « programmation » le plus proche du pour communiquer avec le processeur. Ainsi, les instructions en Assembleur sont directement lues et interprétées par le CPU, qui va agir en fonction. Une instruction Assembleur contient un opcode (qui dit au CPU quoi faire) et des opérandes (qui dit au CPU quoi utiliser pour le faire). Évidement, cet opcode n’est pas un mot, mais un nombre en binaire, qui peut alors être passé à la moulinette de portes logiques pour déterminer de quelle opération il s’agit. Ainsi, dans un CPU, il ne serait pas illogique de retrouver sous une forme ou une autre dans cet opcode les différentes valeurs de signesigne, invAinv_A, invBinv_B et opop.

Le processeur trouve les données qu’il manipule dans des registres : il s’agit de zones mémoires très petites (typiquement, 1 mot) et à accès très rapide (à l’échelle du processeur). Quand on manipule une variable dans un langage de programmation, elle est en pratique copiée dans un registre avant d’être manipulée. Il existe, en fonction de l’architecture du processeur, un nombre de registre différent, mais ce nombre est en fait très faible comparé aux nombres de variables que peut manipuler un programme.

Par exemple, le MIPS32

Prenons par exemple le MIPS32 (en) (un certain type de processeur, aujourd’hui plus scolaire qu’encore utilisé), on voit que les instructions processeurs sont séparées en différents « champs ».

Instruction MIPS (32 bits)

Opcode (6 bits)

rs (5 bits)

rt (5 bits)

rd (5 bits)

shamt (5 bits)

func (6 bits)

Répartition des différents bits dans une instruction MIPS (de type R).

Pour les opérations mathématiques, l’opcode est le même. Le function code (func) contient les différents bits de contrôle pour l’ALU. rs, rt et rd (registres source, target et destination) sont les registres que l’instruction doit manipuler (en MIPS, il y a 32 registres, d’où le code sur 5 bits qui représente le numéro du registre). Le shift amount (shamt) est utile pour les opérations de décalage de bits. Typiquement, pour l’instruction add et sub (qui font, comme leur nom l’indique, l’addition et la soustraction de rs et rt pour inscrire le résultat dans rd), l’opcode vaut 0160_{16} dans les deux cas, tandis que le function code vaut 201620_{16} (321032_{10}) et 221622_{16} (341034_{10}), respectivement (pour les additions et soustractions non-signées, les function codes valent 211621_{16} et 231623_{16}, de manière logique).

Plus d’exemples ici (en) :)

Le monde de l’Assembleur nécessiterait cependant un tutoriel à lui tout seul. En effet, les instructions sont spécifiques à un type de processeur (ARM ou intel, par exemple) et certaines subtilités existent (par exemple au niveau des registres). Néanmoins, le principe de base reste le même. :)


  1. C’est tout à fait équivalent, puisque ci(aibi)=ci(aibi)+ci(aibi)c_{i}\oplus (a_i\oplus b_i)=c_i\,\overline{(a_i\oplus b_i)}+\overline{c_i}\,(a_i\oplus b_i), avec (aibi)=(aibi+aibi)=(aibi)(aibi)=(ai+bi)(ai+bi)=aibi+aibi\overline{(a_i\oplus b_i)} = \overline{(\overline{a_i}\cdot b_i+a_i\cdot\overline{b_i})}=\overline{(\overline{a_i}\cdot b_i)}\,\overline{(a_i\cdot\overline{b_i})}=(a_i+\overline{b_i})\,(\overline{a_i}+b_i)=a_i\cdot b_i+\overline{a_i}\cdot\overline{b_i}. Il faut juste aimer De Morgan.
  2. Aha.
  3. Je ne suis pas certain de la forme des circuits impliqués dans la partie grise de l’ALUn-1, je ne préfère donc pas m’avancer.

La mémoire (et l'horloge)

C’est quoi de la mémoire, en fait ? D’un point de vue tout à fait théorique, ce n’est jamais qu’une grosse fonction booléenne, pour laquelle la variable d’entrée est l’adresse, et la sortie est la valeur du bit situé à l’adresse demandée1 (c’est le concept table de correspondance, look-up table (en), abrégé LUT).

Évidement, vous connaissez des éléments qui constituent « de la mémoire » pour un ordinateur. De tête, on pourrait probablement citer les CD/DVD/Blue Ray, les clés USB et autres cartes flash, les disques durs et SSD, ou encore la fameuse RAM (Random-Access Memory). Là-dedans, on peut quand même distinguer :

  • La mémoire volatile et non-volatile. La mémoire volatile disparaît en l’absence d’alimentation électrique (c’est le cas de la RAM), la mémoire non-volatile existe indépendamment de l’alimentation (c’est le cas de tout les autres composants cités).
  • La mémoire à accès séquentiels ou aléatoire (random). Une mémoire à accès aléatoire permet, en un temps rapide, d’accéder à un endroit précis, là ou une mémoire à accès séquentiels… Le permet, mais en un temps plus long. Un exemple typique du dernier cas sont les bandes magnétiques (oui, les cassettes audio, mais ça a longtemps servi comme support de sauvegarde pour les ordinateurs aussi, des programmes étaient même vendus comme ça). On se rend également compte que le terme « RAM » n’est absolument pas précis, puisqu’on pense aux barrettes mémoires de notre ordi, alors que le terme s’applique en fait très bien à un disque dur (qui permet aussi d’accéder à un endroit arbitraire en un temps court). Le terme exact pour les barrettes de RAM est en fait DRAM (dynamic random-access memory) (voir de la SDRAM si on veut pinailler sur les détails), comme on aura l’occasion de le voir plus loin.
  • La manière de stocker les bits varie également d’un support à un autre. En ce qui concerne les disques optiques (45 tours, CD, DVD), il s’agit bien entendu de creux et de bosses. Le disque dur exploite lui les propriétés magnétiques de métaux (c’était également le cas des disquettes et de la bande magnétique, qui se base sur les propriétés magnétiques de l’oxyde de fer, plus communément appelé la rouille, d’où la couleur des bandes magnétiques d’ailleurs). Quant aux autres types de mémoire, on aura l’occasion d’en parler. ;)

ROM ?

Avant de parler de mémoire volatile, passons un instant sur les ROM (read only memory). On leur donne aussi le petit nom de mémoire morte en français. Il s’agit simplement d’un circuit implémentant la fameuse fonction booléenne qui, pour une adresse entrée (dans les word lines), donne la valeur des bits en sortie (dans les bit lines), les unes et les autres étant reliés par ce qu’il faut de porte logique pour obtenir la fonction booléenne. Dans une version naïve, ces fonctions peuvent être implémentées simplement à l’aide de multiplexeurs, mais ça en ferait vite beaucoup. Une autre idée, c’est d’être plus systématique et en accord avec ce qu’on a vu dans le chapitre sur la logique booléenne : une fonction booléenne, c’est une disjonction de minterms (qui sont des conjonctions de littéraux, pour rappel). Du coup, on peut schématiser les choses comme suit.

CH5: ROM (PLA)
Schéma simplifié d’une ROM, composé de mm entrées et de nn sorties (qu’on appelle une 2m×n2^m\times n bit ROM, donc si par exemple m=n=2m=n=2, alors ce sera une ROM 4 x 2 bits).

Écrire une ROM demande donc de créer un circuit logique (c’est là que l’algorithme de Mc-Cluskey prend tout son sens, afin de demander à un ordinateur de faire le travail de simplification). Pour rendre ça un peu plus systématique, on a inventé des ROM « programmables » (les PROMS). L’idée de base, c’est un array de portes « ET », suivis d’un array de portes « OU », et par défaut, tout est interconnecté, mais la programmation de la ROM permet de changer ça. Voyons ça sur un exemple d’une ROM 4x2 bits:

CH5: PLA 2
Schéma d’une ROM 4x2 bits non-programmée. Il s’agit plus précisément d’un programmable logic array (en) (PLA). Notez qu’une PLA pour une ROM avec mm entrées et nn sorties contient 2m2^m portes « ET » (ou équivalent) et nn portes « OU ».

Chaque petit rond bleu est une connexion qui peut être brisée (pour ça, on peut utiliser des fusibles que l’application d’une grande tension brise). C’est le processus de "programmation" d’une ROM : passer d’un circuit comme celui présenté plus haut à un circuit correspondant à la fonction logique qu’on souhaite implémenter. En l’occurrence, pour implémenter la fonction logique f(a,b)=ab+aˉbf(a,b)=a\cdot b + \bar a \cdot b et g(a,b)=aˉbˉ+abˉg(a,b)=\bar a \cdot \bar b + a\cdot \bar b,

CH5: PLA (fg)
ROM programmée pour que les sorties correspondent aux fonctions booléennes ff et gg.

On trouve généralement un schéma simplifié pour exprimer la même chose.

CH5: PLA (fg, simplifié)
Schéma simplifié de la PLA pour ff et gg.

Dans le cas d’une simple PROM (avec des fusibles), la programmation ne peut être effectuée qu’une fois (au moment de sa création). Mais on a par la suite inventé les EPROM et EEPROM, qui peuvent être reprogrammées après une exposition aux rayons UV pour la première et l’aide d’un courant électrique pour la seconde (ce qui est utile lorsqu’on fait du développement de circuit).

Les ROM étaient par exemple présentes dans les anciens BIOS ou était employée dans les cartouches de jeux vidéos auparavant2 (en anglais, le mot pour les désigner est ROM cartridge, cartridge signifiant cartouche). On s’en sert toujours pour créer des tables de correspondances entre valeurs ou dans l’informatique embarquée.

Mais plus intéressant, les mémoires flashs et disque dur SSD sont en fait composés de ROM reprogrammables à l’aide de courant électriques.

Circuit avec rétroaction et logique combinatoire

Que vaut r(a)r(a) dans le circuit suivant ?

CH5: feedback
Circuit avec rétroaction : la sortie de la porte « OU » est passée en entrée de cette même porte.

Eh bien… Ça dépend. Ça dépend en fait de ce que vaut aa au temps tt, bien entendu, ce qu’on va noter ata_t, mais ça dépend également ce que valait aa avant, ce qu’on va noter at1a_{t-1}. En fait, on peut écrire le petit tableau suivant.

at1a_{t-1} ata_t r(at)r(a_t)
0 0 0
1 0 1
0 1 1
1 1 1
Table de vérité du circuit ci-dessus.

En fait, le plus intéressant, c’est que si at=0a_t={0}, alors le circuit ne change pas de résultat (r(0)=at1r({0})=a_{t-1}). À l’inverse, si a=1a={1}, alors le circuit change son état pour 1 (r(1)=1r({1})={1}), et rien ne pourra plus l’en faire sortir (sauf évidement de couper l’alimentation électrique). Le circuit est donc verrouillé (on parle d’un latch, en anglais).

Et ça, c’est très intéressant : d’une part parce qu’on vient de créer de la mémoire volatile (qui, ici, n’est pas réinscriptible, puisque si on inscrit 1, on ne peut plus en sortir), et d’autre par parce qu’on vient de mettre le doigt sur ce qu’on appelle de la logique séquentielle. En effet, jusqu’ici, on est resté dans le domaine de la logique combinatoire, ou la valeur d’une fonction booléenne dépend uniquement de ses entrées. Sauf que dans l’exemple ci-dessus, on a vu qu’il fallait également introduire la notion de temps, puisque la sortie de la fonction booléenne ne dépend plus seulement des valeurs d’entrée, mais également des valeurs précédentes (ou, comme on le verra plus loin, de l’instant où elles sont mesurées).

Bon, jusqu’ici, on a un circuit qui n’est pas très utile, mais on peut l’améliorer pour qu’il le soit en créant des « verroux ». L’un des exemples les plus utilisés, c’est celui qu’on peut créer avec deux portes NOR assemblées comme suit.

CH5: SR latch
Verrou SR. Vu que transistors et CMOS agissent comme des portes NOR, c’est assez facile à mettre en place.

La table de vérité de ce circuit est la suivante.

SS RR QtQ_t Note
0 0 Qt1Q_{t-1} Lecture de la valeur de QQ
0 1 0 Mise à 0 de QQ
1 0 1 Mise à 1 de QQ
1 1 X Interdit
Table de vérité du verrou SR (SR latch en anglais).

Là, on a un élément mémoire digne de ce nom. On voit que si on met SS à 1 (avec R=0R={0}), on change la valeur de QQ telle qu’elle soit 1 (et ce quelle que soit sa valeur précédente): Qˉ=SQt1=0\bar Q=S \downarrow Q_{t-1} = 0 et ce quel que soit la valeur précédente de Qt1Q_{t-1} (allez revoir la table de vérité de NOR si vous ne me croyez plus). Le reste suit logiquement : RQˉ=1R\downarrow \bar Q=1, donc on arrive bien à la situation où Q=1Q=1.

À l’inverse, RR à 1 (avec S=0S={0}) change la valeur de QQ pour 0. SS et RR sont simplement les premières lettres de set et reset. La valeur de QQ est lue en utilisant R=S=0R=S={0}, c’est l’état verrouillé, comme dans le circuit précédent. À noter que R=S=1R=S={1} est interdit, parce que dans ce cas, on a droit à un magnifique scénario de course (enfin, grosso modo, Q=Qˉ=0Q=\bar Q={0}, ce qui n’a pas beaucoup de sens).

Ce circuit est nommé multivibrateur bistable, c’est-à-dire un circuit dont la sortie change d’état lors de la modification de l’état d’une de ces entrées, et qui garde cette position jusqu’à ce qu’on change les entrées. Le terme « multivibrateur » vient du fait que ce circuit peut également être vu comme un oscillateur électrique, produisant un signal (idéalement) carré.

Ce verrou est dit transparent, dans le sens où un changement des entrées se répercute immédiatement sur la sortie. Et ça, ça peut être un problème à cause des scénarios de courses déjà mentionnés plus haut. Idéalement, on souhaiterait que le changement ne soit possible qu’à certains moments, on va donc ajouter une entrée qui permettra d’activer le changement, qu’on va noter EE (pour enable). Facile, il suffit d’ajouter des portes « ET » :

CH5: gated RS latch
Verrou SR modifié pour ajouter une protection contre la modification (gated SR latch en anglais).
EE SS RR QtQ_t
0 X X Qt1Q_{t-1}
1 0 0 Qt1Q_{t-1}
1 0 1 0
1 1 0 1
1 1 1 X
Table de vérité du verrou SR avec protection.

Sauf qu’il y a une simplification possible ! En effet, lorsqu’on change l’état du verrou (avec E=1E={1}), alors SS et RR sont opposés. Il suffit alors tout simplement de retirer l’une des deux entrées (on va renommer l’autre DD pour data), et d’utiliser une porte « NON » :

CH5: D latch
Verrou D (D latch en anglais).

Ce qui simplifie la table de vérité :

EE DD QtQ_t Note
0 X Qt1Q_{t-1} Pas de changements
1 0 0 Mise à 0 de QQ
1 1 1 Mise à 1 de QQ
Table de vérité du verrou D. Cette fois, le verrou est transparent lorsque E=1E={1}, opaque dans le cas contraire. Notez qu’il n’y a pas de cas interdit.

L’horloge et les flip-flops

La gestion du temps

L’horloge, c’est tout d’abord un circuit capable de passer d’un état bas (0) à un état haut (1) et inversement ff fois par seconde, c’est ce qu’on appelle la fréquence. La période de l’horloge est donnée par T=1/fT=1/f, c’est le temps qu’il faut au circuit pour partir de 0 et revenir à 0 (même chose avec 1). Avec un dessin,

CH5: clock signal
Signal périodique d’horloge. Le temps τ\tau est le temps entre les deux côtés du signal positif, le front ascendant (rising edge, qu’on appelle aussi front montant) et le front descendant (falling edge, en anglais).

Derrière tout ça se cache un matériau piézo-électrique, c’est-à-dire un matériau qui possède la capacité de changer de forme si on lui applique un potentiel électrique (c’est un cristal de quartz qui est employé ici, et si on connaît sa forme, on peut connaître assez précisément la fréquence).

On peut également introduire le rapport cyclique : c’est la fraction de la période durant laquelle le signal est haut. Ce rapport se calcule simplement comme 1α=τ/T1\alpha = \tau /T. Il est généralement de 1/2^{1}/_{2}. L’idée, c’est que c’est ce signal qui (en plus de donner l’heure) va synchroniser les circuits électriques. Autrement dit, on va s’arranger pour que les changements n’arrivent qu’à un instant précis (tout les TT intervalles de temps, évidement), puis on va laisser le temps au circuit pour se stabiliser, et on est assuré qu’après un certain temps (inférieur à TT, ce qui fait que le choix de la période est crucial et dépend du plus lent des composants du circuit, rappelez-vous la remarque sur le calcul du report fait plus haut), la réponse qu’on mesurera(it) sera(it) la bonne.

Une fois qu’on possède un tel circuit d’horloge, on peut s’en servir pour synchroniser tous les circuits. Évidement, chacun de ceux-ci ne travaille pas à la même fréquence (TT plus ou moins long), mais on peut s’arranger (la mémoire vive fonctionne par exemple plus lentement que le processeur, et c’est sans parler des mémoires plus lentes telles que les disques [durs]). Notez que la modification de la fréquence d’horloge d’un processeur est tout à fait possible, on peut ainsi l’augmenter par overclocking (sur-cadençage pour les plus français d’entre nous), mais le processeur peut également faire le travail lui-même, souvent en réduisant la fréquence pour réduire la quantité de chaleur mais aussi en l’augmentant en cas de besoin (avec des technologies comme Intel Turbo Boost (en) et AMD PowerTune (en)).

Donc la première chose à tenter, c’est de brancher le signal EE de notre verrou D au signal d’horloge. Quand on fait ça, on a alors un système dont les valeurs ne peuvent être modifiées que quand le signal d’horloge est haut la valeur associée au signal est de 1). On passe alors d’un verrou à une bascule (flip-flop en anglais, rien à voir avec les « chaussures » du même nom), qui se distingue d’un verrou par le fait que le circuit correspondant est synchrone.

Edge-triggered flip-flops

C’est une bonne idée, mais… puisque durant tout cet intervalle de temps τ\tau, la valeur du verrou peut être modifiée, on est pas à l’abri d’un cas où la valeur de DD changerait plusieurs fois au cours de cet intervalle (à moins d’avoir un τ\tau extrêmement faible). Du coup, on va ruser et permettre le changement uniquement quand le signal d’horloge change. Et pour ça, on peut imaginer plusieurs manières de faire. La première est de réaliser un circuit qui ne vaut 1 que quand le signal d’horloge change de valeur. Et pour ça, il suffit d’exploiter… Le scénario de course qu’on a vu plus haut, tout simplement. Imaginons que le temps pour passer au travers d’une porte « NON » soit de Δt1\Delta{t}_1 tandis que le passage dans une porte « ET » soit de Δt2\Delta{t}_2. On peut alors mesurer la réponse du circuit suivant au cours du temps :

CH5: scénario de course
Le retour du scénario de course :magicien: (notez que les échelles sont un peu exagérées).

On voit que le signal de sortie (en rouge) ne vaut 1 que durant un bref instant Δt1\Delta{t}_1 qui suis le front ascendant de aa (on voit qu’il n’y a pas de pic correspondant au front descendant de aa). On a donc une espèce de détecteur de pulsation, donc il « suffit » de minimiser Δt2\Delta{t}_2 et de contrôler Δt1\Delta{t}_1 pour obtenir quelque chose d’utilisable pour contrôler précisément quand la valeur de la bascule peut être modifiée. Notez que si on veut réaliser un circuit qui détecte le front descendant, il suffit d’utiliser une porte « NOR » à la place du « ET ». On peut donc placer un tel circuit avant l’entrée EE d’un verrou D pour obtenir un système tout à fait décent.

Une autre manière de faire, c’est d’assembler en série deux verrous D, contrôlés par le même signal d’horloge, dans une configuration dite « maitre-esclave »:

CH5: master slave D
Chaine de deux verrous D (maître puis esclave) formant une bascule D maître-esclave se déclanchant sur le front descendant (Master–slave negative edge-triggered D flip-flop). L’entrée CC est l’horloge (Clock).

Déjà, on peut noter que le second verrou (l’esclave) ne change de valeur que si le premier (le maître) change. On peut également noter que ce changement ne peut pas être simultané, puisque les entrées permettant la modification (EE) sont inversée sur les deux verrous. Autrement dit, on ne peut éditer la valeur du maître que si le signal d’horloge vaut 1 (mais la lecture de QQ donnera toujours la valeur précédente), et une fois que le signal d’horloge passe à 0, l’esclave est mis à jour en conséquence (mais le maître ne peut plus être modifié). Autrement dit, ce système change de valeur sur le front descendant du signal d’horloge. Bien entendu, il y a moyen d’obtenir le même genre de circuit pour le front ascendant (en fait, il suffit de mettre une porte « NON » avant le signal d’entrée de l’horloge), mais l’idée reste la même.

Il existe bien entendu un symbole simplifié pour ces composants, que voici :

CH5: positive and negative D ...
Symbole pour les bascules D maître-esclave se déclenchant sur le front ascendant (gauche, dit positif) ou descendant (droite, dit négatif).

Notez qu’en pratique, c’est ce genre de bascules qui sont généralement utilisées (configuration maître-esclave). Il en existe également d’autre, telles que la bascule JK (deux verroux SR mit en série, donc un fonctionnement proche de la version D) et la bascule T, qui change de valeur de sortie (toogle, d’où le T) si son entrée T=1T={1} (la bascule a pour équation Qt=TQˉt1+TˉQt1Q_t = T\cdot \bar Q_{t-1}+ \bar T \cdot Q_{t-1}). Ce qui est drôle, c’est que si l’entrée TT est constamment maintenue à 1, alors le signal varie toutes les 2 périodes d’horloges (donc on divise la fréquence d’horloge par deux).

Plus d’info sur les bascules sur Wikipédia.

SRAM et DRAM

Registres et caches : SRAM

En fait, c’est comme pour l’ALU : le plus dur, c’est de faire le travail pour 1 bit, ensuite il suffit d’en mettre plusieurs d’affilée. Les registres n’y échappent pas :

CH5: register
Registre de nn bits, composés de nn bascules D. Le signal ww sert à autoriser l’écriture, tandis que le signal CC est l’horloge (du processeur). Notez que ce circuit permet de lire et d’écrire en même temps.

On vient de créer ce qu’on appelle de la static random-access memory, ou SRAM (même si en pratique, ce n’est plus des bascules D qui sont utilisées). Celle-ci est plus rapide que la DRAM qu’on verra juste après, bien qu’elle prenne plus de place que cette dernière (et est plus coûteuse). C’est pour ça qu’on la retrouve principalement dans les registres (pour taper sur le clou : zones de mémoires directement manipulées par le processeur), ainsi que les mémoires caches (qui ne sont pas manipulées directement par le processeur, mais contiennent des données qui sont souvent manipulées par celui-ci, ce qui accélère le calcul parce que les données sont plus rapidement accessibles que s’il fallait aller les rechercher en mémoire vive, par exemple).

Bon, et la (S)DRAM, alors ?

Rien à voir. Une des causes du coût de la SRAM, c’est le nombre de portes logiques (et donc de transistors) qu’il est nécessaire pour les créer (entre 4 et 6). Eh bien la DRAM demande deux composant pour stocker un bit : un transistor (nMOS) et un condensateur.

CH5: DRAM cell
Schéma d’une cellule de DRAM, permettant de stocker un bit. Pour rappel, la word line est celle qui provient des entrées (l’adresse) et la bit line symbolise la sortie.

Pour rappel, un transistor nMOS ne laisse passer le courant que si un potentiel est appliqué sur sa grille (c’est comme un interrupteur). Par ailleurs, on retrouve un condensateur, dont on avait déjà expliqué le fonctionnement il y a deux chapitres, mais disons simplement qu’il permet de stocker de l’électricité, comme une micro-batterie. Bref, le concept, c’est que le condensateur stocke la présence ou l’absence de bit par présence ou absence d’électricité. À partir de là, c’est facile : la charge du condensateur n’est modifiée que quand un potentiel haut (équivalent à 1) provient de la word line (ça signifie que la cellule a été choisie). Pour écrire dans la cellule DRAM, c’est le potentiel de la bit line qui va déterminer si le condensateur va être chargé (potentiel haut) ou déchargé (potentiel bas). Pour la lecture, c’est la décharge ou nom du condensateur qui va modifier le potentiel de la bit line (qui est préalablement placé à un potentiel intermédiaire entre haut et bas) et qui va permettre de savoir si le condensateur était chargé (1) ou déchargé (0).

Quelques petites choses à préciser, tout de même :

  1. Ça signifie que quoiqu’il arrive, à la fin d’une opération de lecture, l’état du condensateur est inversé. Donc, il est nécessaire de faire suivre une opération de lecture par « une opération d’écriture » (recharger ou décharger le condensateur pour le replacer dans son état initial) !
  2. Le problème des condensateurs, c’est qu’ils ne sont pas des batteries très efficaces et qu’ils laissent du coup « échapper » le courant très rapidement. Toutes les 64 ms (ou moins), une lecture est faite afin de réécrire les données à la suite et ainsi préserver l’état de la mémoire. C’est de ce besoin périodique de rafraîchissement que provient le « D » de DRAM (dynamic), là où la SRAM n’a pas besoin d’être rafraîchie.
  3. On ne travaille pas par cellules individuelles, mais par « rangées » de cellules. Donc on lit par rangée, on rafraîchit l’état de toutes les cellules de la rangée en même temps et on profite d’une écriture pour également rafraîchir l’état de toute la rangée (pour être sûr). À cause de tout ça, la DRAM possède son propre circuit de gestion, indépendant du processeur et sa propre horloge (qui va normalement moins vite que celle du processeur). C’est la présence de cette horloge qui fait la SDRAM (pour synchroneous). On ne trouve aujourd’hui quasi plus que des SDRAM.
  4. Le terme "DDR" signifie "double data rate" et s’applique uniquement à un certain type de SDRAM. Pour réaliser ce miracle, ces mémoires transfèrent des données non seulement sur le front montant, mais aussi sur le front descendant de l’horloge (évidement, l’électronique est adaptée en fonction). Les itérations suivantes (DDR2, DDR3 et DDR4) jouent sur le nombre de données qui peuvent être transférées simultanément, l’augmentation de la fréquence d’horloge et la réduction du voltage (qui entraîne sinon des surchauffes à cause de l’effet joule).

À noter que la gestion de la mémoire par un ordinateur est un sujet crucial pour les performances, d’une part au niveau du cache (pour que les données les plus utiles soient rapidement accessibles), d’autre par parce que la demande en mémoire d’un programme excède souvent la mémoire disponible (ce qui n’est pas un problème si on le gère convenablement, avec ce qu’on appelle la mémoire virtuelle, où on va mettre l’excédent moins utile sur le disque). Mais tout ça sort du cadre de ce tutoriel. :)


  1. En vrai, on travaille plutôt au niveau de l’octet, voir du mot (64 bits sur les processeurs modernes). Mais c’est pareil.
  2. En parlant de ça, puisque la ROM est non-modifiable, elle ne permet pas la sauvegarde, à l’inverse des mémoires volatiles qu’on verra plus loin. C’est pourquoi il y avait une pile dans les cartouches de Game Boy (et que la plupart ne fonctionnent plus aujourd’hui à moins de changer la pile).

Et voilà ! Il s’agissait d’un aperçu assez général et succin, et beaucoup plus pourrait être dit sur le sujet. Pour ceux que le sujet aurait intéressé, je leur conseille de faire un tour sur ce textbook (en), qui est, en plus d’être vachement complet, accessible gratuitement en ligne. Toute la section matériel et électronique se propose également à vous, dont l’incontournable tutoriel Arduino.

Sinon, après avoir été si bas dans les détails de l’électronique d’un ordinateur, il est éventuellement temps de regarder du côté du langage assembleur, qui vous permettrait d’apprendre comment un processeur (et un programmeur) réussi à exploiter tout ça. C’est également très intéressant, et il n’est pas impossible que je rajoute un jour une seconde partie à ce tutoriel pour expliquer ça.

D’ici là, bon amusement :pirate: