CHARLES PETZOLD

Le langage caché du matériel et

des logiciels de nos ordinateurs

Introduction

À l’époque de la première édition de ce livre en 19991, j’étais très satisfait d’avoir enfin écrit un ouvrage qui n’aurait pas besoin d’être mis à jour tous les deux ans comme cela avait été le cas de mon premier, consacré à la programmation d’applications pour Microsoft Windows. De même, le suivant, sur le système de l’interface utilisateur graphique OS/2 Presentation Manager (comment, ça ne vous dit rien ?), était vite devenu obsolète. En revanche, j’étais absolument certain que le présent livre serait éternel.

Mon idée de départ était de commencer par des concepts très simples, puis de progresser lentement vers une compréhension approfondie du fonctionnement d’un ordinateur numérique. Tout au long de l’ascension régulière de cette montagne de connaissances, je décidai d’utiliser le moins de métaphores, d’analogies et d’illustrations distrayantes possible, et de privilégier le langage et les symboles réellement employés par les ingénieurs qui conçoivent et construisent les ordinateurs. J’avais un tour de passe-passe très astucieux dans ma manche : j’allais passer par des technologies du passé pour démontrer des principes universels, en partant du principe que ces technologies anciennes ne vieilliraient jamais. C’était comme si j’écrivais un livre sur le moteur à essence en me fondant sur le modèle Ford T.

Je reste persuadé que mon approche a été la bonne, mais en me trompant sur certains détails. Au fil des ans, le livre a montré des signes d’obsolescence. Certaines références culturelles sont devenues caduques. Les téléphones tactiles et les tablettes ont remplacé petit à petit les claviers et les souris. Internet existait en 1999, mais à une échelle sans commune mesure avec aujourd’hui. Le standard Unicode d’encodage des textes (qui permet une représentation uniforme de tous les alphabets du monde, sans oublier les émoticônes) occupait moins d’une page dans la première édition. Et le langage de programmation JavaScript, devenu incontournable sur le Web, n’était même pas mentionné.

Un des objectifs que je n’avais pas réussi à atteindre dans la première édition est la démonstration du fonctionnement d’une unité centrale de traitement d’informations (ou CPU), c’est-à-dire un processeur. Dans ce livre, nous allons en découvrir les entrailles. Si vous commencez à perdre pied au cours de votre progression dans la lecture, faites une pause. Une fois que vous aurez assimilé le Chapitre 24, vous aurez acquis des bases solides et la fin du livre ne sera qu’une formalité.

Le site Web d’accompagnement

Tous les circuits importants décrits et illustrés dans le livre ont été construits sous la forme d’animations interactives organisées chapitre par chapitre sur un site Web dédié, accessible en français à l’adresse : code.lisez.com2. (Les connaisseurs de JavaScript pourront voir que j’ai utilisé l’élément canvas de HTML 5.)

Tout au long du livre, les schémas de circuits pour lesquels existent une version animée et une version interactive sont signalés par une icône en marge représentant un globe terrestre.

L’accès au site est totalement gratuit. Pour certains chapitres, il est possible que le dépôt d’un cookie soit demandé pour mieux faire fonctionner un exemple.

Le site peut également contenir d’éventuels errata concernant le livre et ses exemples.

Dédicace finale

Je dédie ce livre aux deux femmes les plus importantes de ma vie. D’abord à ma mère, qui a affronté des adversités qui auraient abattu une autre personne. Elle a donné une orientation forte à ma vie sans jamais me décourager. Nous avons fêté son 95e (et dernier) anniversaire durant la rédaction de ce livre. Ensuite, à ma femme, Deirdre Sinnott, qui a été essentielle et continue de me prodiguer son soutien et son amour.

Je dédie aussi ce livre aux lecteurs de la première édition ; leurs commentaires bienveillants furent très gratifiants.

Charles Petzold

1 N.d.T. : édition américaine non traduite en français.

2 N.d.T. : la version américaine du site étant consultable à l’adresse codehiddenlanguage.com.

À propos de l’auteur

Charles Petzold est également auteur d’un livre consacré au document historique du chercheur anglais Alan Turing qui posait les principes d’une machine de traitement d’information (The Annotated Turing : A Guided Tour through Alan Turing’s Historic Paper on Computability and the Turing Machine, Wiley, 2008). Il a écrit plusieurs ouvrages techniques portant essentiellement sur la programmation d’applications pour Microsoft Windows. Tous sont dépassés aujourd’hui. Charles vit à New York avec sa femme, l’historienne et romancière Deirdre Sinnott, et leurs deux chats. www.charlespetzold.com

code (nom masculin)

Du latin codex, « tablette pour écrire ».

[…]

5. Système conventionnel, rigoureusement structuré, de symboles et de règles combinatoires.

[…]

8. Ensemble d’instructions en langage machine ou symbolique constituant un programme.

— Larousse de la langue française

Chapitre 1  Les meilleurs amis

Vous avez 10 ans. Votre meilleur ami habite de l’autre côté de la rue, et vous pouvez voir la fenêtre de sa chambre depuis la vôtre. Tous les soirs, vos parents vous envoient au lit bien trop tôt, alors que vous avez encore bien des choses à partager : observations, secrets, ragots, blagues et rêves d’avenir. Quoi de plus normal ? Le besoin de communiquer est un trait éminemment humain.

Alors que la lumière est encore allumée dans vos chambres, vous et votre meilleur ami pouvez vous apercevoir par la fenêtre. Par de grands gestes, vous arrivez à transmettre un signal, mais tout échange plus nuancé est impossible. Et une fois que les parents ont décrété l’extinction des feux, votre technique gestuelle tombe à l’eau.

Comment continuer à communiquer ? Si vous avez la chance d’avoir obtenu un téléphone portable pour vos 10 ans, vous pourriez envisager un appel en chuchotant ou un SMS silencieux. Mais que faire si vos parents ont l’habitude de confisquer les téléphones portables et de couper le Wi-Fi à l’heure du coucher ? Une chambre sans communications électroniques devient un lieu coupé du monde.

Ce dont vous et votre meilleur ami pouvez disposer, c’est d’une lampe de poche. Tout le monde sait que les lampes de poche ont été inventées pour permettre aux enfants de lire des livres sous les couvertures, mais elles peuvent aussi servir à communiquer la nuit. Elles sont en tout cas silencieuses. Le rayon lumineux étant très directif, il ne fuira probablement pas sous la porte de la chambre au risque d’éveiller les soupçons.

Peut-on communiquer avec des lampes de poche ? Cela vaut certainement la peine d’essayer. Vous avez appris à tracer des lettres et des mots sur une ardoise ou du papier en maternelle ; il semble donc raisonnable de réutiliser ce savoir. Il vous suffit de vous placer derrière la fenêtre et de dessiner les lettres dans l’air avec votre lampe. Pour un O, vous allumez la lampe, vous tracez un cercle et vous éteignez. Pour un I, c’est un trait vertical. Mais, comme vous le découvrez rapidement, cette méthode est peu pratique. Pendant que vous regardez la lampe de votre ami tracer des courbes et des lignes dans l’air, vous vous rendez compte qu’il est trop difficile de regrouper mentalement les différents gestes. Ces tourbillons et ces traits de lumière ne sont tout simplement pas assez distincts.

Peut-être avez-vous vu un jour un film dans lequel deux marins communiquent d’un navire à l’autre en pleine mer à l’aide d’un projecteur à occultation ? Ou cette scène dans laquelle un espion agite un miroir pour renvoyer les rayons du soleil vers une pièce dans laquelle un autre espion est retenu prisonnier ? C’est peut-être la solution, mais elle suppose de mettre d’abord au point une convention simple : chaque lettre de l’alphabet va correspondre à une série de clignotements de lampe de poche. Le A correspondra à 1 clignotement, le B à 2 clignotements, le C à 3 clignotements, et ainsi de suite, jusqu’à 26 clignotements pour Z. Le mot BAC correspondra à 2, puis 1, puis 3 clignotements avec de courtes pauses entre les lettres pour que vous ne confondiez pas la série des 6 clignotements avec une lettre F. Vous marquez aussi des pauses un peu plus longues entre deux mots1.

Cela semble prometteur. La bonne nouvelle : il n’est plus nécessaire d’agiter la lampe de poche en l’air ; il suffit de pointer et d’allumer. La mauvaise nouvelle : dès qu’un message est un peu détaillé, par exemple « As-tu révisé le bac français ? », il faut plus d’une centaine de clignotements, sans compter la ponctuation ! Vous n’avez même pas encore décidé combien de clignotements correspondent à un point d’interrogation.

Mais vous n’êtes pas loin du but. Vous vous dites que quelqu’un a déjà dû être confronté à ce problème, et vous avez raison. En vous rendant à la bibliothèque ou en effectuant une recherche sur Internet, vous découvrez une merveilleuse invention connue sous le nom de code morse. C’est exactement ce que vous cherchiez, même si vous devez maintenant réapprendre à « écrire » tout l’alphabet.

Dans le système minimal vu plus haut, chaque lettre de l’alphabet correspond à un certain nombre de clignotements, de 1 pour la lettre A à 26 pour la lettre Z. Dans le code morse, il n’y a que deux types de clignotements : court ou long. Cela rend le code morse plus compliqué, bien sûr, mais en pratique, bien plus efficace. La phrase précédente ne demande plus qu’une quarantaine de clignotements (certains courts, d’autres longs) au lieu de plus de 100, et ce en y ajoutant un point d’interrogation.

Lorsque l’on décrit le fonctionnement du code morse, on ne parle pas de clignotements courts et longs, mais de points (dot) et de traits (dash), parce que c’est ainsi que sont imprimés les codes sur papier. En code morse, chaque lettre de l’alphabet correspond à une courte combinaison de points et de traits, comme le montre le tableau suivant.

Le code morse est bien antérieur à l’informatique, mais il constitue un bon moyen pour se familiariser avec le codage et mieux comprendre les langages de conception des logiciels et les mécanismes cachés dans les matériels.

Dans cet ouvrage, le mot code désigne de façon générale une technique ou une convention pour transmettre des informations soit entre deux personnes, soit entre une personne et un ordinateur, soit entre deux ordinateurs.

Un code sert à communiquer. Si certains codes sont secrets, d’autres ne le sont pas du tout, bien au contraire, puisqu’ils sont à la base de la communication humaine.

Les sons que nous émettons en parlant forment des mots et constituent un code intelligible pour quiconque entend notre voix et comprend notre langue. Nous appelons ce code le langage parlé ou la parole.

Les personnes sourdes et malentendantes ont développé différentes langues des signes fondées sur des mouvements des mains et des mimiques pour évoquer des lettres, des mots et des concepts. Les deux systèmes les plus répandus en Amérique du Nord sont l’ASL (American Sign Language), développé au début du XIXe siècle à l’American School for the Deaf, et la langue des signes québécoise (LSQ), qui est une variante de la langue des signes française (LSF) utilisée en France.

Nous utilisons un autre système de codage pour les mots écrits ou les textes rédigés. Le texte peut être écrit à la main, puis imprimé dans des journaux, des magazines et des livres, ou saisi et affiché numériquement sur divers appareils. Dans de nombreuses langues, il existe une forte correspondance entre oral et écrit. En anglais, par exemple, les lettres et les groupes de lettres correspondent (plus ou moins) aux sons prononcés.

Pour les personnes malvoyantes, l’écrit peut être remplacé par le braille, qui utilise un système de points en relief correspondant à des lettres, des groupes de lettres et des mots entiers. (Le braille sera abordé en détail au Chapitre 3.)

Lorsqu’il s’agit de transcrire très rapidement un discours en texte, on adopte la sténographie. Dans les tribunaux et dans le monde des médias, des sténographes utilisent une machine à sténotyper dotée d’un clavier simplifié pour créer des sous-titres en temps réel pour les journaux télévisés ou les émissions sportives.

Nous utilisons toute une série de codes différents pour communiquer entre nous, car certains sont plus pratiques que d’autres. Le code de la parole étant sonore, il ne peut être stocké tel quel sur papier : on utilise donc un code d’écriture. Mais l’échange silencieux d’informations à distance dans l’obscurité, notre problème à résoudre, n’est possible ni avec la parole ni avec l’écriture. Le code morse est donc une solution à envisager.

Un code est utile s’il remplit une fonction qu’aucun autre code ne peut remplir. Comme nous le verrons, plusieurs types de codes sont utilisés dans les ordinateurs pour stocker et communiquer des textes, des nombres, des sons, de la musique, des images fixes et animées, sans oublier les instructions machine destinées à l’ordinateur lui-même. Les ordinateurs ne peuvent pas facilement traiter les codes humains parce qu’ils ne peuvent pas reproduire précisément la façon dont les êtres humains utilisent leurs yeux, leurs oreilles, leur bouche et leurs doigts. Il n’est pas facile d’apprendre à parler à un ordinateur, et il reste encore ardu de bien lui faire comprendre la parole humaine.

Mais des progrès considérables ont été réalisés. Les ordinateurs sont désormais capables de saisir, stocker, manipuler et restituer de nombreux types d’informations utilisées dans la communication humaine, qu’il s’agisse d’informations visuelles (textes et images), sonores (paroles, sons et musique) ou d’une combinaison des deux (animations et films). Tous ces types d’informations nécessitent leurs propres codes.

Même le tableau du code morse que vous venez de voir est en quelque sorte un code. Le tableau montre que chaque lettre est représentée par une série de points et de traits. Bien sûr, il est impossible d’envoyer ces points et traits avec une lampe de poche : on les symbolise par des clignotements courts et longs.

Pour envoyer du morse avec une lampe de poche, il faut donc allumer et éteindre la lampe rapidement pour un point, et un peu plus lentement pour un trait. Pour envoyer un A, par exemple, il faut allumer et éteindre la lampe de poche rapidement, puis l’allumer et l’éteindre moins vite et finir par une pause avant de passer à la lettre suivante.

Par convention, la durée d’un trait doit valoir environ trois fois celle d’un point. La personne qui reçoit le message peut ainsi bien distinguer clignotement court et clignotement long et en déduire qu’il s’agit d’un A.

Les pauses entre les points et les traits du code morse sont cruciales. Lorsque vous envoyez un A, par exemple, la lampe de poche doit être éteinte entre le point et le trait pendant une durée égale à environ un point. Les lettres d’un même mot sont séparées par des pauses plus longues, d’une durée égale à celle d’un trait. Par exemple, voici le code morse pour dire « salut » avec les pauses entre les lettres :

Les mots sont séparés par une pause d’environ deux traits. Voici le code pour signifier « les amis » :

Les durées d’allumage et d’extinction de la lampe de poche ne sont pas fixes. Elles sont toutes des multiples de la durée d’un point, qui, elle, dépend de la rapidité avec laquelle l’interrupteur de la lampe de poche peut être manipulé et de la rapidité avec laquelle l’émetteur du message en morse sait « parler » en morse. Le trait d’un expéditeur chevronné pourra avoir la même durée que le point d’un débutant, ce qui pourrait rendre difficile le décodage d’un message en morse. En réalité, après une ou deux lettres, le récepteur du message comprend ce qu’est un point et ce qu’est un trait.

À première vue, la définition du code morse, c’est-à-dire la correspondance entre les séquences de points et de traits et les lettres de l’alphabet, semble aussi arbitraire que la disposition des touches d’un clavier d’ordinateur. Mais, à y regarder de plus près, ce n’est pas le cas. Les codes les plus simples, donc les plus courts, ont été attribués aux lettres de l’alphabet les plus fréquentes, telles que le E et le T. Les joueurs de Scrabble et les fans de La Roue de la fortune le remarquent illico. Les lettres moins courantes, comme Q et Z (qui rapportent 10 points au Scrabble et apparaissent rarement dans La Roue de la fortune), ont des codes plus longs.

Presque tout le monde connaît un mot de code morse : le signal international de détresse « SOS », trois points, trois traits et trois points. « SOS » n’est pas une abréviation ; il s’agit simplement d’une séquence morse facile à mémoriser et à détecter. Pendant la Seconde Guerre mondiale, la BBC ouvrait certaines émissions radio avec le début de la Cinquième Symphonie de Beethoven : TA, TA, TA, TAHHHHH. Beethoven ne savait pas, au moment où il composait cette musique, qu’elle serait un jour le code morse pour la lettre V (comme « Victoire »).

L’un des inconvénients du code morse est qu’il ne fait pas la différence entre majuscules et minuscules. Mais en complément des lettres, il prévoit des codes pour les nombres sous forme de variations sur une série de cinq points et traits :

Ces codes de chiffres sont un peu plus faciles à mémoriser que ceux des lettres. Quant aux signes de ponctuation, la plupart utilisent cinq, six ou sept points et traits :

Des codes supplémentaires sont définis pour les lettres accentuées de certaines langues européennes et comme séquences sténographiques à usage spécial. Le code SOS est l’une de ces séquences sténographiques : en effet, il est censé être envoyé en continu, avec une pause d’une durée d’un seul point entre les trois lettres et non de trois.

Vous aurez beaucoup moins de mal à échanger du code morse avec votre ami si vous disposez de lampes de poche spécialement conçues à cet effet. En plus de l’interrupteur coulissant normal à deux positions, ces lampes possèdent aussi un bouton-poussoir qu’il suffit de presser et de relâcher pour allumer et éteindre la lampe.

Avec un peu d’entraînement, vous pourrez peut-être atteindre une vitesse d’envoi et de réception de 5 à 10 mots/minute, ce qui est encore beaucoup plus lent que la parole (qui se situe aux alentours de 100 mots/minute), mais tout à fait suffisant.

Lorsque vous et votre meilleur ami aurez appris le code morse par cœur, vous pourrez même vous en servir vocalement comme parole codée. Pour une rapidité maximale, prononcez le point comme DI (et DIT pour le dernier point d’une lettre) et le trait comme DAH, par exemple DI-DI-DI-DAH pour la lettre V. Puisque le code morse réduit la langue écrite aux seuls points et traits, sa version parlée réduit la parole à deux sons vocaliques.

Le mot-clé ici est « deux » : deux types de clignotements ou deux voyelles. En fait, deux choses différentes peuvent, avec des combinaisons appropriées, transmettre tous les types d’informations.

1 N.d.T. : c’est le système SCOTT (Système de communication optique tous temps).

Chapitre 2  Codes et combinaisons

Le code morse a été inventé vers 1837 par l’Américain Samuel Finley Breese Morse (1791-1872), que nous rencontrerons plus en détail dans un prochain chapitre. Il a été amélioré par d’autres, notamment par son compatriote Alfred Vail (1807-1859) et a abouti à plusieurs versions. Le système décrit dans ce livre est généralement désigné sous le nom de code morse international.

L’invention du code morse va de pair avec l’invention du télégraphe, que j’évoquerai dans la suite de l’ouvrage. De même que le code morse constitue une bonne introduction aux codes, le télégraphe met en jeu du matériel pouvant imiter le fonctionnement d’un ordinateur.

Pour la plupart des gens, le code morse est plus facile à envoyer qu’à recevoir. Même si vous n’avez pas appris l’alphabet morse, vous pouvez utiliser le tableau classé par ordre alphabétique déjà vu dans le chapitre précédent :

Recevoir un code morse et le traduire en mots est beaucoup plus difficile et long que d’en envoyer, car il faut travailler à l’envers pour trouver la lettre qui correspond à une séquence codée particulière de points et de traits. Si vous n’avez pas mémorisé les codes et que vous receviez un message composé de la séquence « trait, point, trait, trait », vous devrez parcourir le tableau lettre par lettre avant de découvrir qu’il s’agit de la lettre Y.

Le problème est que nous disposons d’un tableau qui fournit la conversion dans un sens :

Lettre alphabétique → Séquence de points et de traits en morse

Mais nous n’avons pas de tableau qui permette la conversion en sens inverse :

Séquence de points et de traits en morse → Lettre alphabétique

Dans les premières séances d’apprentissage du code morse, un tel tableau serait certainement pratique, mais il n’est pas du tout évident de savoir comment le construire. Il n’y a rien dans ces points et ces traits que nous puissions ramener à un ordre alphabétique.

Oublions donc l’ordre alphabétique. Une meilleure approche serait de regrouper les codes en fonction du nombre de points et de traits qui les constituent. Par exemple, une séquence de code morse qui ne contient qu’un seul point ou un seul trait ne peut représenter que l’une des deux lettres E ou T :

Une combinaison d’exactement deux points et/ou traits fournit 4 lettres supplémentaires : I, A, N et M.

En combinant trois points ou traits, on obtient 8 lettres supplémentaires :

Enfin (si nous arrêtons l’exercice avant d’aborder les chiffres et les signes de ponctuation), les séquences de quatre points et traits permettent d’obtenir 16 caractères supplémentaires :

Au total, ces quatre tableaux contiennent 2 plus 4 plus 8 plus 16 codes pour un total de 30 lettres, soit 4 de plus que les 26 lettres de l’alphabet latin. Vous remarquerez que 4 des codes du dernier tableau ci-dessus servent à des lettres accentuées : les U, A et O tréma et un S avec cédille.

Ces quatre tableaux peuvent certainement vous aider à décoder du morse. Pour chaque lettre reçue, vous comptez le nombre de points et de traits et consultez le bon tableau parmi les quatre. Chaque tableau est organisé de manière méthodique, en commençant par le code « que des points » en haut à gauche et en terminant par le code « que des traits » en bas à droite.

Voyez-vous une évolution se dessiner dans la taille des tableaux ? Chaque tableau contient deux fois plus de codes que le précédent. C’est logique : chaque tableau part de tous les codes du tableau précédent en y ajoutant tantôt un point, tantôt un trait.

Nous pouvons résumer cette tendance intéressante ainsi :

Nombre de points et de traits
Nombre de codes possibles

1

2

2

4

3

8

4

16

Le nombre de codes possibles double d’un tableau au suivant. Si le premier tableau possède 2 codes, le deuxième tableau en possède 2 × 2 et le troisième tableau, 2 × 2 × 2. Voici une autre façon de le montrer :

Nombre de points et de traits
Nombre de codes possibles

1

2

2

2×2

3

2×2×2

4

2×2×2×2

Puisqu’il s’agit de multiplier un nombre par lui-même, nous pouvons utiliser l’opération d’exponentiation et écrire des puissances de 2. Par exemple, 2 × 2 × 2 × 2 peut s’écrire 2⁴ (2 à la puissance 4). 2, 4, 8 et 16 sont tous des puissances de 2 : vous les calculez en multipliant 2 par lui-même. Le tableau précédent peut être reformulé de la manière suivante :

Nombre de points et de traits
Nombre de codes possibles

1

2¹

2

2²

3

2³

4

2⁴

Ce tableau est devenu très simple. Le nombre de codes est simplement égal à 2 élevé à la puissance correspondant au nombre de points et de traits :

Nombre de codes = 2 (nombre de points et de traits)

Les puissances de 2 ont tendance à apparaître souvent dans les codes, et en particulier dans ce livre. Vous en verrez un autre exemple dans le chapitre suivant.

Pour faciliter le décodage du code morse, vous pouvez vous servir d’un diagramme en arborescence.

Figure 2.1

Ce diagramme montre les lettres qui correspondent à chaque séquence particulière de points et de traits. Pour décoder l’une d’elles, il suffit de descendre dans l’arbre en suivant les flèches, de gauche à droite. Supposons, par exemple, que nous voulions connaître la lettre qui correspond à la séquence point-trait-point. Commencez par la gauche et sélectionnez la branche du point ; progressez vers la droite le long des flèches et choisissez un trait, puis un autre point et arrêtez là : c’est la lettre R.

À bien y réfléchir, un tel tableau avait sans doute été construit pour établir le code morse, d’abord pour éviter d’utiliser le même code pour deux lettres ! Deuxièmement, on est ainsi certain d’utiliser tous les codes possibles sans créer de séquences de points et de traits inutilement longues.

En prenant le risque d’étendre ce tableau au-delà des capacités d’une page imprimée, nous pourrions le compléter avec les codes à cinq points et traits. Une séquence d’exactement cinq points et traits nous donne 32 codes supplémentaires (2 × 2 × 2 × 2 × 2, soit 2⁵). Normalement, cela devrait suffire pour les 10 chiffres et les 16 symboles de ponctuation définis dans le code morse. Effectivement, les chiffres sont codés avec cinq points et traits. Beaucoup d’autres codes qui utilisent une séquence de cinq points et traits représentent des lettres accentuées plutôt que des signes de ponctuation.

Pour inclure tous les signes de ponctuation, il faut étendre le système à six points et traits, ce qui donne 64 codes (soit 2⁶) supplémentaires pour un total de 2 + 4 + 8 + 16 + 32 + 64, soit 126 possibilités. C’est trop pour le code morse, qui laisse de ce fait un grand nombre de codes longs indéfinis (c’est-à-dire qui ne signifient rien).

Si vous recevez un message en morse et détectez un code indéfini, vous pouvez être certain qu’il s’agit d’une erreur de codage.

En nous appuyant sur cette petite formule :

Nombre de codes = 2 (nombre de points et de traits)

nous pouvons prévoir le nombre de codes possibles en utilisant des séquences plus longues :

Nombre de points et de traits
Nombre de codes possibles

1

2¹ = 2

2

2² = 4

3

2³ = 8

4

2⁴ = 16

5

2⁵ = 32

6

2⁶ = 64

7

2⁷ = 128

8

2⁸ = 256

9

2⁹ = 512

10

2¹⁰ = 1 024

Heureusement, il n’est pas nécessaire d’écrire tous les codes possibles pour en déterminer le nombre. Tout ce que nous avons besoin de savoir faire, c’est de multiplier 2 par lui-même de nombreuses fois.

On dit que le code morse est binaire (ce qui signifie littéralement « qui va par deux ») parce que les composants du code ne sont constitués que de deux choses : le point et le trait. Il en va comme avec une pièce de monnaie qui ne peut tomber que sur pile ou sur face. Lancée dix fois de suite, cette dernière produit une séquence de piles et de faces parmi les 1 024 possibles.

Les combinaisons d’objets binaires (comme les pièces de monnaie) et de codes binaires (comme le morse) sont toujours décrites avec des puissances de 2. Ce chiffre 2 va rester omniprésent dans l’ensemble de ce livre.

Chapitre 3  Braille et codes binaires

© Ullstein bild Dtl/Getty Images

La première personne ayant réussi à traduire les lettres d’une langue écrite en un code interprétable n’est pas Samuel Morse. Ce n’est pas non plus le premier dont on se souvient davantage du nom comme celui d’une technique que pour lui-même.

Cet honneur revient à un adolescent français devenu aveugle à l’âge de 5 ans. Bien que né quelque dix-huit ans après Morse, il s’est fait connaître beaucoup plus tôt. On sait peu de choses sur sa vie personnelle, mais ce que l’on sait constitue une histoire passionnante.

Louis Braille est né en 1809 à Coupvray, à une journée de marche à l’est de Paris, d’un père bourrelier. À 5 ans (âge où l’on ne devrait pas laisser un jeune garçon jouer seul dans l’atelier de son père), il se plante par accident un outil pointu dans l’œil droit. La plaie ne se résorbe pas, l’infection se propage à l’autre œil, et le garçon devient totalement aveugle. En ces temps, une personne qui subissait un tel sort était condamnée à une vie d’ignorance et de pauvreté, mais l’intelligence du jeune Louis et son désir d’apprendre lui assurèrent un autre destin.

Louis fréquente d’abord l’école du village avec les autres enfants, puis, grâce à l’intervention du curé du village et du maire, il est envoyé à 10 ans à l’Institution royale des jeunes aveugles à Paris.

Le principal obstacle à l’éducation des enfants aveugles était leur incapacité à lire les livres imprimés. Valentin Haüy (1745-1822), le fondateur de l’institution de Paris, avait inventé un système de gaufrage des lettres en relief sur le papier dans une grande police de caractères arrondie qui pouvait être lue au toucher. Ce système était complexe à utiliser, et seuls quelques livres avaient été produits selon cette méthode.

Le bien-voyant Haüy était limité par son absence de handicap : pour lui, un A était tout simplement un A, et la lettre A des aveugles devait ressembler à un A. (Si on lui avait donné une lampe de poche pour communiquer, il aurait peut-être essayé de dessiner des lettres dans l’air, comme nous l’avons fait avant de découvrir les limites de cette technique.) Haüy n’avait probablement pas envisagé un code très différent des lettres en relief qui serait mieux adapté aux personnes non voyantes.

Les origines d’un autre type de code proviennent d’une source inattendue. En 1815, Charles Barbier de La Serre, capitaine de l’armée française, avait mis au point un système d’écriture appelé plus tard « écriture nocturne ». Ce système utilisait un motif de points en relief sur du papier épais et on pensait qu’il le destinait aux soldats pour se transmettre des notes dans l’obscurité et en silence. Les points étaient créés dans le papier à l’aide d’un stylet en forme d’alène et pouvaient ensuite être relus avec les doigts.

Louis Braille se familiarisa avec le système de Barbier à l’âge de 12 ans, appréciant notamment l’utilisation des points en relief, non seulement pour la facilité de lecture avec les doigts, mais aussi pour la facilité d’écriture. Un élève de la classe équipé de papier et d’un stylet pouvait en effet prendre des notes et les relire.

Braille travailla assidûment à l’amélioration du système et, en l’espace de trois ans (à l’âge de 15 ans), il mit au point son propre système, dont les bases sont celles encore en vigueur de nos jours. Pendant de nombreuses années, le système ne fut connu qu’au sein de l’institution, mais il se répandit peu à peu dans le reste du monde. En 1835, Louis Braille contracta la tuberculose, qui le tuera peu après son 43e anniversaire en 1852.

Aujourd’hui, les différentes versions du système braille sont en concurrence avec les livres audio pour ce qui est de l’accès des personnes non voyantes ou malvoyantes à l’écrit, mais le braille reste un système inestimable et le seul moyen de lire pour les personnes à la fois aveugles et sourdes. Au cours des dernières décennies, le braille est devenu plus familier au grand public, car les boutons d’ascenseurs et les touches de distributeurs de billets, notamment, sont dotés de code braille par souci d’accessibilité.

Dans ce chapitre, je vais décortiquer le code braille et vous montrer comment il fonctionne. Vous n’avez pas besoin d’apprendre le braille ou de mémoriser quoi que ce soit. Le seul but de cet exercice est de vous donner un autre aperçu de ce qu’est un code.

En braille, chaque symbole utilisé dans le langage écrit habituel, en particulier les lettres, les chiffres et les signes de ponctuation, est codé sous la forme de un à six points en relief dans les limites d’une matrice de deux unités de large sur trois de haut. Les points de la cellule sont généralement numérotés de 1 à 6 :

Des machines à écrire spéciales ont été mises au point pour embosser les points braille dans le papier et, de nos jours, des embosseuses informatisées font le travail. Étant donné que le coût de l’embossage en braille de quelques pages de ce livre serait prohibitif, j’ai utilisé une notation courante à plat pour représenter le braille sur la page imprimée. Dans cette notation, parmi les six points de la cellule, les gros points indiquent ceux en relief et les petits points indiquent ceux qui sont plats. Considérons le caractère braille suivant :

Les points 1, 3 et 5 sont en relief et les points 2, 4 et 6 ne le sont pas. Ce qui devrait nous intéresser à ce stade, c’est que ces points sont binaires. Chaque point est soit plat (absent), soit en relief. Cela signifie que nous pouvons appliquer au braille ce que nous avons appris sur le code morse et les combinaisons binaires. Il y a six points et chaque point peut prendre deux états. Le nombre total de combinaisons de six points est donc de 2 × 2 × 2 × 2 × 2 × 2, soit 2⁶ ou 64.

Le système braille est donc capable de représenter 64 codes différents. Les voici tous :

Il n’est pas nécessaire d’utiliser les 64 codes en braille, mais c’est la limite supérieure imposée par un code binaire à six éléments.

Pour commencer à analyser ce code, examinons l’alphabet minuscule de base :

Par exemple, la phrase « toi et moi » en braille donne ceci :

Vous remarquez que les cellules des lettres d’un mot sont séparées par un petit espace, alors qu’un espace plus grand (de la largeur d’une cellule) est ménagé entre deux mots.

C’est la base du braille tel que Louis Braille l’avait conçu, ou du moins tel qu’il s’applique aux lettres de l’alphabet latin. Louis Braille avait également prévu des codes pour les lettres accentuées, fréquentes en français. Vous remarquerez qu’il n’y a pas de code pour la lettre w, qui n’était pas employée en français classique. (À ce stade, seules 25 des 64 possibilités de codage ont donc été utilisées.)

En y regardant de plus près, vous découvrirez un motif dans les codes braille des 25 lettres en minuscules. La première rangée (lettres de a à j) n’utilise que les quatre points du haut de la cellule (points 1, 2, 4 et 5). La deuxième rangée (lettres de k à t) reprend la même séquence de motifs en ajoutant en relief le point 3 en bas à gauche. La troisième rangée (lettres de u à z) reprend le même motif en ajoutant en relief les deux points du bas, 3 et 6.

À l’origine, Louis Braille avait conçu son système pour qu’il soit perforé à la main. Il savait que cela ne serait probablement pas très précis, et il a donc intelligemment défini les 25 lettres minuscules en sorte de réduire les ambiguïtés. Par exemple, sur les 64 codes braille possibles, six ont un point en relief. Quatre des 64 codes ont deux points verticaux adjacents, mais là encore, un seul est utilisé pour la lettre b. Trois codes ont deux points horizontaux adjacents, mais un seul est utilisé pour la lettre c. Ce que Louis Braille a réellement défini, c’est une collection de formes uniques qui peuvent légèrement se décaler sur la page tout en signifiant toujours la même chose. Un a correspond à un point en relief, un b à deux points adjacents verticalement, un c à deux points adjacents horizontalement, et ainsi de suite.

Les codes sont souvent sujets à des erreurs. Lorsque l’erreur se produit lors de l’écriture d’un code (par exemple, lorsqu’un étudiant en braille grave des points sur du papier), c’est une erreur d’encodage. Une erreur commise à la lecture du code est une erreur de décodage. La troisième catégorie d’erreurs est celle des erreurs de transmission, par exemple lorsqu’une page contenant du braille est altérée.

Les codes plus sophistiqués intègrent souvent différents types de corrections d’erreur. En ce sens, le braille, tel qu’il a été défini à l’origine par Louis Braille, est un système de codage sophistiqué : il utilise la redondance pour supporter une certaine imprécision dans la perforation et la lecture des points.

Au fil du temps, le code braille a été enrichi de diverses manières, notamment pour permettre la notation des mathématiques et de la musique. Il a surtout cherché à réduire la consommation de papier et accélérer la lecture en définissant des abréviations. Actuellement, le système le plus souvent employé est le braille abrégé. Dans ce système, lorsqu’un code de lettre apparaît seul, il désigne non une lettre mais un des mots les plus fréquents tels que « nous », « vous », « bien », « de », « faire », « qui », « sur », etc. Les trois lignes suivantes montrent les principaux codes de mots :

Ainsi, « que faire sans vous » peut être écrit en braille abrégé, soit 4 codes braille au lieu de 16 :

Les codes numériques des chiffres 0 à 9 de la base décimale sont les mêmes que ceux des lettres de a à j, mais doivent être précédés d’un code spécial de basculement en chiffres (le modificateur mathématique, un point 6 seul) :

Ainsi, la séquence de codes suivante signifie le nombre 256 :

Il existe deux conventions pour le préfixe de basculement en chiffres : la notation historique braille qui utilise la matrice avec les points 3, 4, 5 et 6, et la notation Antoine recommandée dorénavant, qui n’utilise que le point 6 pour le préfixe.

Les points 4 et 6 forment l’indicateur de passage en majuscules de la lettre qui suit le code. Par exemple, nous pouvons écrire le nom de famille du créateur original de ce système comme ceci :

Cette séquence commence par l’indicateur de majuscule, suivi des lettres b, r, a, i, l, l et e. (Dans l’usage, le nom pourrait être encore plus abrégé en éliminant les deux dernières lettres, qui ne se prononcent pas.)

L’indicateur de majuscule en braille signifie que la lettre suivante (et seulement cette lettre) est en majuscule. Un tel code est aussi appelé code d’échappement (escape). Les codes d’échappement permettent d’échapper à l’effet normal d’un code pour l’interpréter différemment. Les codes de décalage et les codes d’échappement sont courants lorsque les langues écrites sont représentées par des codes binaires, mais ils peuvent introduire des complexités car les codes individuels ne peuvent pas être interprétés correctement sans connaître les codes précédents.

Dès 1855, des pratiquants du braille ont commencé à étendre le système avec une rangée supplémentaire de deux points. Le braille à huit points a été utilisé à des fins particulières, telles que la musique, la sténographie et les caractères japonais kanji. Comme il élève le nombre de codes uniques à 2⁸, soit 256 codes, il s’avère pratique dans les applications informatiques, puisqu’il permet d’attribuer un code unique à chaque lettre minuscule et majuscule, chiffre et signe de ponctuation, sans les inconvénients des codes de décalage et d’échappement1.

1 N.d.T. : pour en savoir plus, consultez le manuel disponible à l’adresse https://www.avh.asso.fr/sites/default/files/manuel_abrege2013_complet_noir_0.pdf

Chapitre 4  Anatomie d’une lampe de poche

Les lampes de poche ont de nombreux usages, parmi lesquels la lecture sous la couette et l’envoi de messages codés à son copain. Cette même banale lampe de poche domestique peut également se montrer très pertinente dans le cadre d’une action pédagogique à propos de l’étrange matière qu’est l’électricité.

L’électricité est un phénomène étonnant, omniprésent tout en restant largement mystérieux même pour les personnes qui prétendent savoir comment cela fonctionne. Heureusement, il suffit de saisir quelques concepts de base pour comprendre comment l’électricité est mise à profit dans les ordinateurs.

La lampe de poche classique est certainement l’un des appareils électriques les plus simples que l’on trouve dans la plupart des foyers. Si vous démontez une lampe de poche classique, vous constatez qu’elle se compose d’une ou plusieurs piles, d’une ampoule, d’un interrupteur, de quelques pièces métalliques et d’un boîtier qui maintient le tout ensemble.

De nos jours, la plupart des lampes de poche fonctionnent grâce à des diodes électroluminescentes (LED). Un des avantages des anciennes ampoules est de permettre de voir à l’intérieur de l’ampoule en verre :

C’est ce qu’on appelle une ampoule à incandescence. La plupart des Américains pensent que l’ampoule à incandescence a été inventée par Thomas Edison, tandis que les Britanniques sont tout à fait certains que c’est Joseph Swan qui en est l’auteur. En réalité, plusieurs autres scientifiques et inventeurs ont fait des progrès décisifs avant qu’Edison ou Swan ne s’en mêlent.

À l’intérieur de l’ampoule se trouve un filament de tungstène qui rougit sous l’effet du passage des électrons. L’ampoule est remplie d’un gaz inerte pour éviter que le tungstène ne se consume en chauffant. Les deux extrémités du filament sont soudées à des fils fins qui sont reliés au châssis de l’ampoule et au contact central en bas d’ampoule.

Vous pouvez fabriquer votre propre lampe de poche en vous débarrassant de tout ce qui se trouve à l’intérieur, à l’exception de la pile et de l’ampoule. Vous aurez également besoin de petits morceaux de fil isolé (dénudés aux extrémités) et de suffisamment de mains pour tenir le tout ensemble :

Remarquez les deux extrémités libres des fils sur la droite du schéma. C’est notre embryon d’interrupteur. En supposant que la pile est bonne et que l’ampoule n’est pas grillée, le fait de faire se toucher ces extrémités libres doit faire briller la lampe :

Dans ce livre, nous utilisons la couleur rouge pour les fils quand l’électricité est présente et circule, par exemple pour allumer une ampoule.

Ce que nous avons construit ici est un circuit électrique simple, et la première chose remarquable est qu’un circuit doit être continu et former un chemin circulaire. L’ampoule ne s’allumera que si le chemin entre la pile, le fil, l’ampoule, l’interrupteur et l’autre borne de la même pile est continu. Toute rupture dans ce circuit entraîne l’extinction de l’ampoule. Le but de l’interrupteur est de contrôler ce processus.

La nature circulaire du circuit électrique suggère que quelque chose se déplace dans le circuit, comme de l’eau coulant dans des tuyaux. L’analogie de l’eau et des tuyaux est assez courante dans les explications du fonctionnement de l’électricité, mais elle a ses limites, comme toute analogie. L’électricité ne ressemble à rien d’autre dans cet univers et nous devons l’affronter selon ses termes propres.

L’une des approches permettant de comprendre le fonctionnement de l’électricité est la théorie électronique, qui explique l’électricité par le mouvement de ces électrons.

Comme nous le savons, toute la matière – celle que nous pouvons voir et sentir (en général) – est composée d’éléments extrêmement petits appelés atomes. Chaque atome est composé d’au plus trois types de particules : les neutrons, les protons et les électrons. L’atome est parfois représenté comme un petit système solaire, avec les neutrons et les protons liés dans un noyau et les électrons tournant autour du noyau comme des planètes autour d’un soleil. C’est un modèle obsolète.

Le nombre d’électrons dans un atome est en général le même que celui des protons. Dans certaines circonstances, les électrons peuvent être délogés de leurs atomes. C’est ainsi que se produit l’électricité.

Les termes électron et électricité dérivent tous deux du grec ancien ηλεκτρον (elektron), qui désigne l’ambre, cette sève d’arbre durcie et translucide ressemblant à du verre. La raison de cette relation improbable est que les Grecs de l’Antiquité avaient expérimenté le frottement d’un bloc d’ambre avec de la laine, ce qui produit ce que nous appelons aujourd’hui l’électricité statique. Le frottement permet à la laine de capturer les électrons de l’ambre. La laine est alors chargée, avec plus d’électrons que de protons ; l’ambre a alors moins d’électrons que de protons. Des millions d’expériences d’électricité statique sont réalisées tous les jours par tous ceux qui marchent sur de la moquette qui capte les électrons des semelles de nos chaussures.

On dit que le proton a une charge positive (+) et que l’électron a une charge négative ( – ), mais ces symboles ne signifient pas « plus » et « moins » au sens arithmétique du terme, ni que les protons ont quelque chose à offrir ou possèdent quelque chose que les électrons n’ont pas.

Les symboles + et – indiquent simplement que les protons et les électrons sont opposés d’une certaine manière. Cette caractéristique opposée se manifeste dans la façon dont les protons et les électrons interagissent.

Les protons et les électrons sont plus heureux et plus stables lorsqu’ils sont en nombre égal. Un déséquilibre entre protons et électrons cherche toujours à se corriger. Lorsque la moquette absorbe les électrons de vos chaussures, l’équilibre est restauré dès que vous touchez quelque chose de métallique et ressentez une étincelle. Cette étincelle est de l’électricité statique : elle signe le mouvement des électrons par un chemin plutôt détourné qui va de la moquette à vos chaussures en passant par votre corps.

L’électricité statique ne se limite pas aux petites étincelles produites par les doigts touchant des poignées de porte. Pendant les orages, le bas des nuages accumule des électrons tandis que le haut des nuages en perd ; le déséquilibre est finalement compensé par un éclair. La foudre est constituée d’un grand nombre d’électrons qui se déplacent très rapidement.

L’électricité dans le circuit d’une lampe de poche est beaucoup mieux contrôlée que celle d’une étincelle ou de la foudre. La lumière est émise de façon régulière et continue parce que les électrons ne sautent pas d’un endroit à l’autre à la vitesse de la lumière. Lorsqu’un atome du circuit perd un électron au profit d’un atome voisin, il récupère un électron d’un autre voisin, qui en emprunte un à un autre voisin, et ainsi de suite. L’électricité dans un circuit est une circulation d’électrons entre atomes.

Ce flux d’électrons ne se produit pas par magie. Il ne suffit pas de câbler un ramassis d’objets métalliques et s’attendre à ce qu’il y ait de l’électricité. Nous avons besoin de quelque chose pour générer le déplacement des électrons dans le circuit. Si l’on se reporte à notre diagramme de la lampe de poche minimaliste, nous pouvons supposer sans risque que ce n’est ni le fil, ni la lampe qui sont capables de produire du courant. Il s’agit donc probablement de la pile (électrique, justement).

Les piles utilisées dans les lampes de poche sont généralement cylindriques et identifiées par des lettres telles que D, C, AA et AAA selon leur taille et donc leur capacité. La borne plate de la pile est marquée du signe moins ( – ) ; l’autre, dotée d’une petite protubérance centrale, est marquée du signe plus (+).

Une pile électrique produit de l’électricité au moyen d’une réaction chimique entre des substances choisies de manière que la réaction entre eux provoque un surcroît d’électrons du côté de la pile marqué du signe moins (appelé pôle négatif ou anode) et un manque d’électrons sur l’autre pôle (le pôle positif ou cathode). Il en résulte une différence de potentiel entre les deux pôles, donc une tension. La pile convertit de l’énergie chimique en énergie électrique1.

Une pile bâton AA ou AAA telle que celles utilisées dans les lampes de poche produit une différence de potentiel d’environ 1,5 volt. Pour obtenir 3 volts, on en place deux en série. J’y reviens un peu plus loin.

La réaction chimique ne peut avoir lieu que si, d’une manière ou d’une autre, des électrons supplémentaires peuvent être extraits du pôle négatif de la batterie et renvoyés vers la borne positive. Il faut créer un chemin reliant les deux bornes, c’est-à-dire un circuit électrique. Dans le schéma suivant, les électrons se déplacent dans le circuit en sens inverse des aiguilles d’une montre.

On peut se demander si les électrons des produits chimiques contenus dans la pile sont bien compatibles avec ceux qui constituent le fil de cuivre. Aucun souci : tous les électrons de l’univers sont identiques.

Remarquez que les deux piles sont orientées dans le même sens : le pôle + de la pile du bas accueille les électrons du pôle – de la pile du haut.

C’est comme si les deux piles avaient été combinées en une seule pile plus grande avec une borne positive à une extrémité et une borne négative à l’autre extrémité. La batterie combinée offre une tension de 3 volts au lieu de 1,5 volt.

Si nous mettons les piles tête-bêche, rien ne se passe (pas de dégâts) et le circuit ne fonctionnera pas :

En effet, dans ce montage, les deux pôles positifs sont tous deux en attente d’électrons. Ils peuvent attendre longtemps. En revanche, rien n’empêche de relier les deux piles en parallèle : les deux pôles positifs de la pile sont interconnectés, et les deux pôles négatifs aussi :

Ce raccordement fonctionne. Les piles sont connectées en parallèle plutôt qu’en série comme précédemment. La tension combinée reste égale à 1,5 volt, ce qui correspond à la tension nominale de chaque pile. La lampe continuera peut-être à briller, mais pas aussi intensément qu’avec deux piles en série. La différence est que la lampe brillera deux fois plus longtemps, parce que la quantité d’énergie disponible dans deux piles est deux fois celle d’une pile.

On considère généralement qu’une pile a comme rôle de fournir de l’électricité à un circuit. Mais nous avons vu que nous pouvons aussi considérer que c’est le circuit qui permet aux réactions chimiques de la pile de se produire. Le circuit prend des électrons du pôle négatif de la pile et les conduit jusqu’à son pôle positif. Les réactions dans la pile se poursuivent jusqu’à ce que tous les produits chimiques soient épuisés, moment auquel vous vous débarrassez proprement de la pile ou décidez de la recharger (on parle dans ce cas d’un accumulateur).

Les électrons vont d’un pôle de la pile à l’autre en passant par l’extérieur (il y a un isolant dans la pile) : ils circulent dans le fil et passent par la lampe. Mais pourquoi avons-nous besoin de fils ? L’électricité ne peut-elle pas simplement circuler dans l’air ? La réponse est normande : oui et non. Oui, l’électricité peut circuler dans l’air (en particulier dans l’air humide) ; sinon, nous ne verrions pas d’éclairs. Mais l’électricité ne circule pas facilement dans l’air.

Certaines substances sont nettement plus aptes que d’autres à transporter (conduire) l’électricité. La capacité d’un élément à propager de l’électricité est liée à sa structure subatomique. Les électrons entourent le noyau sur différents niveaux, appelés orbites. Un atome qui n’a qu’un seul électron dans son orbite extérieure peut facilement le céder à un collègue, et c’est ce qui permet de transporter l’énergie électrique. Les substances propices au transport de l’électricité sont appelées des conducteurs. Les meilleurs conducteurs sont le cuivre, l’argent et l’or. Ce n’est pas un hasard puisque ces trois éléments se trouvent dans la même colonne du tableau périodique. Le cuivre est la substance la plus utilisée pour fabriquer des fils.

Le contraire de la conductivité est la résistance. Certaines substances sont plus résistantes que d’autres au passage de l’électricité. Une substance qui a une résistance très élevée, c’est-à-dire qui ne conduit pas bien l’électricité, est un isolant. Le caoutchouc et le plastique sont de bons isolants, et c’est pourquoi ces substances sont souvent utilisées pour gainer les fils de cuivre. Le tissu et le bois sont également de bons isolants, tout comme l’air sec. Pour autant, presque toute matière peut être forcée à conduire l’électricité si la tension appliquée est suffisamment élevée.

Le cuivre a une très faible résistance, mais elle n’est pas nulle. Cette résistance est proportionnelle à la longueur du fil. Si vous essayez d’alimenter une lampe de poche à partir d’une pile située à plusieurs kilomètres (quelle idée !), la résistance sera telle que la lampe de poche ne s’allumera même pas.

En revanche, et cela peut étonner de prime abord, plus le diamètre du fil est important (sa section), plus sa résistance est faible. On pourrait penser qu’un fil épais nécessite beaucoup plus d’électricité pour être « rempli ». En réalité, le diamètre du fil permet à beaucoup plus d’électrons de se déplacer en même temps.

J’ai mentionné le terme de tension, mais je ne l’ai pas défini. Qu’est-ce que cela signifie qu’une pile ait une tension de 1,5 volt2 ? La tension est en effet un concept fondamental de l’électricité délicat à présenter. Elle fait référence à une énergie potentielle de travail physique. La tension existe dans une pile même si elle n’est connectée à aucun circuit.

Prenons l’exemple d’une brique. Posée sur le sol, la brique a un potentiel très faible, mais elle en a plus si tenue dans votre main à un mètre du sol. Pour réaliser ce potentiel, il suffit de laisser tomber la brique. Tenue dans votre main par une fenêtre du dernier étage d’un grand immeuble, la brique a encore plus de potentiel. Dans les trois cas, vous tenez la brique et elle ne fait rien, mais son potentiel est différent.

Un concept beaucoup plus simple de l’électricité est celui de courant. Le courant est le nombre d’électrons qui circulent dans le circuit. L’unité de mesure du courant est l’ampère, du nom d’André-Marie Ampère (1775-1836). On dit « un fusible de 10 ampères ». Faire circuler un ampère de courant dans un fil représente plus de 6 milliards de milliards d’électrons qui se déplacent en une seconde. C’est le chiffre 6 suivi de 18 zéros.

L’analogie entre l’eau et les tuyaux nous sera utile ici. Le courant peut se comparer au débit d’un tuyau, soit la quantité d’eau qui s’écoule pendant un temps déterminé. La tension correspond à la pression de l’eau. La résistance est apparentée à la largeur du tuyau : plus son diamètre est faible, plus sa résistance est élevée. Ainsi, la quantité d’eau qui pourra s’écouler va augmenter si on augmente la pression. La quantité d’eau qui s’écoule dans un tuyau (le courant) est directement proportionnelle à la pression de l’eau (la tension) et inversement proportionnelle à la section du tuyau (sa résistance)3.

En électricité, vous pouvez calculer la quantité de courant qui va circuler dans un circuit si vous connaissez la tension et la résistance. La résistance (la force avec laquelle un matériau s’oppose au flux d’électrons) se mesure en ohms, d’après Georg Simon Ohm (1789-1854), qui a également proposé la célèbre loi d’Ohm qui s’énonce comme suit :

I=U/R

La lettre I est par convention utilisée pour symboliser le courant en ampères, la lettre U est utilisée pour représenter la tension (c’est la force électromotrice, FEM) et la lettre R correspond à la résistance.

Prenons l’exemple d’une pile qui n’est au départ connectée à rien :

La tension U est égale à 1,5 V. C’est son énergie potentielle. Mais comme les pôles positif et négatif ne sont reliés que par de l’air, la résistance (symbole R) est très élevée, ce qui signifie que le courant (I) est égal à 1,5 volt divisé par un très grand nombre. Le courant est donc quasiment nul.

Relions maintenant les pôles positif et négatif à l’aide d’un court fil de cuivre (à partir de maintenant, l’isolant des fils sera sous-entendu) :

Ce schéma est ce qu’on appelle un court-circuit. La tension est toujours de 1,5 V, mais la résistance est maintenant très, très faible. Le courant est égal à 1,5 V divisé par un très petit nombre. Cela signifie que le courant est très élevé. De très nombreux électrons circuleront dans le fil. Dans la pratique, le courant va plafonner à cause de contraintes physiques de la pile qui ne pourra probablement pas fournir autant de courant que possible en théorie ; la tension va tomber au-dessous de 1,5 V. Si la batterie a suffisamment de capacité, le fil aura le temps de chauffer car l’énergie électrique est convertie en chaleur à cause de la résistance (comme dans un grille-pain). Le fil pourrait devenir incandescent et fondre.

Dans la pratique, les circuits sont utiles en se situant entre ces deux extrêmes. Nous pouvons symboliser le plus simple qui soit ainsi :

Les ingénieurs en électricité reconnaissent la ligne en zigzag comme le symbole du composant appelé résistance. Il signifie que le circuit comporte une résistance de valeur moyenne.

Si un fil a une résistance faible mais non nulle, son métal peut devenir chaud et commencer à rougir. C’est ainsi que fonctionne une ampoule à incandescence.

Le filament que l’on trouve généralement dans les ampoules à incandescence des anciennes lampes de poche possède une résistance d’environ 4 ohms. Si la lampe de poche utilise deux piles bâton en série (ce qui donne une tension de 3 V), le courant sera de 3 V divisés par 4 ohms, soit 0,75 A (ampère) ou 750 milliampères (mA). Cela signifie que plus de 4,5 milliards de milliards d’électrons circulent dans l’ampoule chaque seconde. La résistance du filament transforme l’énergie électrique en lumière et en chaleur.

Une autre mesure essentielle en électricité est la puissance mesurée en watts, d’après James Watt (1736-1819) mieux connu pour ses travaux sur la machine à vapeur. Le watt indique la puissance (P) qui se calcule très simplement à partir de la tension et du courant :

P=U×I

Alimentée avec 3 V, notre ampoule consomme 0,75 A. Nous pouvons en conclure que c’est une ampoule de 2,25 watts. Les LED remplacent de plus en plus les ampoules à incandescence car elles peuvent fournir la même quantité de lumière en émettant moins de chaleur et en consommant beaucoup moins. Les factures d’électricité étant basées sur le nombre de watts, la réduction de la puissance des ampoules permet d’économiser tout en préservant l’environnement.

Il semble que nous ayons maintenant analysé tous les aspects de la lampe de poche : la pile, les fils et l’ampoule, non ? En fait, nous avons oublié l’élément le plus important !

L’interrupteur ! C’est lui qui décide si l’électricité circule ou non dans le circuit. Lorsqu’un interrupteur laisse passer l’électricité, on dit qu’il est fermé. Il est ouvert quand le courant ne peut pas passer. Le sens des adjectifs « fermé » et « ouvert » pour les interrupteurs est opposé à celui qu’ils ont pour les portes de lieux et les robinets. Une porte fermée empêche tout passage alors qu’un interrupteur fermé permet le passage de l’électricité.

Soit l’interrupteur est fermé, soit il est ouvert. Le courant passe ou ne passe pas. L’ampoule s’allume ou ne s’allume pas.

Tout comme les codes binaires inventés par Morse et Braille, notre simple lampe de poche est soit allumée, soit éteinte. Il n’y a pas d’entre-deux. Cette similitude entre codes binaires et circuits électriques minimalistes va s’avérer très utile dans les chapitres à venir.

1 N.d.T. : les bornes d’un générateur tel qu’une pile sont également appelées pôles. Nous parlons ici de courant continu. Les prises de courant de nos domiciles délivrent du courant alternatif, les pôles + et – étant intervertis 50 fois par seconde.

2 L’unité de mesure de la tension est le volt, abrégé V et nommé d’après le comte Alessandro Volta (1745-1827) qui a inventé la première pile électrique en 1800.

3 N.d.T. : on utilise aussi l’image de la hauteur d’une chute d’eau pour la tension.

Chapitre 5  Communiquer sans se voir

Vous avez 12 ans. Un beau (façon de parler…) jour, la famille de votre meilleur ami s’en va vivre dans une autre ville. Vous continuez à échanger courriels et messages avec votre ami de temps en temps, mais ce n’est pas aussi excitant que vos séances de code morse nocturnes avec les lampes de poche. Un autre bon copain, qui vit dans la maison voisine mais pas en face, finit par devenir votre nouveau meilleur ami. Il est temps de lui enseigner le morse pour que les lampes de poche clignotent à nouveau dans la nuit.

Le problème est que la fenêtre de la chambre de votre nouveau camarade ne fait pas face à celle de la vôtre. Les maisons sont côte à côte, et les chambres sont implantées selon le même plan. À moins que vous ne trouviez un moyen d’installer des miroirs dans les jardins, vos lampes de poche ne servent plus car vous n’avez pas de liaison visuelle directe.

La situation est-elle désespérée ?

Si vous avez quelques notions d’électricité, vous pouvez envisager de fabriquer des lampes de poche spéciales avec des piles, des ampoules, des interrupteurs et des fils. Dans une première expérience, vous installez les piles et l’interrupteur dans votre chambre, deux fils sortent par la fenêtre, traversent la clôture et aboutissent dans la chambre de votre ami, où ils sont reliés à une ampoule :

Dans la suite du livre, les circuits seront représentés de manière plus symbolique. Par exemple, même si je ne fais apparaître qu’une seule pile, il se peut que vous ayez besoin d’en empiler deux pour augmenter le voltage1. Dans ce schéma et dans tous les suivants, le symbole d’un interrupteur dans l’état ouvert (coupé) est celui-ci :

Et voici le même interrupteur dans l’état fermé (le courant passe) :

La lampe de poche de ce chapitre fonctionne de la même manière que celle illustrée dans le chapitre précédent, sauf que les fils qui relient les composants sont maintenant bien plus longs. Lorsque vous fermez l’interrupteur de votre côté, l’ampoule brille chez votre ami :

Vous pouvez immédiatement commencer à envoyer des messages en code morse.

Pour que votre ami puisse à son tour envoyer des messages, il suffit d’installer un second circuit dans le sens inverse :

Félicitations ! Vous venez de mettre au point un système télégraphique bidirectionnel. Notez qu’il s’agit de deux circuits identiques et totalement indépendants l’un de l’autre. En théorie, vous pouvez donc envoyer un message à votre ami pendant qu’il vous en envoie un (même s’il est difficile pour un cerveau de lire et d’envoyer un message en même temps).

Vous ferez preuve de perspicacité en découvrant que vous n’avez pas besoin de tirer deux fois deux fils entre les deux maisons. Vous pouvez éliminer un des quatre fils en réalisant la configuration suivante :

Dans les schémas de ce livre, lorsque des fils seront interconnectés, cela sera symbolisé par un petit point évoquant une goutte de soudure au niveau de la connexion. Le schéma précédent présente deux connexions de ce type, l’une sous le pôle négatif de la pile de gauche et l’autre sous l’ampoule chez votre ami.

Remarquez que les pôles négatifs des deux piles sont maintenant interconnectés. Les deux circuits (pile vers interrupteur vers ampoule vers pile) continuent de fonctionner indépendamment, même s’ils sont maintenant reliés. Cette connexion entre les deux circuits est appelée commun. Dans ce circuit, le commun correspond à la portion entre les deux points de connexion des fils, du point de connexion entre ampoule et pile de gauche au point entre ampoule et pile de droite.

Regardons les flux de plus près pour nous assurer qu’il ne se passe rien de bizarre. Lorsque vous fermez l’interrupteur de votre côté, l’ampoule de la maison de votre ami s’allume. Les fils rouges indiquent le flux d’électricité dans le circuit :

Notez bien qu’aucun courant ne circule dans l’autre partie du circuit : les électrons ne peuvent pas se déplacer car l’interrupteur de droite est ouvert.

Inversement, lorsque vous ne fermez pas votre interrupteur mais que votre ami ferme le sien, il fait briller la lampe chez vous. Le schéma suivant montre où l’électricité circule dans le circuit :

Lorsque vous et votre ami essayez de communiquer en même temps, parfois les deux interrupteurs sont ouverts, parfois l’un des deux est fermé et l’autre est ouvert, et parfois les deux sont fermés. Dans ce dernier cas, le flux d’électricité dans le circuit s’établit ainsi :

Vous noterez avec intérêt que lorsque les deux lampes sont allumées, aucun courant ne circule dans la partie commune du circuit.

En utilisant une partie commune pour relier deux circuits distincts en un seul circuit, nous avons réduit l’installation électrique entre les deux maisons de quatre fils à trois fils et donc réduit notre dépense en câbles de 25 %.

Si nous devions tirer les fils sur une très longue distance, nous pourrions être tentés de réduire encore nos dépenses de câblage en éliminant un autre fil. Malheureusement, cela n’est pas possible avec les piles D de 1,5 volt et les petites lampes. Mais s’il s’agissait de piles de 100 volts et d’ampoules beaucoup plus grosses, ce serait certainement possible.

L’astuce est ambitieuse : une fois que vous avez établi une partie commune dans le circuit, vous n’êtes pas obligé d’utiliser du fil pour l’incarner, mais vous pouvez remplacer le fil par autre chose. Et par quoi remplacer un fil conducteur ? Par une sphère géante d’environ 12 700 km de diamètre, composée de métal, de roche, d’eau et de matières organiques. Cette sphère géante est connue sous le nom de terre.

Lorsque j’ai décrit les bons conducteurs dans le chapitre précédent, j’ai mentionné l’argent, le cuivre et l’or, mais pas la roche, ni l’humus. En réalité, la terre n’est pas un très bon conducteur, même si certains types de terre (la terre humide, par exemple) sont meilleurs que d’autres (comme le sable sec). Mais nous avons appris une chose sur les conducteurs : plus ils sont gros, mieux c’est. Un fil très épais est beaucoup plus conducteur qu’un fil très fin. En diamètre, la terre est imbattable. Elle est vraiment grande.

Pour utiliser la terre comme conducteur, il ne suffit pas de planter un petit fil dans le sol à côté de vos plants de tomates. Il faut utiliser quelque chose qui maintienne un contact substantiel avec la terre, c’est-à-dire un conducteur qui a une grande surface. Une bonne solution consiste à utiliser un piquet en cuivre d’au moins 2 m de long et de 15 mm de diamètre. Cela représente une surface de contact avec la terre de 0,1 M². On enfonce le poteau dans le sol à l’aide d’une masse et on y connecte un fil nommé tresse de terre. Si les canalisations d’eau froide de votre maison sont en cuivre et prennent naissance dans le sol à l’extérieur de la maison, vous pouvez y connecter un fil.

Un contact électrique avec la terre est appelé earth au Royaume-Uni, mais ground en Amérique, tandis qu’en France, on utilise plutôt le terme « terre ». Méfiez-vous d’une confusion possible à propos de ce mot « terre », car il sert souvent à désigner la partie d’un circuit que nous appelons le « commun ».

Dans ce chapitre, et jusqu’à mention contraire, je désigne comme terre une connexion physique avec la terre. Lorsque les gens dessinent des circuits électriques, ils utilisent ce symbole pour représenter la (prise de) terre :

Les électriciens utilisent ce symbole pour éviter de devoir dessiner un poteau de cuivre de 2 m enterré dans le sol. Un circuit connecté à ce symbole est dit « relié à la terre » ou « mis à la terre »2.

Voyons comment cela fonctionne. Nous avons commencé ce chapitre en examinant une configuration à sens unique telle que la suivante :

Si vous aviez décidé d’utiliser des piles de fort voltage et des lampes supportant une haute tension, vous n’auriez besoin que d’un seul fil entre votre maison et celle de votre ami, car vous pourriez utiliser la terre comme l’un des connecteurs :

Lorsque vous fermez l’interrupteur, l’électricité circule comme suit :

Les électrons sortent du sol chez votre ami, passent par la lampe et le fil, traversent l’interrupteur chez vous puis rejoignent le pôle positif de votre pile. Les électrons du pôle négatif de votre pile vont dans le sol.

Vous pourriez également imaginer des électrons sortant du poteau de terre enfoncé dans le jardin de votre maison, puis se précipitant à travers le sol jusqu’à atteindre l’autre poteau de cuivre, celui enfoncé dans le jardin de la maison de votre ami. Mais vous devinez que la terre remplit cette même fonction pour plusieurs milliers de circuits électriques dans le monde. Vous pourriez donc vous demander comment les électrons savent où aller. De toute évidence, ils n’en savent rien. Une autre image de la terre est nécessaire.

Oui, la terre est un énorme conducteur d’électricité, mais elle peut également être considérée comme une source d’électrons et un bassin d’électrons. La terre est aux électrons ce que l’océan est aux gouttes d’eau. La terre est une source pratiquement illimitée d’électrons et une mer d’électrons géante.

La terre présente une certaine résistance. C’est pour cette raison que nous ne pouvons pas utiliser la terre pour réduire nos besoins en câblage lorsque nous utilisons des piles de faible voltage (1,5 V) et une lampe de poche. La terre a tout simplement trop de résistance pour les basses tensions.

Vous remarquerez que les deux schémas précédents incluent une pile dont le pôle négatif est relié à la terre :

Je ne vais plus perdre de temps à dessiner cette pile. À la place, je vais utiliser pour le pôle positif un V majuscule qui représente la tension en volts. Un fil partant d’un V majuscule est identique à un fil connecté à la borne positive d’une pile dont le pôle négatif est relié à la terre. Le télégraphe à ampoule à sens unique ressemble maintenant à ceci :

Le V signifie « tension », mais dans un sens, il pourrait aussi signifier « vide ». Vous pouvez considérer le V comme un aspirateur à électrons et la terre comme un océan d’électrons. L’aspirateur à électrons attire les électrons depuis la terre à travers le circuit, en effectuant un travail en cours de route (comme l’allumage d’une lampe).

La terre est parfois également appelée point de potentiel zéro. Cela signifie qu’il n’y a pas de tension. Une tension est une énergie potentielle de travail, tout comme une brique suspendue dans l’air est une source potentielle d’énergie mécanique. Le potentiel zéro, c’est comme une brique posée au sol : elle ne peut pas tomber plus bas.

Dans le Chapitre 4, l’une des premières choses que nous avions remarquées est qu’un circuit est un cercle. Pourtant, notre nouveau circuit ne ressemble plus du tout à un cercle. C’en est toujours un, néanmoins. Vous pourriez remplacer le V par une batterie dont le pôle négatif serait relié à la terre, puis dessiner un fil reliant tous les endroits où vous voyez un symbole de terre. Vous obtiendriez le même diagramme que celui avec lequel nous avons commencé ce chapitre.

Ainsi, avec l’aide de quelques piquets de cuivre (ou branchements sur des canalisations d’eau froide), nous pouvons construire un système de code morse bidirectionnel avec seulement deux fils traversant la clôture entre votre maison et celle de votre ami :

Ce circuit est fonctionnellement identique à la configuration présentée en début de chapitre dans laquelle trois fils seulement relient les deux maisons, mais il ne fonctionnerait qu’avec des piles et des ampoules à tension élevée.

Dans ce chapitre, nous avons franchi une étape importante dans l’évolution des communications. Auparavant, nous pouvions communiquer en code morse, mais uniquement en ligne droite et dans les limites de la portée du faisceau lumineux d’une lampe de poche.

En utilisant des fils, nous avons non seulement construit un système permettant de communiquer en dehors de la portée du regard, mais nous nous sommes également affranchis de la limite de distance. Nous pouvons communiquer sur des centaines ou des milliers de kilomètres en installant des fils de plus en plus longs.

Enfin, pas exactement. Bien que le cuivre soit un très bon conducteur d’électricité, il n’est pas parfait. Plus les fils sont longs, plus ils sont résistants. Plus la résistance est grande, moins le courant circule. Moins il y a de courant, plus les ampoules sont faibles.

De quelle longueur exacte de fils avons-nous besoin ? Cela dépend. Supposons que vous utilisiez le câblage bidirectionnel à quatre fils présenté en premier, donc sans terre ni point commun, et que vous optiez pour une alimentation par piles et des ampoules de lampe de poche. Pour limiter les coûts, vous pouvez acheter un rouleau de 50 m de câble pour enceintes de chaîne stéréo haute fidélité. Ce câble d’enceinte est constitué de deux fils isolés collés l’un contre l’autre. C’est un bon choix pour notre système télégraphique. Si les deux chambres sont distantes de moins de 20 m, ce rouleau de fil fera l’affaire.

L’épaisseur des petits fils électriques se mesure en unités AWG (American Wire Gauge)3. Plus le nombre AWG est petit, plus le fil est épais et plus il est conducteur (moins résistant). Si vous achetez un fil d’enceinte de calibre 20 AWG, son diamètre sera d’environ 8/10e de mm (0,032 pouce) et sa résistance d’environ 30 ohms au km, soit de l’ordre de 1 ohm pour faire les 40 m de l’aller-retour entre les deux chambres à coucher.

Ce n’est pas mal du tout, mais que se passerait-il si le fil s’étendait sur 1 km ? Sa résistance totale serait supérieure à 100 ohms. Rappelons que, dans le chapitre précédent, la résistance de notre ampoule n’était que de 4 ohms. À partir de la loi d’Ohm, nous pouvons facilement calculer que le courant à travers le circuit ne sera plus de 0,75 ampère (3 volts divisés par 4 ohms), mais sera maintenant inférieur à 0,03 ampère (3 volts divisés par plus de 100 ohms). Il est presque certain que ce courant ne suffira plus à allumer l’ampoule.

L’utilisation d’un fil plus gros est une solution immédiate, mais qui va s’avérer coûteuse. Un fil de calibre 10 AWG (épaisseur d’environ 0,1 pouce) offre une résistance vingt fois moindre, de l’ordre de 5 ohms au kilomètre.

Une autre solution consisterait à augmenter la tension pour pouvoir utiliser des ampoules de plus forte résistance, comme celles que l’on possède (de moins en moins souvent) chez soi. La résistance des fils affecterait alors beaucoup moins l’ensemble du circuit.

Ces problèmes ont été affrontés au milieu du xixe siècle par ceux qui ont installé les premiers télégraphes en Amérique du Nord et en Europe. Quels que soient l’épaisseur des fils et le voltage, les fils télégraphiques ne pouvaient tout simplement pas être allongés sans limite. En pratique, un tel système restait utilisable sur quelques centaines de kilomètres au maximum. On ne pouvait pas couvrir ainsi les milliers de kilomètres entre New York et la Californie.

La solution au problème (pas pour les lampes de poche, mais pour les télégraphes) s’est avérée être un dispositif sans prétention, mais qui est devenu le composant essentiel des premiers ordinateurs.

1 N.d.T. : c’est une batterie de piles.

2 N.d.T. : en France, on parle aussi de masse pour désigner les parties métalliques des appareils qui doivent être reliés à la terre.

3 N.d.T. : on utilise aussi le mm² en Europe, mais pour les fils de très petit diamètre, AWG est employé aussi chez nous.

Chapitre 6  La logique par interruption

Qu’est-ce que la vérité ? Le grand penseur Aristote considérait qu’il y avait une relation forte entre le concept de logique et celui de vérité. Son ensemble de traités nommé Organum constitue la première étude détaillée du concept de logique (elle date du ive siècle avant notre ère). Dans la Grèce antique, on considérait que la logique consistait à analyser le langage pour en détecter la véracité, et cette activité était considérée comme de la philosophie. Le principe de base de la logique aristotélicienne est le syllogisme. Le syllogisme le plus connu de nos jours (qui ne se trouve pourtant pas dans les écrits d’Aristote) est le suivant :

Tous les hommes sont mortels.

Socrate est un homme.

Donc Socrate est mortel.

Un syllogisme se fonde sur deux prémisses qui sont considérées comme correctes, ce qui permet d’en déduire une conclusion.

Réussir à prouver que Socrate était mortel semble assez simple, mais il existe bien des variantes du syllogisme. Intéressons-nous par exemple aux deux prémisses qu’avait proposées au xixe siècle le mathématicien Charles Dodgson, plus connu sous son pseudonyme Lewis Carroll :

Tout philosophe est logique.

Un homme illogique est toujours obstiné.

La conclusion qu’il en a tirée est la suivante :

Certaines personnes obstinées ne sont pas philosophes.

Elle n’est pas très convaincante. Vous remarquez notamment l’effet surprenant et perturbateur du terme « certains ».

Les mathématiciens se sont battus pendant plus de deux millénaires avec la logique d’Aristote, cherchant à la dompter au moyen de symboles et d’opérateurs mathématiques. Jusqu’au xixe siècle, la seule personne qui a réussi à progresser sensiblement dans ce domaine est le philosophe allemand Gottfried Wilhelm Leibniz (1648-1716). Il s’était passionné pour la logique dans ses jeunes années, puis était passé à d’autres sujets, avec, par exemple, l’invention du calcul intégral (en même temps qu’Isaac Newton, et en toute indépendance l’un de l’autre).

© Science and Society Picture Library/Getty Images

Puis, un jour, est apparu un certain George Boole.

Né au Royaume-Uni en 1815, George Boole ne commençait pas son existence sous les meilleurs auspices. Père cordonnier et mère servante, il n’aurait normalement pas pu espérer une carrière différente, à cause de la rigidité de la société britannique de l’époque. Mais son esprit curieux joint à la bienveillance paternelle (son père s’intéressait beaucoup aux sciences, aux mathématiques et à la littérature) a permis au jeune Georges de recevoir une éducation d’un niveau réservé aux garçons de la bonne société. Il a appris le latin, le grec et les mathématiques. Ses premiers articles en mathématiques l’ont fait remarquer au point d’être le premier à être nommé professeur de mathématiques au Queen’s College de Cork en Irlande. C’était en 1849.

Dès le milieu du XVIIe siècle, plusieurs mathématiciens avaient réfléchi à donner une définition mathématique de la logique, notamment Augustus De Morgan. C’est cependant George Boole qui a produit une avancée significative, tout d’abord dans son bref ouvrage de 1847, Analyse mathématique de la logique, essai pour un calcul du raisonnement déductif, puis dans un travail beaucoup plus ambitieux en 1854, Étude des lois de la pensée sur lesquelles sont fondées les théories mathématiques de la logique et des probabilités, abrégé en Les Lois de la pensée. George Boole décéda en 1864 à 49 ans d’une pneumonie contractée après avoir couru sous la pluie pour aller donner cours.

Le titre de son livre de 1854 laisse deviner une véritable ambition : Boole était persuadé que le cerveau humain fonctionnait de façon logique. Si l’on parvenait à symboliser la logique grâce aux mathématiques, on obtiendrait une description mathématique du fonctionnement de la pensée. Mais rassurez-vous : on peut étudier les mathématiques booléennes sans devoir adhérer aux théories neuropsychologiques particulières de Boole.

George Boole a donc inventé une nouvelle algèbre, qui depuis porte le nom d’algèbre booléenne, car elle se distingue radicalement de l’algèbre classique.

En algèbre classique, on utilise généralement des lettres pour symboliser des valeurs numériques. Ce sont des opérandes et on les combine au moyen d’opérateurs, comme le signe + de l’addition et le signe × de la multiplication. Regardons l’expression suivante :

A = 3 × (B + 5)

L’algèbre classique se fonde sur des règles, et ces règles ont été tellement inculquées depuis l’école primaire que nous ne les considérons même plus comme des règles, allant même jusqu’à en oublier les noms. Pourtant, toute forme de raisonnement mathématique est fondée sur des règles.

La première règle pour l’addition et la multiplication est celle de commutativité. Elle signifie que l’on peut intervertir les symboles situés des deux côtés d’un opérateur :

A+B=B+A

A×B=B×A

En revanche, les opérations de soustraction et de division ne sont pas commutatives. L’addition et la multiplication sont également associatives :

A + (B + C) = (A + B) + C

A × (B × C) = (A × B) × C

Enfin, la multiplication est distributive par rapport à l’addition :

A × (B + C) = (A × B) + (A × C)

Une autre caractéristique évidente de l’algèbre classique est qu’elle traite nécessairement des valeurs numériques, par exemple les grammes des ingrédients d’une recette de cuisine, le nombre de têtes de bétail d’un troupeau ou la distance franchie par un train en une seconde.

L’idée géniale de Boole a consisté à rendre l’algèbre plus abstraite en la libérant de sa dépendance aux nombres. Dans l’algèbre booléenne, les opérandes ne sont pas des valeurs numériques mais des classes. Une classe est un groupe de choses, ce qui plus tard sera désigné par le terme ensemble (set).

Intéressons-nous aux chats. Un chat peut être soit mâle, soit femelle. Choisissons la lettre M pour les chats mâles et la lettre F pour les chats femelles. Mais attention ! Ces deux symboles ne correspondent jamais à une quantité de chats. Le nombre de chats ou de chattes peut changer à tout moment suite aux naissances et aux regrettables décès des vieux matous. Les lettres désignent des catégories de chats, regroupés selon des caractéristiques distinctives. Au lieu de parler des chats mâles, nous emploierons la lettre M.

D’autres lettres vont nous servir à classer les chats selon la couleur de leur pelage. La lettre T conviendra pour les chats tigrés, la lettre N pour les chats noirs, la lettre B pour les chats blancs et, enfin, la lettre A pour tous les chats des autres couleurs, c’est-à-dire ceux qui ne font partie d’aucune des trois classes T, N ou B.

Nous pouvons terminer notre classification en distinguant les chats castrés des non-castrés avec la lettre K pour les chats castrés et la lettre E pour les chats entiers.

Les deux opérateurs + et × de l’addition et de la multiplication de l’algèbre classique sont réutilisés en algèbre booléenne, ce qui peut vous rendre perplexe. Tout le monde sait additionner et multiplier des nombres, mais que signifie additionner et, plus encore, multiplier des classes d’objets ?

En fait, ce ne sont plus des additions et multiplications de quantités ; les deux symboles + et × signifient tout à fait autre chose.

Le + de l’algèbre booléenne incarne l’opération d’union de deux classes. Lorsque deux classes sont unies, le résultat correspond à la collecte des éléments de la première classe et des éléments de la seconde classe. Par exemple, l’expression N + B permet d’obtenir la classe de tous les chats qui sont ou noirs ou blancs1.

De même, le symbole × prend une autre signification : c’est l’intersection de deux classes. Le résultat englobe seulement les éléments qui existent dans les deux classes à la fois. L’opération F × T désigne tous les chats qui sont femelles et qui sont tigrées en même temps. On peut abréger l’écriture comme dans l’algèbre classique, en écrivant F.T ou FT au lieu de F × T. D’ailleurs, Boole préférait la notation FT), L’opération qui sert d’exemple désigne donc les chattes tigrées.

Afin d’éviter toute confusion entre l’algèbre classique et l’algèbre booléenne, certains préfèrent utiliser les deux symboles ∪ et ∩ à la place de + et × pour respectivement l’union et l’intersection. Pour autant, une partie de l’énorme impact de Boole sur les mathématiques résulte de sa volonté de continuer à utiliser les opérateurs préexistants mais de façon plus abstraite. Je choisis donc dans la suite de respecter son choix, consistant à ne pas utiliser ces deux nouveaux symboles pour sa nouvelle algèbre.

Les règles de commutativité, d’associativité et de distributivité restent valables en algèbre booléenne. Mieux encore, l’opérateur + de l’algèbre booléenne est distributif par rapport à l’opérateur ×, alors que ce n’est pas le cas dans l’algèbre classique :

B + (N × F) = (B + N) × (B + F)

L’union de l’ensemble de tous les chats blancs avec l’ensemble des seules femelles noires équivaut à l’intersection de deux unions : l’union de tous les chats blancs et noirs et l’union de tous les chats blancs et de tous les chats femelles. N’hésitez pas à refaire le raisonnement jusqu’à pouvoir confirmer sa véracité.

L’algèbre booléenne requiert trois autres symboles, dont un est le signe moins et les deux autres ressemblent à des chiffres, mais ce n’est qu’une apparence car ils ne sont pas traités comme des valeurs numériques. En algèbre booléenne, le symbole 1 correspond à l’univers entier, c’est-à-dire tout ce dont on peut parler. Dans notre exemple, ce symbole correspond à la classe de tous les chats passés, présents et futurs. Par exemple :

M+F=1

Il est évident que l’union de tous les chats mâles et de tous les chats femelles donne la classe de tous les chats2. L’union des chats tigrés, des chats noirs, des chats blancs et de tous les chats d’une autre couleur aboutit à la classe d’absolument tous les chats :

T+N+B+A=1

Vous pouvez également aboutir à la classe de tous les chats de l’univers ainsi :

K+E=1

En plaçant le signe – avant la lettre de classe, on désigne l’univers moins quelque chose. Un exemple :

1 – M

Nous désignons ainsi la classe de tous les chats sauf tous les mâles. Bien sûr, la classe universelle sans les chats mâles équivaut à la classe de tous les chats femelles :

1 – M=F

Nous avons parlé d’un troisième symbole : c’est le 0. En algèbre booléenne, ce 0 désigne la classe vide, la classe ne contenant aucun élément. C’est à cette classe que l’on aboutit lorsqu’on demande l’intersection de deux classes n’ayant rien en commun, par exemple l’intersection entre la classe des chats mâles et celle des chats femelles :

F×M=0

Il faut remarquer que les deux symboles 1 et 0 sont dans certains cas seulement interprétés de la même façon dans les deux algèbres. Par exemple, l’intersection de la classe de tous les chats avec celle de tous les chats femelles est tout bonnement la classe des chats femelles :

1×F=F

L’intersection de la classe vide avec la classe des femelles reste la classe vide (sans aucun chat) :

0×F=0

L’union de la classe sans chat et de la classe des femelles est la classe des femelles :

0+F=F

Cette similarité dans le raisonnement montre vite ses limites. Par exemple, l’union de tous les chats et de tous les chats femelles reste la classe de tous les chats :

1+F=1

En algèbre classique, le fait qu’une addition n’ajoute rien n’a pas de sens.

Si la classe F symbolise celle de tous les chats femelles et la classe (1 – F) celle de tous les chats qui ne sont pas femelles, l’union des deux classes est égale à 1 :

F + (1 – F) = 1

L’intersection de ces deux classes est égale à 0 :

F × (1 – F) = 0

Ces deux formulations incarnent un concept essentiel en logique : il s’agit de la loi de contradiction. Cette loi stipule qu’une chose ne peut pas être en même temps elle-même et son contraire.

C’est dans une équation telle que la suivante que l’algèbre booléenne s’écarte franchement de l’algèbre classique :

F×F=F

En algèbre booléenne, cette affirmation est limpide : l’intersection de tous les chats femelles avec tous les chats femelles reste la classe des chats femelles. Bien sûr, cela semblerait erroné si F correspondait à une valeur numérique. D’après Boole, l’équation

X²=X

est celle qui distingue clairement son algèbre de l’algèbre classique. Voici une autre instruction booléenne qui semble étrange en algèbre habituelle :

F+F=F

L’union des chats femelles avec les chats femelles reste la classe des chats femelles.

Nous pouvons résoudre le syllogisme d’Aristote en appliquant la méthode mathématique de l’algèbre booléenne. Revoyons les deux prémisses de ce syllogisme, en utilisant un langage non genré :

Toutes les personnes sont mortelles.

Socrate est une personne.

Nous allons symboliser la classe de toutes les personnes par P, la classe des mortels par M et la classe des Socrate par S. Que signifie « toutes les personnes sont mortelles » ? C’est la même chose que d’établir l’intersection entre la classe de toutes les personnes et la classe de toutes les choses mortelles, le résultat étant la classe de toutes les personnes :

P×M=P

Ce serait une erreur que de prétendre que P × M = M, parce que la classe de tous les mortels comprend également les chats, les chiens et les châtaigniers.

L’affirmation comme quoi Socrate est une personne correspond à l’intersection de la classe contenant Socrate (une classe très réduite) et la classe de toutes les personnes (une classe bien plus volumineuse). Le résultat de l’intersection est la classe des Socrate :

S×P=S

La première équation nous avait appris que P est équivalent à P × M, nous pouvons faire une substitution dans la deuxième équation :

S × (P × M) = S

En appliquant la loi d’associativité, nous pouvons réécrire l’équation :

(S × P) × M = S

Sachant que S × P est égal à S, nous pouvons simplifier :

S×M=S

La démonstration est achevée. La formule nous annonce que l’intersection de la classe des Socrate et de la classe de toutes les choses mortelles est la classe S, ce qui prouve bien que Socrate est mortel. Si nous avions trouvé que S × M était égal à 0, nous aurions pu en déduire que Socrate n’était pas mortel. Si nous avions trouvé que S × M était égal à M, la conclusion fantaisiste aurait été que toutes les choses mortelles sont des Socrate !

Apprendre à maîtriser l’algèbre booléenne pour prouver une chose aussi évidente que la mortalité de Socrate (d’autant qu’il l’a prouvée lui-même voici plus de 2400 ans) semble un effort incongru. Mais c’est un apprentissage : l’algèbre booléenne permet de vérifier de façon rigoureuse que quelque chose satisfait à un ensemble de critères.

Supposons que vous entriez dans une animalerie et demandiez au vendeur : « Je veux un chat mâle castré, blanc ou tigré ou alors un chat femelle stérilisé de n’importe quelle couleur sauf blanc ; si vous n’avez rien pour me satisfaire, je prends n’importe quel chat du moment qu’il soit noir. » Le vendeur peut reformuler votre demande en algèbre booléenne avec l’expression suivante :

(M × K × (B + T)) + (F × K × (1 – B)) + N

Il vous demande de confirmer et vous répondez : « C’est exactement cela ! »

Pour vérifier que le vendeur a bien compris votre demande, vous pouvez choisir de symboliser les deux concepts d’union et d’intersection par les deux mots OU et ET (OR et AND en anglais). J’écris volontairement les mots en majuscules parce qu’ils représentent ici des opérations d’algèbre booléenne. Lorsque vous voulez unir deux classes, vous acceptez dans la classe résultante les objets appartenant à la première ou à la seconde classe. Dans le cas d’une intersection, vous n’acceptez de ne conserver que ce qui se trouve dans la première et dans la seconde classe. Vous disposez enfin du mot NON (NOT) pour l’opérateur de négation – devant un chiffre. Récapitulons :

+ : l’union signifie également OU ou OR.

× : l’intersection signifie ET ou AND.

– : le signe moins (l’univers moins quelque chose) signifie NON ou NOT.

Voici comment nous pouvons reformuler l’expression du vendeur :

(M ET K ET (B OU T)) OU (F ET K ET (NON B)) OU N

La lecture donne un résultat proche de ce que vous aviez exprimé en langage humain. Notez bien la présence de parenthèses pour supprimer toute ambiguïté. Par cette expression, vous voulez adopter un chat ou une chatte qui appartient à l’une des trois classes suivantes :

(M ET K ET (B OU T))

OU

(F ET K ET (NON B))

OU

N

À partir du moment où cette formule est validée, le vendeur va pouvoir réaliser des tests booléens dans lesquels les lettres font référence à des propriétés, caractéristiques ou des attributs des chats, qui peuvent prendre la valeur 0 ou 1. La valeur 1 signifie oui, vrai, ce chat possède bien cette caractéristique alors que la valeur 0 signifie que non, faux, ce chat ne possède pas ce critère.

Le vendeur propose d’abord un chat mâle tigré non castré. Nous partons de l’expression complète qui regroupe les trois précédentes :

(M × K × (B + T)) + (F × K × (1 – B)) + N

Voici le résultat de cette expression une fois les critères valorisés par des 0 et des 1 en fonction du chat candidat :

(1 × 0 × (0 + 1)) + (0 × 0 × (1 – 0)) + 0

Vous constatez que les deux seuls symboles qui correspondent à des caractéristiques satisfaites, donc à la valeur 1, sont le M et le T, puisque le chat est effectivement un mâle et qu’il est effectivement de pelage tigré.

Nous pouvons maintenant envisager de simplifier l’expression. Si le résultat final vaut 1, c’est que le chat candidat satisfait à vos critères. Bien sûr, s’il vaut 0, c’est que le chat ne convient pas (l’ensemble résultant est vide). N’oubliez pas, pendant cette simplification d’expression, qu’il ne s’agit pas d’additions et de multiplications au sens habituel. La plupart des règles restent applicables lorsque le + signifie OU et le × signifie ET. Certains ouvrages utilisent la notation basée sur le symbole accent circonflexe ∧ pour le ET et le symbole ∨ pour le OU. Nous allons nous simplifier la vie dans la suite en conservant les deux signes habituels de l’arithmétique + et ×, d’autant que quelques règles restent les mêmes qu’en algèbre classique.

Si on considère que l’opérateur × signifie l’opération logique ET, les quatre résultats possibles sont les suivants :

0×0=0

0×1=0

1×0=0

1×1=1

La lecture de cette table permet de conclure que le résultat n’est positif, vrai, égal à 1, que si l’opérande de gauche ET l’opérande de droite sont à 1, vrais. L’opération équivaut donc à une multiplication habituelle, et nous pouvons la résumer dans un petit tableau. Le nom de l’opération concernée est indiqué dans l’angle supérieur gauche et les combinaisons des deux opérantes correspondent aux lignes et colonnes :

ET

0

1

0

0

0

1

0

1

En ce qui concerne l’opérateur + qui correspond à OU, les quatre résultats possibles sont les suivants :

0+0=0

0+1=1

1+0=1

1+1=1

Le résultat est vrai, égal à 1, dès que l’opérande de gauche OU celui de droite est égal à 1. Les résultats sont donc très proches de ceux d’une addition classique, sauf que 1 + 1 vaut ici 1. (S’il est vrai qu’un chat est tigré ou bien s’il est vrai qu’un chat est tigré, cela signifie qu’un chat est tigré.) Voici le petit tableau qui résume les possibilités de l’opération logique OU :

OU

0

1

0

0

1

1

1

1

Nous pouvons nous servir de ces deux tableaux pour calculer le résultat de l’expression suivante :

(1 × 0 × 1) + (0 × 0 × 1) + 0 = 0 + 0 + 0 = 0

Rappelons que 0 signifie non, faux. Le chat candidat ne convient donc pas ici.

C’est alors que le vendeur revient avec un autre chat, une femelle blanche castrée. Rappelons l’expression d’origine :

(M × K × (B + T)) + (F × K × (1 – B)) + N

Nous remplaçons par les valeurs en 0 et en 1 :

(0 × 1 × (1 + 0)) + (1 × 1 × (1 – 1)) + 0

Nous simplifions pour obtenir le résultat :

(0 × 1 × 1) + (1 × 1 × 0) + 0 = 0 + 0 + 0 = 0

Cette chatte ne convient pas non plus.

Le vendeur trouve alors une femelle grise castrée. Le pelage gris fait partie de l’ensemble des couleurs autres que blanc, noir ou tigré. Voici l’expression logique correspondante :

(0 × 1 × (0 + 0)) + (1 × 1 × (1 – 0)) + 0

Nous simplifions :

(0 × 1 × 0) + (1 × 1 × 1) + 0 = 0 + 1 + 0 = 1

Nous obtenons un résultat égal à 1, ce qui confirme que cette chatte a trouvé son nouveau propriétaire !

Le soir même, alors que votre nouveau compagnon s’est endormi sur vos genoux, vous vous demandez s’il ne serait pas possible de combiner quelques interrupteurs avec une ampoule pour faire une investigation des différentes possibilités logiques de recherche du chat idéal. (Vous êtes d’un naturel curieux.) Sans le savoir, vous allez ainsi faire un pas de géant en termes conceptuels. Vous allez en effet vous lancer dans des expériences qui empruntent une passerelle entre algèbre de George Boole et circuits de commutation électronique. C’est cette voie qui a mené à la conception et la fabrication des ordinateurs. Ne soyez pas intimidé par ce pas de géant que vous allez faire !

Pour commencer l’expérience, vous construisez un circuit avec une ampoule et une pile, mais cette fois-ci en mettant en place deux interrupteurs au lieu d’un seul :

L’icône qui représente une planète dans la marge de ce livre rappelle que vous pouvez accéder à une version interactive du circuit présenté et de tous les autres sur le site Web de l’éditeur (code.lisez.com).

Lorsque deux interrupteurs sont reliés comme ci-dessus, l’un à la suite de l’autre, on parle de liaison série. Si vous ne fermez que l’interrupteur de gauche, cela ne change rien :

Si vous laissez l’interrupteur de gauche ouvert et fermez celui de droite, rien ne se produit non plus. La lampe ne va briller que si les deux interrupteurs sont fermés :

Ce montage concrétise le concept du ET logique. L’interrupteur de gauche et l’interrupteur de droite doivent tous deux être fermés pour que le courant circule.

Le circuit illustre un exercice de logique fondamentale. La lampe est réellement en mesure de répondre à la question « Est-ce que les deux interrupteurs sont fermés ? ». Nous pouvons faire la synthèse des quatre états possibles du circuit dans le tableau suivant :

Interrupteur gauche
Interrupteur droit
Lampe

Ouvert

Ouvert

Éteinte

Ouvert

Fermé

Éteinte

Fermé

Ouvert

Éteinte

Fermé

Fermé

Allumée

En imaginant que les interrupteurs et la lampe sont des opérateurs booléens, on peut symboliser les deux états possibles de chacun d’eux par les deux valeurs 0 et 1. La valeur 0 signifie « interrupteur ouvert » ou « lampe éteinte » et la valeur 1 « interrupteur fermé » ou « lampe allumée ». Nous pouvons donc reformuler notre tableau :

Interrupteur gauche
Interrupteur droit
Lampe

0

0

0

0

1

0

1

0

0

1

1

1

Vous aurez remarqué que les résultats ne changent pas si on intervertit les deux interrupteurs ; il est donc inutile de les différencier. Notre tableau peut être reformulé pour s’apparenter au tableau de l’opération logique ET vu plus haut :

Interrupteurs en série

0

1

0

0

0

1

0

1

Effectivement, nous obtenons quelque chose d’identique au tableau ET :

ET

0

1

0

0

0

1

0

1

Ce circuit électronique très simple est néanmoins capable de réaliser une opération ET de l’algèbre booléenne.

Recâblons maintenant notre circuit de façon différente au niveau des deux interrupteurs :

Les interrupteurs sont branchés en parallèle. Il suffit donc que l’un des deux interrupteurs soit fermé pour que la lampe s’allume, ici celui du haut :

De même, en fermant uniquement l’interrupteur du bas :

La lampe s’allume aussi lorsque les deux interrupteurs sont fermés (a fortiori) :

La lampe s’allume donc dès que l’interrupteur du haut ou celui du bas est fermé. Le mot-clé est ici le mot OU (OR).

Il s’agit bien d’un autre exemple de logique concrète. En brillant, la lampe répond à la question de savoir si au moins l’un des deux interrupteurs est fermé. Nous pouvons faire une synthèse des résultats dans le tableau suivant (en gras, ce qui change par rapport au ET logique) :

Interrupteur gauche
Interrupteur droit
Lampe

Ouvert

Ouvert

Éteinte

Ouvert

Fermé

Allumée

Fermé

Ouvert

Allumée

Fermé

Fermé

Allumée

Nous utilisons la valeur 0 pour symboliser un interrupteur ouvert et la lampe éteinte et la valeur 1 pour un interrupteur fermé et la lampe allumée. Nous pouvons reformuler le tableau comme pour le ET logique :

Interrupteur gauche
Interrupteur droit
Lampe

0

0

0

0

1

1

1

0

1

1

1

1

L’ordre des deux interrupteurs n’ayant pas d’importance ici non plus, nous pouvons simplifier la table ainsi :

Interrupteur en parallèle

0

1

0

0

1

1

1

1

Vous avez déjà deviné que cela équivaut à l’opération booléenne OU :

OU

0

1

0

0

1

1

1

1

Autrement dit, deux interrupteurs en parallèle réalisent concrètement l’équivalent d’une opération booléenne OU (OR).

Revenons à nos félins. En entrant dans l’animalerie, vous aviez précisé vos désirs au vendeur ainsi :

soit un mâle castré blanc ou tigré ;

soit une femelle castrée de n’importe quelle couleur sauf le blanc ;

soit, au pire, n’importe quel chat ou chatte au pelage noir.

Le vendeur traduit ces critères en une expression logique :

(M × K × (B + T)) + (F × K × (1 – B) + N

Nous savons maintenant que deux interrupteurs en série incarnent l’opération logique ET (symbolisé par le signe ×) alors que deux interrupteurs en parallèle incarnent le OU logique (symbolisé par le signe +). Nous pouvons donc construire un circuit avec huit interrupteurs dans trois branches :

Chaque interrupteur est désigné par une des lettres mentionnées dans l’expression booléenne. L’écriture B signifie NON B et équivaut à (1 – B). Si vous parcourez le montage de haut en bas et de gauche à droite, vous rencontrez les lettres des critères dans le même ordre que dans l’expression des besoins. Chaque signe de multiplication × équivaut à une liaison établie entre un interrupteur (ou une paire d’interrupteurs) et un autre, soit une connexion en série. De même, le signe d’addition + équivaut à deux interrupteurs en parallèle. Il y a bien trois circuits en parallèle pour les trois groupes de critères et une paire de circuits en parallèle dans la branche du haut.

Le vendeur avait d’abord proposé un chat mâle tigré et non castré. Fermons les trois interrupteurs des critères satisfaits (donc, y compris le « non blanc ») :

Les trois interrupteurs fermés sont M, T et NON B, mais cela ne suffit pas à allumer la lampe.

Le vendeur propose alors une femelle castrée et blanche comme neige (donc, pas « non blanche ») :

Comme lors du premier essai, aucune des trois branches n’assure une continuité électrique.

Le vendeur sort alors sa dernière proposition : une chatte grise et castrée. Traduisons ces critères dans le circuit :

Cette fois-ci, la lampe brille et prouve que le félin candidat satisfait à l’un des jeux de critères.

George Boole n’a jamais eu l’occasion de créer ce genre de circuit électrique et n’a donc pas eu le plaisir de voir concrètement une expression booléenne sous forme de fils, d’interrupteurs et de lampes. L’ampoule électrique n’a été inventée que quinze années après la mort de Boole. Ceci dit, le télégraphe était apparu dix ans avant la publication du livre de Boole Les Lois de la pensée. Un des composants du système télégraphique était d’ailleurs un appareillage simple mais capable d’effectuer des opérations logiques bien plus vite que des interrupteurs.

1 N.d.T. : mais ni les tigrés, ni les autres, et surtout pas les chats noirs et blancs !

2 N.d.T. : pas d’entre-deux à ce jour chez les félins.

Chapitre 7  Télégraphes et relais

© ullstein Bild Dtl/Getty Images

Samuel Finley Morse est né en 1791 à Charlestown, dans le Massachusetts. C’est dans cette ville, devenue depuis un quartier nord de Boston, que s’est déroulée en 1775 la fameuse bataille de Bunker Hill entre Américains et Anglais. Au moment où Samuel Morse voit le jour, la Constitution des États-Unis d’Amérique n’a encore que deux ans et George Washington effectue son premier mandat de président. La Russie est dirigée par Catherine la Grande et la Révolution française est loin de son terme, puisque Louis XVI et Marie-Antoinette ne seront décapités que deux ans plus tard. C’est toujours lors de cette même année 1791 que Mozart termine son dernier opéra, La Flûte enchantée, avant de disparaître à 35 ans. Parallèlement, le jeune Beethoven vient de fêter ses 20 ans, et commence tout juste à faire parler de lui.

Morse fait ses études à Yale, avant d’aller étudier les arts à Londres. Il devient d’ailleurs un portraitiste talentueux. L’hôtel de ville de New York accueille toujours son portrait du général La Fayette daté de 1825. Son dernier tableau est beaucoup plus personnel : il s’agit de La Muse, représentant sa fille Suzanne et exposé au Metropolitan Museum of Art de New York.

Morse fait également figure de pionnier en matière de photographie. Il apprend à réaliser des daguerréotypes auprès de Louis Daguerre lui-même, ce qui l’amène à être l’un des premiers à en produire aux États-Unis. Dès 1840, il transmet cette technique au jeune Mathew Brady âgé de 17 ans, qui, avec ses collègues, va devenir l’auteur de certaines des plus remarquables photographies de la guerre de Sécession, avec des portraits d’Abraham Lincoln et de Samuel Morse lui-même.

Mais ce que nous venons d’évoquer, ce ne sont que des moments remarquables d’une carrière éclectique. En effet, Samuel Morse est surtout connu de nos jours pour avoir inventé le télégraphe et le code qui porte son nom.

Si les capacités de communiquer de façon instantanée avec le monde entier nous semblent naturelle de nos jours, elles sont pourtant assez récentes. Au début des années 1800, il était possible de communiquer instantanément ou sur de longues distances, mais pas les deux à la fois. Les communications instantanées étaient limitées à la portée de la voix ou à celle du regard, en utilisant éventuellement des jumelles. Concernant les communications longue distance, il fallait recourir à des chevaux, des trains ou des bateaux pour transporter le courrier.

Durant plusieurs décennies avant l’invention du morse, on a tenté d’accélérer les communications à distance. Une technique simple à mettre en œuvre se fondait sur un système de chaînes de personnes debout sur des collines ou des promontoires et agitant des drapeaux selon certains motifs, ce qui équivalait à des sémaphores. Des versions plus sophistiquées ont été inventées, notamment par le Sarthois Claude Chappe, sous la forme d’un mât avec des bras articulés, le résultat étant quasiment le même qu’avec des drapeaux.

Le concept de télégraphie, c’est-à-dire d’écriture à distance, était certainement déjà dans l’air au début des années 1800. D’autres inventeurs avaient travaillé sur le sujet avant que Samuel Morse ne commence ses essais en 1832. Le principe d’un télégraphe électrique est simple : vous réalisez une action à une extrémité d’un câble de sorte qu’il se produise une autre action à l’autre extrémité du câble. C’est exactement ce que nous avons fait dans le Chapitre 5 avec notre lampe à éclairs longue distance. Morse ne peut pas recourir à une lampe comme appareil de signalisation parce que l’ampoule ne sera inventée qu’en 1879. Morse s’est donc appuyé sur le principe de l’électromagnétisme.

C’est au physicien danois Hans Christian Œrsted que l’on doit la première étude systématique des relations entre électricité et magnétisme. Il avait publié en 1820 un article montrant comment un courant électrique faisait dévier l’aiguille magnétique d’une boussole. Le phénomène avait intrigué les meilleurs esprits scientifiques du xixe siècle, parmi lesquels Michael Faraday et James Clerk Maxwell. Le Traité sur l’électricité et le magnétisme de ce dernier, paru en 1873, reste de nos jours un classique de la physique et des mathématiques appliquées. Au moment de la parution de ce livre, d’ingénieux innovateurs tels que Samuel Morse utilisaient déjà depuis longtemps l’électromagnétisme. Il suffit en effet de prendre une barre de fer, de la recouvrir de plusieurs centaines de tours d’un fil de cuivre isolé puis de faire passer du courant dans ce fil. La barre de fer devient alors magnétique et va attirer d’autres pièces de fer ou d’acier. Il suffit de couper le courant pour que ce magnétisme disparaisse :

En première approche, ce montage semble entraîner un court-circuit. En réalité, les nombreux tours du très fin fil de cuivre produisent suffisamment de résistance électrique pour qu’il n’y ait pas de court-circuit.

Le composant essentiel d’un télégraphe est l’électro-aimant. En ouvrant et fermant l’interrupteur à une extrémité du câble, on provoque une réaction dans l’électro-aimant situé à l’autre extrémité.

Les premiers télégraphes réalisés par Morse étaient plus complexes que les versions épurées apparues ensuite. Morse pensait au départ que le système devait créer une trace écrite sur papier, ce que les utilisateurs d’informatique actuels désignent sous le terme de copie papier (hard copy). Il n’envisageait pas, bien sûr, imprimer des mots car cela aurait été trop complexe, mais il pensait que quelque chose pouvait être imprimé, que ce soient des rayures ou des griffures. Morse s’était enferré dans un paradigme imposant de gérer du papier et des capacités de lecture. Il est arrivé quasiment la même chose à Valentin Haüy lorsqu’il cherchait à produire des livres pour les aveugles : il stipulait qu’il fallait produire des lettres gaufrées en relief de l’alphabet.

Dès 1836, Samuel Morse avait informé le bureau des brevets qu’il avait réussi à inventer un télégraphe fonctionnel. Pourtant, il n’a pu persuader le Congrès de financer une démonstration publique qu’en 1843. La fameuse démonstration eut lieu le 24 mai 1844. Une ligne télégraphique avait été tirée entre Washington, DC et Baltimore dans le Maryland. Elle a permis de transmettre un message biblique extrait du Livre des nombres (Chapitre 23, Verset 23) : « Qu’est-ce que Dieu a fait », qui n’est pas une question mais une exclamation du style « Admirez ce que Dieu a fait ! ».

Voici l’aspect du bouton du manipulateur d’un télégraphe traditionnel pour envoyer un message :

Son aspect ne cherchait pas l’élégance, car il devait d’abord garantir une vitesse maximale d’émission. Pour utiliser ce bouton longtemps, le mieux consiste à le tenir entre pouce, index et majeur pour pouvoir facilement le lever et l’abaisser. Un appui court produit un point et un appui long produit un trait.

L’autre extrémité de la liaison correspond au récepteur qui se résume à un électro-aimant agissant sur un levier métallique. Au départ, il agissait sur un stylo qui se levait et se baissait pour tracer des points et des traits sur un rouleau de papier qui avançait lentement sous l’effet d’un ressort. Une personne sachant lire le morse pouvait alors transcrire les points et traits en lettres et en mots.

L’espèce humaine est à la fois paresseuse et ingénieuse. Les premiers opérateurs télégraphiques ont très vite remarqué qu’ils étaient en mesure de transcrire le code morse simplement en écoutant le son de la plume qui se baisse et se lève. Ils ont donc supprimé le mécanisme de tracé pour ne conserver que la partie sonore connue des télégraphes classiques qui présentaient l’aspect suivant :

Le barreau mobile en haut est maintenu éloigné de la bobine, donc en position horizontale, au moyen d’un contrepoids ou d’un ressort situé dans le support vertical de gauche. Ce barreau doit pouvoir s’abaisser par rotation. En abaissant le levier du manipulateur morse, l’électro-aimant attire le barreau métallique vers le bas, ce qui lui fait toucher la partie inférieure du support de droite et émettre un clic. Lorsque le courant est interrompu, le barreau remonte dans sa position de repos en émettant un autre bruit, du style clac. L’enchaînement rapide d’un clic et d’un clac correspond à un point et l’enchaînement plus lent, à un trait.

On peut réaliser le même montage que celui utilisé dans le chapitre précédent qui utilisait une lampe en combinant un manipulateur, une pile, un électro-aimant et du fil de cuivre :

Nous avons vu dans le Chapitre 5 que l’on pouvait se contenter d’un seul fil entre les deux stations télégraphiques, à condition que la tension (le voltage) suffise. On peut en effet utiliser la terre (masse) comme voie de retour du courant.

Nous avons également vu dans le Chapitre 5 que l’on pouvait simplifier le schéma en remplaçant la source d’énergie qu’est la pile par la simple mention V pour voltage. Notre schéma allégé pour une communication dans un seul sens (unidirectionnelle) va prendre l’aspect suivant :

Pour une communication bidirectionnelle (dans les deux sens), il suffit de prévoir un second équipement installé en sens inverse, comme nous l’avons déjà vu.

On peut vraiment considérer l’apparition du télégraphe comme le début des communications modernes. Pour la première fois, il devenait possible de transmettre des informations au-delà de la portée des yeux ou des oreilles, et plus vite qu’un cheval au galop. Le fait que l’inventeur ait choisi d’utiliser un code binaire est tout à fait remarquable. Les systèmes de communication électrique filaires ou sans fil ultérieurs, en particulier le téléphone, la radio et la télévision, n’ont plus utilisé de code binaire. Ce n’est qu’avec l’avènement de l’informatique que ce dernier est revenu en force pour se voir adopter dans quasiment tous les appareillages électroniques.

Si le télégraphe morse a supplanté les systèmes rivaux, c’est en partie parce qu’il est très tolérant en cas de mauvaise qualité de la liaison électrique. En général, le simple fait de tirer un fil entre un manipulateur et un récepteur permettait une liaison fiable, ce qui était bien moins évident avec les autres systèmes télégraphiques à l’époque. Malgré cela, et comme nous l’avons déjà vu dans le Chapitre 5, plus la liaison est longue, plus la résistance au passage du courant augmente. Cela a longtemps constitué un obstacle majeur pour les liaisons télégraphiques à longue distance. Pour contourner le problème, certaines liaisons exploitaient jusqu’à 300 V de tension, ce qui permettait de couvrir une distance de 500 km, mais on atteignait là une limite définitive.

La solution évidente consistait à ajouter des relais intermédiaires. Il fallait poster un opérateur avec un émetteur et un récepteur tous les 200 ou 300 km, l’opérateur se chargeant de lire le message entrant pour le renvoyer immédiatement.

Imaginez que vous venez d’être embauché par une compagnie télégraphique pour jouer ce rôle de relais. Vous avez été installé en pleine campagne entre New York et la Californie dans une petite cabane avec une table et une chaise. Un câble arrivant par la fenêtre du côté est relié au récepteur. Votre manipulateur est connecté à une pile et au câble qui sort par la fenêtre du côté ouest. Votre travail consiste à recevoir les messages de New York pour les réémettre vers la Californie. Le même système de relais est utilisé dans l’autre sens, pour les messages de la Californie vers New York.

Dans un premier temps, vous attendez d’avoir reçu la totalité d’un message avant de commencer à le renvoyer. Vous notez les lettres qui correspondent aux clics puis vous transmettez le message avec votre manipulateur. Vous remarquez rapidement que vous pouvez aller plus vite en répétant directement avec le manipulateur ce que vous entendez au fur et à mesure, donc sans perdre de temps à noter les mots avant les renvoyer.

Un beau jour, vous admirez le barreau mobile du récepteur qui descend et remonte sans arrêt puis vous regardez vos doigts qui montent et qui descendent sur le manipulateur. En comparant les deux événements, vous finissez par comprendre que le mouvement est exactement le même. Vous sortez alors de votre cabane à la recherche d’une petite branche rigide que vous fixez de telle sorte que le manipulateur soit mû par le mouvement de la barre du récepteur :

Voilà que votre relais est devenu automatique, et vous allez pouvoir passer le reste de l’après-midi à taquiner le goujon dans la rivière en contrebas.

Amusante, cette petite histoire, mais en réalité, Samuel Morse avait compris l’astuce très tôt. L’équipement que nous venons d’inventer est un répéteur, autre nom pour un relais. Dans ce genre d’appareil, le courant d’entrée alimente l’électro-aimant qui attire la barre de l’émetteur. La barre peut faire partie d’un interrupteur qui va connecter une pile au câble de sortie. Cette technique permet d’ailleurs d’utiliser un courant faible en entrée pour générer un courant fort en sortie, c’est-à-dire l’amplifier.

Le schéma minimaliste d’un relais est celui-ci :

Lorsqu’un courant en entrée excite l’électro-aimant, celui-ci attire vers le bas le contact métallique, ce qui ferme le circuit de sortie :

Les deux termes entrée et sortie rappellent les deux fenêtres du côté entrant et du côté sortant de la cabane à relais. Leur version anglaise, In et Out, sont les termes génériques employés en électronique pour une arrivée et un départ d’information sous forme de signaux électriques. Il y a une relation de cause à effet : le signal du circuit d’entrée provoque une modification du signal dans le circuit de sortie.

Le relais électromagnétique est un composant remarquable. Il joue un rôle d’interrupteur qui est manipulé non pas par un humain, mais par un courant électrique. Ce composant permet d’envisager énormément de fonctions. Vous pouvez même quasiment construire un ordinateur uniquement avec des relais.

Vous devinez que nous n’allons pas abandonner ce fameux relais dans le musée de la télégraphie. Empruntons-en un exemplaire au musée, fourrons-le dans notre poche et sortons fièrement devant les gardiens : nous venons d’avoir une idée.

Chapitre 8  Des relais aux portes

En simplifiant à l’extrême, un ordinateur est la synthèse concrète entre l’algèbre booléenne et l’électricité. Les composants fondamentaux qui réalisent cette fusion entre mathématiques et matériel sont des portes logiques1.

Une porte logique peut tout à fait être comparée au concept habituel de porte matérielle, objet mobile qui ne peut être que dans un état parmi deux : soit ouverte, soit fermée. Une porte logique effectue une opération de logique booléenne consistant à laisser passer ou ne pas laisser passer du courant électrique sur sa sortie.

Nous avions au cours du Chapitre 6 pris comme exemple une visite dans une animalerie pour adopter un chat. Nous avions spécifié toute une batterie de critères de sélection : « Je veux un chat mâle castré, blanc ou tigré ou alors un chat femelle stérilisé de n’importe quelle couleur sauf blanc ; ou bien n’importe quel chat du moment qu’il soit noir ». Rappelons que la demande exprimée en algèbre booléenne donnait l’expression suivante :

(M × K × (B + T)) + (F × K × (1 – B)) + N

Nous avions construit un circuit avec huit interrupteurs dans trois branches, une pile et une lampe :

Figure 8.1

Certains électroniciens parlent de ce genre de schéma comme d’un réseau, mais de nos jours, le terme « réseau » est devenu synonyme de système d’interconnexion entre ordinateurs, et notamment du grand réseau Internet. Le concept de réseau d’interrupteurs n’a plus cours.

Notre circuit est constitué d’un certain nombre d’interrupteurs, dont certains sont branchés en série et d’autres en parallèle. Vous vous souvenez que deux interrupteurs reliés en série incarnent l’opération logique EST (AND), ce qui correspond à l’opérateur booléen ×. De même, deux interrupteurs reliés en parallèle incarnent l’opération logique OU (OR) et l’opérateur +. Notre circuit représentant une expression booléenne, si nous parvenions à simplifier l’expression, nous pourrions simplifier le câblage du circuit !

Rappelons une fois de plus l’expression correspondant aux critères de sélection du chat :

(M × K × (B + T)) + (F × K × (1 – B)) + N

Essayons de la simplifier. En appliquant la loi de commutativité, nous pouvons réorganiser les variables qui sont réunies par les opérateurs ET, ce qui permet de reformuler l’expression ainsi :

(K × M × (B + T)) + (K × F × (1 – B)) + N

Pour alléger la suite de l’étude, je décide de définir deux nouveaux symboles X et Y :

X = M × (B + T)

Y = F × (1 - B)

L’expression complète peut dorénavant se présenter de manière bien plus lisible :

(K × X) + (K × Y) + N

Nous pourrons développer à nouveau les deux sous-expressions X et Y en fin de processus. Vous remarquez que la variable K apparaît deux fois dans l’expression. Profitons de la loi de distributivité pour reformuler l’expression avec un seul K factorisé :

(K × (X + Y)) + N

Nous pouvons maintenant redévelopper les deux sous-expressions X et Y :

(K × ((M × (B + T)) + (F × (1 – B)))) + N

Bien sûr, nous récoltons une pléthore de parenthèses, au point que l’on se demande en quoi l’expression a été simplifiée. Mais si vous comptez bien, vous verrez qu’il y a une variable de moins. Nous avons donc économisé un interrupteur, comme le confirme le nouveau circuit2 :

Figure 8.2

Il est clairement plus évident de voir que le circuit a été simplifié en comparant les deux schémas électriques plutôt qu’en comparant les deux expressions !

Néanmoins, il y a encore trop d’interrupteurs. Il y en a, par exemple, deux pour le choix du sexe, un pour mâle et un pour femelle alors qu’un seul devrait suffire. Nous pourrions décider que l’état Fermé de l’interrupteur signifie Femelle et l’état Ouvert, mâle. De même, il y a deux interrupteurs pour tester si le pelage est blanc ou non blanc.

Pour plus de confort, construisons un panneau de sélection. Il va comprendre cinq interrupteurs (du même type que ceux qui servent à allumer la lumière) et un témoin lumineux qui sert de lampe :

Figure 8.3

Nous faisons en sorte qu’un interrupteur est en position levée lorsqu’il est fermé et en position baissée lorsqu’il est ouvert. Le premier interrupteur sert à choisir entre mâle et femelle. Le deuxième entre castré et non castré (entier). Les trois interrupteurs restants sont consacrés à la couleur du pelage qui peut être choisi noir, blanc ou tigré, ou bien d’une autre couleur si aucun des trois interrupteurs n’est fermé.

Dans la terminologie informatique, ce panneau est un périphérique d’entrée. Les entrées sont les données qui déterminent comment le circuit va se comporter, ici pour confirmer qu’un ensemble d’états des leviers satisfait ou pas à un ensemble de critères pour trouver le chat idéal. Le périphérique de sortie correspond ici à la lampe qui ne s’allume que si les critères sont satisfaits par un chat candidat. Dans la Figure 8.3, les interrupteurs sont positionnés pour sélectionner un chat femelle entier et noir. Cela satisfait largement aux critères et la lampe brille.

Nous pouvons maintenant nous atteler à la conception du circuit qui permettra d’utiliser ce panneau de sélection.

Nous avons vu dans les chapitres précédents l’importance de ce composant qu’est le relais dans le système télégraphique. Sur de grandes distances, la résistance du câble devenait de plus en plus critique. Il avait fallu trouver une solution pour raviver un signal devenu trop faible et le renvoyer. C’est à cela qu’a servi le relais, en faisant en sorte qu’un électro-aimant contrôle directement un interrupteur, ce qui constitue une amplification de signal.

Mais, pour le moment, ce n’est pas l’utilisation d’un relais en tant qu’amplificateur de signal qui nous intéresse. Ce que nous voulons, c’est utiliser le relais comme un interrupteur contrôlé non par des doigts, mais par l’électricité. Au départ, les relais étaient employés pour la télégraphie, puis ils ont été adoptés à vaste échelle dans les commutateurs des réseaux téléphoniques. C’est ainsi que leur grande polyvalence a été remarquée par de plus en plus d’ingénieurs en électricité imaginatifs.

Comme avec des interrupteurs, on peut relier des relais en série et en parallèle pour concrétiser des opérations logiques simples. Le choix du terme est capital : les circuits doivent être les plus simples possibles. L’énorme avantage des relais par rapport aux interrupteurs manuels est qu’un relais peut être ouvert ou fermé par un autre relais. Il devient ainsi possible de combiner de grandes quantités de portes logiques pour réaliser des tâches logiques complexes, par exemple des fonctions d’arithmétique, et finalement un ordinateur complet.

La découverte de la possibilité de réaliser des opérations logiques booléennes avec des relais est généralement mise au crédit du pionnier de l’informatique Claude Elwood Shannon (1916-2001). Il avait soutenu en 1938 son mémoire de maîtrise à l’institut technologique MIT sous le titre Analyse symbolique des relais et des circuits à commutation. Rappelons néanmoins l’existence quelques années auparavant d’un travail similaire sous la plume de l’ingénieur en électronique japonais Akira Nakashima.

On peut combiner un relais avec un interrupteur, une lampe et deux piles électriques ainsi :

Figure 8.4

Dans ce schéma, l’interrupteur de gauche est ouvert et la lampe est donc éteinte. Dès que vous fermez l’interrupteur, la pile de gauche délivre son courant à travers les tours du fils autour du barreau métallique, ce qui le magnétise. Le résultat est que la barre métallique rotative en haut du relais est attirée vers le bas, ce qui ferme le contact du circuit de sortie et fait briller la lampe :

Figure 8.5

Lorsque l’électro-aimant attire le contact métallique, on dit que le relais est dans l’état excité. Lorsque l’on coupe le courant dans le relais, la barre n’est plus attirée et revient à sa position de repos sous l’effet d’un ressort.

Tout cela fait penser à une solution inutilement complexe pour allumer une lampe. Si le but du montage était d’allumer la lampe, le relais est inutile. Mais ce n’est pas qu’une lampe que nous voulons allumer. Nous avons un objectif bien plus ambitieux.

Au cours de ce chapitre, je vais beaucoup utiliser les relais, puis quasiment les abandonner une fois que nous aurons découvert les portes logiques. Voyons comment nous pourrions simplifier le schéma. Nous pouvons nous passer de certains fils en utilisant la symbolique de la terre et de la source de tension V comme nous l’avons fait dans les Chapitre 5 et 7. Dans notre exemple, la terre symbolise une connexion commune ; il n’est pas nécessaire que le tout soit connecté réellement à la terre géographique. Voici comment le schéma en est allégé :

Figure 8.6

Lorsque l’on ferme l’interrupteur, on permet au courant de passer entre le point V et la terre, ce qui le fait circuler dans les tours de bobine de l’électro-aimant. Ce dernier provoque ainsi l’attraction du contact métallique mobile, qui établit le courant entre la source de tension V et l’autre connexion à la terre, provoquant enfin l’allumage de la lampe :

Figure 8.7

Les schémas montrent deux sources de tension et deux prises de terre, mais nous pouvons relier tous les points V ensemble et toutes les terres aussi.

Nous pouvons même généraliser le schéma du relais en supprimant l’interrupteur et la lampe, mais en utilisant les deux termes Entrée et Sortie :

Figure 8.8

Dès que du courant circule dans l’entrée (par exemple, avec un interrupteur qui relie cette entrée à un point V), cela excite l’électro-aimant qui provoque l’apparition d’une tension sur la sortie.

Il n’est pas nécessaire que le point d’entrée du relais soit un interrupteur. De même, la sortie du relais peut être autre chose qu’une lampe. On peut tout à fait relier la sortie d’un relais à l’entrée d’un autre, par exemple ainsi :

Figure 8.9

Dans ce genre de montage, les relais sont en cascade. Lorsque vous fermez l’interrupteur à gauche, le premier relais est excité, ce qui provoque l’excitation du second relais, et finalement l’allumage de la lampe :

Figure 8.10

La possibilité de connecter des relais les uns aux autres est la clé de la construction de portes logiques.

La porte ET (AND)

On peut relier deux relais en série, comme nous l’avions fait pour deux interrupteurs :

Figure 8.11

Vous constatez que nous avons deux interrupteurs, un par relais. La sortie du relais du haut détermine si une tension peut apparaître sur la sortie du relais du bas. Lorsque les deux interrupteurs d’entrée sont ouverts, l’ampoule ne s’allume pas. Voyons ce qui se passe lorsque nous fermons uniquement l’interrupteur du relais du haut :

Figure 8.12

La lampe ne s’allume pas, parce que l’interrupteur d’entrée du bas est ouvert, et le relais correspondant n’est donc pas excité. Inversons les états des deux relais. Avec l’interrupteur fermé en bas et l’autre ouvert en haut, nous obtenons ceci :

Figure 8.13

La lampe ne brille toujours pas. Le circuit d’alimentation de l’ampoule n’est en effet pas fermé parce que le relais du haut n’est pas excité. Pour que l’ampoule brille, il faut que les deux interrupteurs d’entrée soient fermés :

Figure 8.14

Une fois les deux relais excités, le courant peut circuler entre V et la terre en passant par la lampe.

Les deux relais montés de cette manière concrétisent un raisonnement de logique, comme l’ont fait les deux interrupteurs branchés en série dans le Chapitre 6. La lampe ne s’allume que si les deux relais sont excités. Lorsque deux relais sont branchés en série, ils incarnent une porte logique ET qui réalise l’opération booléenne ET (AND).

Afin d’alléger leurs schémas, les ingénieurs en électronique ont choisi un symbole simple pour représenter la porte logique ET :

Il s’agit de la première des six catégories fondamentales de portes logiques. Une porte ET possède deux entrées et une sortie. En général, ses entrées sont du côté gauche et sa sortie, du côté droit. Cette convention découle du fait que la plupart des gens lisent de gauche à droite en Occident, et préfèrent parcourir les schémas électriques de la même façon. En théorie, rien n’empêche de dessiner le symbole de la porte avec les entrées en haut, à droite ou en bas.

Nous pouvons maintenant reformuler de façon beaucoup plus lisible le montage des schémas précédents qui incorporent deux relais :

Ce symbole de porte remplace les deux relais connectés en série. Il masque la connexion du relais du haut avec une source de tension, ainsi que la connexion à la terre des deux relais. Rappelons que la lampe ne va s’allumer que si les deux interrupteurs sont fermés, ce qui est la caractéristique même d’une porte logique ET.

Nous pouvons considérer que l’absence de tension équivaut à 0 et que la présence équivaut à 1. La sortie d’une porte logique ET dépend donc des deux entrées de la façon suivante :

Le fonctionnement de la porte ET peut être résumé par le petit tableau suivant, identique à celui vu pour les deux interrupteurs branchés en série :

ET

0

1

0

0

0

1

0

1

Nous avions dit qu’une entrée de porte logique pouvait être connectée à autre chose qu’un interrupteur. De même, la sortie n’est pas nécessairement reliée à une lampe. On peut tout à fait utiliser la sortie d’une porte comme entrée d’une autre porte :

Dans ce montage, la lampe ne va s’allumer que si les trois interrupteurs sont fermés. La sortie de la porte ET du haut ne sera sous tension que si ses deux entrées le sont. La sortie de la porte ET du bas ne sera sous tension que si l’autre entrée de cette porte est également sous tension.

On peut reformuler ce montage avec le symbole suivant :

On appelle cela une porte logique à trois entrées. La sortie sera à 1 seulement si les trois entrées le sont. On peut tout à fait imaginer des portes ET ayant encore plus d’entrées.

La porte OU (OR)

Découvrons maintenant l’autre catégorie fondamentale de porte logique, celle qui correspond à deux relais connectés en parallèle et non en série :

Figure 8.15

Vous constatez que les sorties des deux relais sont interconnectées. Vous devinez que la lampe va s’allumer dès que l’une des deux sorties présentera de la tension. Fermer l’interrupteur du haut suffit à faire briller la lampe puisqu’elle va être alimentée par le circuit du relais du haut :

Figure 8.16

Si nous intervertissons les états des deux interrupteurs en fermant celui du relais du bas et en ouvrant l’autre, la lampe brille également :

Figure 8.17

La lampe brille également lorsque les deux interrupteurs sont fermés3 :

Figure 8.18

Ce nouveau montage allume la lampe dès que l’interrupteur du haut ou celui du bas est fermé. Le mot-clé est le mot OU et c’est pourquoi on parle de porte logique OU (OR). Les ingénieurs en électronique ont choisi un autre symbole pour cette porte :

Le symbole ressemble à celui de la porte ET sauf que le côté des deux entrées est incurvé pour suggérer la courbe du côté droit de la lettre O majuscule (cela peut servir d’indice mnémotechnique).

Il y aura présence de tension sur la sortie de la porte dès que l’une des deux entrées sera sous tension. Nous conservons la convention consistant à dire que l’absence de tension est égale à 0 et que la présence est égale à 1. La porte OU peut donc être dans l’un des quatre états suivants :

Comme pour la porte ET, nous pouvons dresser le tableau logique des comportements d’une porte OU :

OU

0

1

0

0

1

1

1

1

Comme la porte ET, la porte OU peut avoir plus de deux entrées. La sortie sera à 1 dès qu’une des entrées le sera, et à 0 seulement lorsque toutes les entrées seront à 0.

La porte NON (NOT)

Les relais que nous avons utilisés jusqu’ici sont ce que l’on appelle des relais bipolaires. Lorsque le relais est au repos, le contact pivotant touche une borne puis en touche une autre tant que l’électro-aimant est excité. Le contact du bas correspond à ce que l’on appelle la sortie normalement ouverte (pas de conduction), abrégée en NO. C’est celle utilisée jusqu’ici, mais nous pouvons tout à fait utiliser le contact d’en haut qui est le contact de l’état normalement fermé, abrégé en NF. Si nous choisissons ce contact, la logique de sortie du relais sera inversée. L’ampoule ne s’allume que lorsque l’interrupteur d’entrée est ouvert :

Figure 8.19

Lorsque l’électro-aimant est alimenté par l’entrée, et donc excité, la lampe s’éteint :

Figure 8.20

Lorsqu’un relais est utilisé de cette façon, il devient un inverseur. L’inverseur logique fait l’objet d’un symbole spécial :

On parle d’inverseur parce que ce composant inverse l’état : une entrée à 0 donne une sortie à 1 et inversement :

Ce montage est une réalisation concrète de l’opérateur booléen NON (NOT).

Les personnes qui voient le premier des deux inverseurs ci-dessus se demandent parfois comment il est possible qu’il y ait une tension en sortie alors qu’il n’y a aucune tension en entrée. D’où vient cette tension ? Il suffit de se souvenir que le symbole de l’inverseur sous-entend un relais connecté à une source de tension.

Application féline

Grâce à notre inverseur et à nos deux portes logiques ET et OU, nous allons construire le schéma du circuit qui va être relié aux interrupteurs du panneau de commande de sélection de chat idéal (voir Figure 8.3).

Partons des interrupteurs en façade. Le premier à gauche est fermé (position haute) pour signifier une femelle (il est ouvert pour un mâle). Nous pouvons donc, grâce à lui, faire produire deux signaux que nous appelons F et M :

Lorsque F vaut 1, M vaut 0 et inversement. Le deuxième interrupteur est fermé pour un chat castré (K) et ouvert pour un chat entier (E) :

Les trois autres interrupteurs sont plus simples puisque chacun d’eux sert à sélectionner une couleur ou non : noir, blanc ou tigré. Nous pouvons relier les trois interrupteurs à la même source de tension :

Quelques règles simples sont à respecter absolument quant à la façon de connecter des portes entre elles :

La sortie d’une porte inverseuse peut servir d’entrée à plusieurs autres portes.

En revanche, cela n’aurait aucun sens de connecter la sortie de deux portes différentes ou plus à une seule entrée4.

Rappelons la version allégée de l’expression booléenne de sélection du chat :

(K × M × (B + T)) + (K × F × (1 – B)) + N

Chaque opérateur + de cette expression doit être incarné par une porte logique OU et chaque opérateur ×, par une porte logique ET.

Figure 8.21

Les lettres des signaux du côté gauche du diagramme sont dans le même ordre que dans l’expression booléenne. Ces signaux proviennent des sorties des trois petits circuits précédents. Notez l’ajout d’un inverseur pour exprimer l’état « pelage non blanc » (1 – B) en fin d’expression.

Vous serez peut-être étonné de voir apparaître une véritable petite communauté de portes logiques et vous n’aurez pas tort. Dans chaque porte ET ou OU, il y a deux relais et il faut un relais dans chaque inverseur. Rassurez-vous, si on peut dire : vous allez rencontrer bien plus de relais dans les chapitres à venir ! Soyez heureux de ne pas devoir les acheter à l’unité puis les interconnecter chez vous (sauf si vous en avez envie par curiosité).

J’ai indiqué un peu plus haut qu’il existe six portes logiques fondamentales. Nous en avons découvert trois pour l’instant. Les deux premières se fondent sur la sortie normalement fermée (NF) du relais déjà utilisé par l’inverseur. Rappelons que, dans la configuration NF, il y a une tension en sortie lorsqu’il n’y a pas de tension d’entrée (normalement signifie « en l’absence d’excitation »). Dans le montage suivant, la sortie NF du premier relais alimente le second.

La porte NOR (OU-NON)

Dans ce montage, la lampe brille lorsque les deux entrées sont à 0, non alimentées :

Figure 8.22

Dès que l’interrupteur du haut est fermé, le relais du haut est excité, son contact s’ouvre et l’ampoule s’éteint :

Figure 8.23

L’ampoule s’étend parce que le contact NF du relais du bas n’est plus alimenté par la sortie NF du relais du haut. Si nous inversons les états en ouvrant l’interrupteur du haut et fermons celui du bas, l’ampoule reste éteinte :

Figure 8.24

Vous ne serez pas étonné de découvrir que, lorsque les deux interrupteurs sont fermés, l’ampoule reste muette :

Figure 8.25

Si l’on récapitule, le comportement est strictement l’inverse de celui d’une porte OU. On parle d’ailleurs de porte NON OU, mais on utilise de préférence l’abréviation anglaise NOR. Voici le symbole d’une porte NOR :

Vous constatez que c’est le même symbole que pour une porte OU sauf qu’un petit cercle a été ajouté sur la sortie, ce cercle symbolisant l’inversion d’état. Une porte NOR combine une porte OU suivie d’une porte NON, un inverseur :

Nous pouvons dresser le tableau de vérité de la porte NOR :

NOR

0

1

0

1

0

1

0

0

Vous pouvez comparer ce tableau avec celui de la porte OU : ici, la sortie est à 1 seulement si les deux entrées sont à 0.

La porte NAND (ET-NON)

Découvrons l’autre manière de connecter les sorties normalement fermées de deux relais :

Figure 8.26

Dans ce montage, les deux sorties sont connectées en parallèle, comme pour une porte OU, sauf que nous utilisons maintenant les contacts NF, normalement fermés. L’ampoule va briller dès que les deux entrées seront à 0, donc dès que les deux interrupteurs seront ouverts.

Si l’on ferme interrupteur du haut, l’ampoule reste allumée parce qu’elle continue à être alimentée par le relais du bas qui est au repos :

Figure 8.27

Si nous intervertissons l’état des deux entrées, l’ampoule continue à briller puisqu’elle est dorénavant alimentée par le relais du haut au repos :

Figure 8.28

L’ampoule va finir par changer d’état, donc s’éteindre, si l’on ferme les deux interrupteurs :

Figure 8.29

Le comportement est exactement l’inverse de celui d’une porte ET, et c’est pourquoi on parle d’une porte NON ET, abrégée en NAND (on n’utilise pas non plus l’abréviation française NET ici). Précisons qu’à la différence de NOR, l’abréviation NAND a véritablement été adoptée pour parler de cette porte logique dès 1958.

Le symbole d’une porte NAND correspond à celui d’une porte ET avec un cercle sur la sortie, ce qui signifie que l’état de la sortie est l’inverse de celui d’une porte ET :

Nous pouvons dresser le tableau de vérité de la porte NAND :

NAND

0

1

0

1

1

1

1

0

Vous vous souvenez que, dans le cas de la porte ET, la sortie est à 1 seulement si les deux entrées sont à 1 et à 0 dans les trois autres cas. La sortie de la porte NAND est exactement l’inverse.

Synthèse d’étape

Pour le moment, nous avons découvert quatre solutions différentes pour interconnecter deux relais ayant deux entrées et une sortie chacun. Les comportements se distinguent les uns des autres. Pour simplifier les schémas, nous avons décidé de symboliser les relais sous forme de portes logiques, en adoptant les symboles utilisés par les ingénieurs en électronique. L’état de la sortie de chacune de ces quatre portes logiques dépend de l’état de ses deux entrées, ce que nous résumons par les quatre tableaux suivants :

Nous y ajoutons la porte inverseuse symbolisée ainsi :

Son rôle se résume à inverser l’état du signal d’entrée qui passe de 0 à 1 ou de 1 à 0.

Le tampon (buffer)

Complétons notre ménagerie avec une porte très simple dont le comportement correspond directement à celle d’un relais :

Figure 8.30

Cette porte est tout à fait transparente, puisque l’état de sa sortie est strictement identique à l’état de son entrée. On appelle ce composant un tampon ou buffer, et voici le symbole correspondant :

Force est de constater que le symbole est le même que pour l’inverseur, mais sans le petit cercle d’inversion. En effet, ce tampon n’a aucun effet en termes logiques puisque l’état de la sortie est identique à celui de l’entrée :

Un circuit tampon va s’avérer utile dès que le signal d’entrée est trop faible. Vous vous souvenez que c’est la raison pour laquelle ont été inventés les relais au départ : pour amplifier le signal d’une ligne télégraphique afin de lui permettre de couvrir une distance plus grande. Dans les circuits électroniques logiques, il est nécessaire dans certains cas d’alimenter plusieurs entrées à partir d’une sortie, ce qui correspond à une sortie éventaillée (fan out). Vous devinez certainement que la puissance disponible sur chaque entrée va être réduite ; un circuit tampon permet de renforcer le niveau du signal. On peut également avoir recours à un tel tampon pour retarder la propagation d’un signal. Il faut en effet toujours une petite fraction de seconde pour que la sortie adopte l’état de l’entrée.

Dans la suite du livre, nous ne rencontrerons que rarement des relais. Nous allons construire nos circuits à partir des composants logiques fondamentaux que sont les tampons, les inverseurs, les quatre types de portes logiques à deux entrées puis quelques circuits plus complexes fondés sur ces composants élémentaires. Tous les composants restant à découvrir fonctionnent sur le principe du relais, mais nous n’aurons plus besoin de descendre au niveau de détail de ces relais.

En début de chapitre, nous avions proposé de construire un panneau pour sélectionner le chat idéal. Il est équipé de cinq interrupteurs, pour les trois couleurs de pelage noir, blanc et tigré, mais il n’y a pas d’interrupteur pour sélectionner les autres couleurs, c’est-à-dire un pelage ni noir ou blanc, ni tigré. La raison en est qu’un signal correspondant à cet autre choix peut être produit en utilisant trois inverseurs et une porte ET à trois entrées :

L’état des trois entrées est inversé puis fourni en entrée d’une porte ET. La sortie de cette porte sera dans l’état 1 seulement si les trois entrées sont dans l’état 0.

Une version simplifiée de cette porte se présente ainsi :

Vous remarquez qu’il y a trois petits cercles d’inversion sur les entrées de la porte. L’absence de tension devient une présence de tension dans chaque entrée, et inversement.

S’il ne fallait sélectionner qu’un type de porte logique parmi les six présentés jusqu’ici, ce serait soit la porte NAND, soit la porte NOR. En effet, chacune de ces deux portes permet de créer toutes les autres. Voici, par exemple, comment créer un inverseur en reliant les deux entrées d’une porte NAND :

En positionnant cet inverseur sur la sortie d’une porte NAND, vous obtenez une porte ET. En revanche, vous pourriez croire qu’il est impossible de remplacer une porte OU avec une porte NAND. C’est possible, car une porte ET dont toutes les entrées sont inversées équivaut à une porte NOR :

La sortie est dans l’état 1 seulement si les deux entrées sont à l’état 0.

De même, une porte OU dont les deux entrées sont inversées équivaut à une porte NAND :

Ici, la sortie est à 0 seulement si les deux entrées sont à 1.

Ces deux paires de circuits équivalents constituent une mise en œuvre électrique des lois de De Morgan. Né neuf ans avant George Boole, Augustus De Morgan était lui aussi un mathématicien de l’époque victorienne. Son livre sur la logique formelle est paru en 1847, soi-disant exactement le même jour que celui sur l’analyse mathématique de la logique de George Boole. Il est vrai que Boole avait été motivé à s’intéresser à la logique en ayant connaissance de la dispute publique entre De Morgan est un autre mathématicien britannique à propos d’une accusation de plagiat (l’histoire a innocenté De Morgan). Ce fameux De Morgan a reconnu l’importance des travaux de Boole et l’a encouragé de façon très altruiste pendant ses travaux. Il est triste de constater que, de nos jours, De Morgan est quasiment oublié, sauf pour ses fameuses lois.

Ces lois de De Morgan peuvent être exprimées de façon très compacte ainsi :

Les lettres A et B correspondent à deux opérandes booléens. Les traits au-dessus des lettres indiquent l’inversion de l’état. Dans la première expression, les deux opérandes A et B sont inversés puis combinés par l’opérateur booléen ET. Cela revient au même que de combiner les deux opérateurs avec un opérateur booléen OU puis inverser le résultat, ce qui correspond à la porte NOR. Cela fonctionne également en langue anglaise ou française : s’il ne pleut pas ET s’il ne neige pas, alors il ne pleut pas OU il ne neige pas.

Dans la deuxième expression de De Morgan, les deux opérandes sont inversés puis combinés par un opérateur OU. C’est l’équivalent d’une combinaison des opérandes avec un opérateur ET suivie d’une inversion, ce qui donne le circuit NAND. Par exemple, « si je suis pas gros OU si je ne suis pas fort, alors je ne suis pas gros ET fort à la fois ». Ces lois de De Morgan sont indispensables pour simplifier les expressions booléennes et, donc, les circuits électroniques qui les incarneront. C’est au départ ce que cherchait à démontrer la thèse de Claude Shannon selon les ingénieurs en électronique. Pour autant, nous n’allons pas chercher à trop simplifier les circuits dans la suite de ce livre. Il faut d’abord faire fonctionner les choses avant de chercher à les simplifier le plus possible.

Notre prochain projet d’importance sera ambitieux puisqu’il s’agira d’une machine numérique à additionner, uniquement constituée de portes logiques. Mais pour attaquer ce projet, nous devons passer par quelques chapitres préliminaires, car il nous faut réapprendre à compter.

1 N.d.T. : pour être exact, ce ne sont pas les portes qui sont logiques ; ce sont des portes permettant de réaliser des opérations logiques, donc des portes de logique. Nous conservons néanmoins l’expression « portes logiques » (qui ne l’est pas).

2 N.d.T. : vous constatez que l’état castré (K) ou non (E = non K) est dorénavant testé en entrée des deux branches du haut.

3 N.d.T. : il n’est pas inutile de rappeler que le fait qu’une tension soit présente simultanément sur les deux sorties n’a pas d’effet négatif.

4 N.d.T. : combiner des portes sur une entrée revient à mélanger des états logiques, et le résultat n’a plus aucun sens.

Chapitre 9  Sur nos dix doigts

Il n’est pas difficile d’admettre que les langues humaines sont des codes. La plupart d’entre nous ont eu l’occasion de découvrir au moins une langue étrangère au collège. Nous savons que l’animal que nous appelons « chat » est appelé dans d’autres pays gato, cat, Katze, кошка ou γάτα.

En revanche, il semble que le comptage au moyen de chiffres soit moins polymorphe. Dans énormément de langues, et quelle que soit la façon dont ils sont prononcés, les chiffres s’écrivent de la façon suivante :

1 2 3 4 5 6 7 8 9 10

Il y a sans doute une raison pour que les mathématiques soient considérées comme le langage universel.

Parmi tous les codes que nous utilisons au quotidien, les plus abstraits sont certainement les chiffres. Regardez le chiffre suivant :

3

Vous n’avez même pas besoin de le mettre en relation avec quelque chose de concret pour vous le représenter. Il peut concerner 3 pommes ou trois autres choses, ou bien un jour de naissance, le numéro d’une chaîne de télévision, le nombre de buts de votre équipe favorite, le nombre d’œufs dans une recette ou simplement le mois de mars. Nos chiffres sont tellement abstraits au départ qu’il faut faire un effort pour accepter que la quantité de pommes suivante :

pourrait en théorie être notée autrement qu’avec ce symbole :

3

L’essentiel de ce chapitre et du suivant va être consacré à nous persuader que cette même quantité de pommes :

peut être dénotée en écrivant ceci :

11

Une fois que l’on accepte de noter des valeurs numériques de cette façon radicale, il devient possible de faire stocker et traiter des nombres par des circuits électriques qui, à terme, vont constituer des ordinateurs. Pour être fin prêt à faire ce saut conceptuel, il est indispensable d’avoir compris comment fonctionnent les systèmes de numération.

Depuis que la race humaine a commencé à compter, elle utilise ses doigts pour ce faire. La plupart des civilisations ont basé leur système de comptage sur le nombre dix, sauf quelques exceptions qui ont adopté la valeur 5, 20 ou 60, qui restent néanmoins proches de dix. (L’ancien système babylonien à base 60 a survécu dans nos secondes et minutes.) Notre système décimal n’a rien de remarquable hormis sa correspondance avec le nombre de doigts des mains humaines. Si nous avions été dotés de huit ou de douze doigts, nous compterions différemment. Ce n’est pas un hasard que le mot « digital » fasse référence aux doigts comme aux nombres.

Dans ce sens, la base décimale ou base dix est absolument arbitraire. Pourtant, nous considérons nos nombres décimaux comme d’une totale évidence et attribuons des noms dédiés à certains d’entre eux : dix ans forment une décennie et dix siècles forment un millénaire. Un millier de milliers est un million ; mille millions font un milliard. Ce sont des puissances de 10 :

10¹ = 10

10² = 100

10³ = 1 000 (mille)

10⁴ = 10 000

10⁵ = 100 000

10⁶ = 1 000 000 (un million)

10⁷ = 10 000 000

10⁸ = 100 000 000

10⁹ = 1 000 000 000 (un milliard)

Les historiens sont généralement d’accord pour dire que les nombres ont été d’abord inventés pour compter des objets ou des gens et clarifier les transactions commerciales. Lorsque quelqu’un avait choisi de s’occuper de quatre canards, il pouvait noter cette quantité en dessinant quatre canards :

La personne chargée de comptabiliser les canards de chacun avait fini par se demander comment gagner du temps et ne plus dessiner un à un tous les objets. « Comment pourrais-je me contenter de dessiner un seul canard puis indiquer qu’il y en a quatre, par exemple avec des traits ou des rayures ? »

Puis, un jour, une personne s’est trouvée à posséder vingt-sept canards, et le nombre de griffures est devenu illisible :

Quelqu’un a fini par se dire qu’il fallait trouver autre chose, et c’est ainsi qu’est né un système de numération.

Parmi tous les anciens systèmes de numération, seul celui de Rome reste en usage quotidien, sur les cadrans des horloges, sur les monuments et les statues, pour numéroter les chapitres et les pages de livres, et, surtout (ce qui a le don d’en énerver certains), pour l’année de sortie des longs-métrages. Lorsque vous cherchez à savoir de quand date ce fameux film, vous devez être rapide pour déchiffrer MCMLIII avant que la mention ait disparu de l’affichage du générique.

Voici comment se notent nos vingt-sept canards en chiffres romains :

La logique est fort simple : la lettre X représente dix marques et la lettre V en représente cinq.

Les chiffres romains suivants ont survécu :

I V X L C D M

Le I vaut un et provient sans doute d’une griffure ou d’un doigt levé. Le V provient sans doute des cinq doigts d’une main. Deux V tête-bêche forment un X qui vaut dix. Le L vaut cinquante, le C vaut cent, le D vaut cinq cents et le M vaut mille.

Même si de nos jours, nous trouvons que la numération des romaines n’est pas très efficace, elle était appréciée pour les additions et soustractions, au point d’avoir été pratiquée longtemps en Europe pour la comptabilité. Additionner deux nombres écrits en romain consiste à combiner les symboles des deux nombres pour ensuite simplifier le résultat en appliquant quelques règles : cinq I font un V, deux V font un X, cinq X font un L, etc.

En revanche, les multiplications et divisions étaient ardues en notation romaine. C’était aussi le cas d’autres anciens systèmes de numération (comme celui de la Grèce antique). Ces génies grecs ont inventé une géométrie splendide qui reste enseignée quasiment telle quelle de nos jours, mais leur algèbre n’a pas fait leur réputation.

Notre système de numération actuel est dit indo-arabe. Inventé en Inde, il a été apporté aux Européens par les mathématiciens arabes, parmi lesquels le célèbre Muhammed ibn Musa al-Khwarizmi, d’origine persane (dont le nom latinisé a donné le mot « algorithme »). Il avait écrit vers l’an 820 de notre ère un livre sur l’algèbre qui exploitait justement le système de comptage des hindous. Traduit en latin en 1145, le livre a favorisé l’abandon en Europe des chiffres romains au profit du nouveau système indo-arabe.

Le système indo-arabe se distingue de ses prédécesseurs par trois aspects :

Le système de numération indo-arabe est positionnel, ce qui signifie que chaque chiffre possède une valeur différente selon la position qu’il a par rapport aux autres chiffres du nombre. La position a plus d’importance que la valeur intrinsèque ! Les deux nombres 100 et 1 000 000 ne comportent tous deux qu’une fois le chiffre 1, mais on sait que leur valeur n’est vraiment pas la même.

Quasiment tous les systèmes antérieurs prévoyaient un symbole dont le système indo-arabe n’a pas besoin : un symbole dédié au nombre dix.

Inversement, il manque quelque chose de capital à pratiquement tous les anciens systèmes de numération que possède le système indo-arabe : un symbole pour le zéro.

Oui, le zéro. Ce minuscule zéro est sans conteste l’une des inventions capitales de la numération et des mathématiques. Il rend possible la notation par position en permettant de distinguer 25 de 205 et de 250. Le zéro simplifie des opérations qui sont fastidieuses dans les systèmes non positionnels, surtout pour la multiplication et la division.

La structure de la numérotation habituelle des chiffres indo-arabiques se dévoile lorsque nous prononçons un nombre. Prenons 4 825 comme exemple. Nous le prononçons « quatre mille huit cent vingt-cinq ». Ce qui représente :

quatre mille

huit cents

deux dix

cinq unités

Nous pouvons décomposer la valeur ainsi :

4 825 = 4 000 + 800 + 20 + 5

Nous la détaillons en fonction des multiples de 10 :

4 825 = 4 × 1 000 +

8 × 100 +

2 × 10 +

5×1

La même chose peut être reformulée en puissances de 10 :

4 825 = 4 × 10³ +

8 × 10² +

2 × 10¹ +

5 × 10⁰

Rappelons que 10 puissance 0 étant égal à 1, un nombre multiplié par cette valeur est égal à lui-même.

Chaque rang d’un nombre à plusieurs chiffres possède un poids différent. Les sept cases suivantes permettent de décomposer les sept chiffres de n’importe quel nombre entre 0 et 9 999 999 :

Figure 9.1

Note : les Anglo-Saxons utilisent la virgule pour séparer les milliers (1,000), c’est-à-dire les groupes de trois chiffres, tandis que les Français utilisent pour cela une espace (1 000). La virgule sert chez nous à marquer la décimale et est remplacée chez les Anglo-Saxons par un point, comme dans 12,345.67.

Chaque position du nombre correspond à une puissance de 10, et nous n’avons de ce fait plus besoin de symbole spécial pour la quantité dix. Pour exprimer cette valeur, on intronise le 1 à une nouvelle position et on insère un 0 pour occuper le rang des unités.

Un autre superbe avantage de cette technique est qu’elle continue à s’appliquer aux valeurs fractionnaires avec des chiffres derrière la virgule. Décomposons la valeur non entière 42 705,68 :

4 × 10 000 +

2 × 1 000 +

7 × 100 +

0 × 10 +

5×1+

6 ÷ 10 +

8 ÷ 100

Vous remarquez que les deux dernières lignes utilisent l’opérateur de division et pas celui de multiplication. Ces divisions peuvent être reformulées en multiplications fractionnaires :

4 × 10 000 +

2 × 1 000 +

7 × 100 +

0 × 10 +

5×1+

6 × 0,1 +

8 × 0,01

Voici la même valeur reformulée avec des puissances de 10 :

4 × 10⁴ +

2 × 10³ +

7 × 10² +

0 × 10¹ +

5 × 10⁰ +

6 × 10⁻¹ +

8 × 10⁻²

Les exposants décroissent jusqu’à 0 puis deviennent négatifs. Notre système de numération nous est tellement familier que nous ne prenons plus le temps d’admirer l’élégance de son mécanisme.

Sachant que 3 + 4 font 7, on devine que 30 + 40 font 70, 300 + 400 font 700 et 3 000 + 4 000 font 7 000. Là est la beauté du système indo-arabe. Additionner des nombres décimaux de n’importe quelle longueur se résume à décomposer l’opération en étapes simples. Chaque étape est une addition de nombres à un chiffre. Voilà pourquoi on vous a forcé à apprendre les tables d’addition :

Tableau 9.1

+
0
1
2
3
4
5
6
7
8
9

0

0

1

2

3

4

5

6

7

8

9

1

1

2

3

4

5

6

7

8

9

10

2

2

3

4

5

6

7

8

9

10

11

3

3

4

5

6

7

8

9

10

11

12

4

4

5

6

7

8

9

10

11

12

13

5

5

6

7

8

9

10

11

12

13

14

6

6

7

8

9

10

11

12

13

14

15

7

7

8

9

10

11

12

13

14

15

16

8

8

9

10

11

12

13

14

15

16

17

9

9

10

11

12

13

14

15

16

17

18

Vous cherchez les deux nombres à additionner au croisement d’une ligne et d’une colonne, par exemple 4 + 6 = 10.

Pour multiplier deux nombres décimaux, la méthode est un peu plus complexe mais continue à décomposer le problème afin de ne demander que des opérations sur des nombres à un chiffre. Vous vous souvenez sans doute bien de vos tables de multiplication :

Tableau 9.2

×
0
1
2
3
4
5
6
7
8
9

0

0

0

0

0

0

0

0

0

0

0

1

0

1

2

3

4

5

6

7

8

9

2

0

2

4

6

8

10

12

14

16

18

3

0

3

6

9

12

15

18

21

24

27

4

0

4

8

12

16

20

24

28

32

36

5

0

5

10

15

20

25

30

35

40

45

6

0

6

12

18

24

30

36

42

48

54

7

0

7

14

21

28

35

42

49

56

63

8

0

8

16

24

32

40

48

56

64

72

9

0

9

18

27

36

45

54

63

72

81

Le grand intérêt du système de numération positionnel est qu’il fonctionne aussi pour les systèmes de comptage qui ne sont pas fondés sur la dizaine. En effet, notre système de numération en base 10 ne convient pas nécessairement à tout le monde. Par exemple, il n’est pas adapté aux personnages de dessins animés qui n’ont que quatre doigts à chaque main (ou patte) et préféreront donc un système de numération octal, basé sur 8.

Il est intéressant de noter qu’une grande partie de ce que nous savons sur la numération décimale peut être appliquée à un système de numération plus approprié à nos héros de dessins animés.

Chapitre 10  Il y a dix et 10

Le nombre 10 est d’une importance capitale pour les humains. C’est d’abord le nombre de doigts des deux mains et d’orteils des deux pieds, et nous sommes très heureux qu’il ne nous en manque aucun. C’est parce que nous transportons en permanence avec nous cette ribambelle d’objets quasiment identiques que sont nos doigts que nous avons inventé un système de numérotation basé sur cette quantité.

J’ai déjà indiqué dans le chapitre précédent que notre système de numérotation conventionnel correspond à la base 10 et se nomme le système décimal. La numérotation décimale est tellement ancrée en nous qu’il n’est pas simple dans un premier temps d’en imaginer un autre. Lorsque nous voyons le nombre 10, nous considérons automatiquement qu’il désigne ce nombre de canards :

La seule et unique raison pour laquelle il y a une équivalence entre le nombre 10 et la quantité de canards de la figure précédente est qu’il y a équivalence entre le nombre de canards et le nombre de doigts des deux mains. Si les humains avaient un nombre de doigts différent, notre façon de compter serait différente et le nombre 10 signifierait une quantité différente. Le même nombre 10 peut donc désigner ce nombre de canards :

ou bien ce nombre de canards :

ou même aussi peu de canards que ceci :

Une fois que l’on est parvenu à accepter que le nombre 10 désigne deux canards, c’est que l’on commence à être mentalement paré pour jongler avec les interrupteurs, les câbles et les lampes pour symboliser des quantités et comprendre comment il va devenir possible de réaliser des calculs avec des relais et des portes logiques, autrement dit avec des ordinateurs.

Huit par huit : octal

Imaginez que les humains n’aient que quatre doigts à chaque main, comme les personnages de dessins animés. Ils n’auraient jamais créé un système de numérotation à base 10. On aurait au contraire trouvé complètement naturel et inévitable de compter en base 8. Cette base 8 correspond à la numérotation dite octale.

Si nous numérotions en base 8 au lieu de 10, nous n’aurions plus besoin du symbole qui correspond à ce chiffre :

9

Si vous montrez ce symbole à un personnage de dessin animé, il se demandera à quoi cela correspond. Si l’on y réfléchit un peu, nous n’aurions pas non plus besoin du symbole pour le chiffre suivant :

8

En effet, en décimal, nous n’avons pas de symbole dédié à la valeur 10. Dans la base octale, il n’y a donc pas besoin de symbole pour la valeur 8.

En base décimale, nous comptons 0,1, 2, 3, 4, 5, 6, 7, 8, 9 et 10. Dans la numérotation octale, nous comptons 0, 1, 2, 3, 4, 5, 6, 7 puis quoi ? Nous sommes à court de symboles de chiffre. En toute logique, nous devrions noter 10 ; c’est effectivement ce qu’il faut faire. En base octale, le nombre qui suit 7 est 10. Cette valeur 10 ne correspond pas au nombre de doigts d’un être humain, mais à celui du nombre de doigts d’un personnage de dessin animé.

Nous pouvons continuer à compter sur nos orteils de cartoon :

Pour limiter les risques de confusion lorsque vous travaillez avec un système de numérotation autre que décimal, il est conseillé de prononcer « un zéro » pour la valeur 10. De même, dire « un trois » pour 13 et « deux zéro » pour 20. Pour éviter vraiment toute ambiguïté, vous pouvez aussi dire treize-base-8 ou 20-octal.

Une fois les huit doigts et huit orteils utilisés, rien ne nous empêche de continuer à compter en base 8. Il en va de même que pour compter en base 10, sauf que nous passons à la huitaine suivante en n’utilisant jamais les chiffres 8 et 9 :

0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17, 20, 21, 22, 23, 24, 25, 26, 27, 30, 31, 32, 33, 34, 35, 36, 37, 40, 41, 42, 43, 44, 45, 46, 47, 50, 51, 52, 53, 54, 55, 56, 57, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 77, 100...

La dernière valeur de la liste doit se prononcer un-zéro-zéro. C’est le nombre de doigts d’un personnage de cartoon multiplié par lui-même.

Nous sommes conditionnés depuis l’école maternelle à supposer qu’une séquence de chiffres correspond à une quantité précise d’objets du monde réel. Choisir de compter en utilisant une autre base de numérotation, c’est comme entrer dans un nouveau monde. Voici quelques exemples de valeurs exprimées en octal :

Le nombre de nains autour de Blanche Neige est égal à 7 (comme en décimal).

Le nombre de doigts des mains d’un héros de cartoon est de 10.

Beethoven a écrit 11 symphonies.

Les humains ont 12 doigts aux mains.

Il y a 14 mois dans une année.

Si vous vous surprenez à convertir mentalement ces valeurs octales en décimal, toutes nos félicitations car c’est un très bon exercice. Lorsqu’une valeur octale sur deux chiffres commence par un 1, l’équivalent décimal est égal à 8 plus la valeur du second chiffre. Le nombre de mois de l’année est égal à 14 en octal, donc à 8 + 4 en décimal, soit 12. Continuons :

Le nombre redouté par les triskaïdékaphobes est 15.

Il y a 16 jours dans deux semaines.

Une journée compte 30 heures.

L’alphabet latin comporte 32 lettres.

Lorsqu’un nombre octal sur deux chiffres commence par un autre chiffre que 1, la conversion vers la base décimale suppose de multiplier le premier chiffre par 8 avant d’ajouter le second. Le nombre de lettres de l’alphabet est égal à 32 en octal, c’est-à-dire en décimal 3 × 8 + 2 soit 26d1.

Le mois de décembre comporte 37o jours.

Un jeu de cartes complet comprend quatre fois 15o, soit 64o cartes.

Le plateau d’un jeu d’échecs comporte 100o cases (10o × 10o). Bien sûr, en décimal, il comporte 8 × 8 cases, c’est-à-dire 64d cases.

La durée réglementaire d’un match de football est de 132o minutes.

La vitesse maximale sur autoroute en France est de 162o km/h.

Le nombre de caractères dans le code braille à huit points est de 400o.

Cette liste comporte quelques valeurs bien arrondies, telles que 30, 100 et 400. Ce sont ces valeurs qui se terminent par des zéros. Lorsque le nombre se termine par deux zéros en décimal, c’est qu’il est un multiple de 100, soit 10 × 10. En octal, deux zéros en fin de nombre signifient que c’est un multiple en octal qui représente 64 en décimal. En octal, il y a 100 cases sur un plateau d’échecs, donc 64 en décimal. De même, en décimal, le nombre de codes Braille sur huit points est de 256.

Dès les premiers chapitres de ce livre, nous avions vu que les valeurs binaires constituaient des puissances de 2 et que le nombre de codes morse possibles avec quatre points ou traits équivalait à 2 à la puissance 4, soit 16. Avec six points ou traits, nous arrivions à 2 à la puissance 6, soit 64. Avec huit éléments, nous arrivions à 256, soit 2 puissance 8. Une multiplication d’une puissance de 2 par une autre puissance de 2 donne une puissance de 2.

Voici les douze premières puissances de 2 avec leur équivalent en décimal et en octal :

Puissance de 2
Décimal
Octal

2⁰

1

1

2¹

2

2

2²

4

4

2³

8

10

2⁴

16

20

2⁵

32

40

2⁶

64

100

₂⁷

128

200

2⁸

256

400

2⁹

512

1 000

₂¹⁰

1 024

2 000

₂¹¹

2 048

4 000

₂¹²

4 096

10 000

Vous remarquez que la colonne des valeurs octales contient de nombreuses valeurs arrondies, parce que 8 est une puissance de 2. Il y a donc une relation intime entre les bases de numération binaire et octale, bien plus que par rapport à la base décimale.

En ce qui concerne sa structure, le système octal n’est pas différent du système décimal. Seuls les détails changent. En octal, chaque position d’un nombre est un chiffre correspondant à une puissance de 8 :

Figure 10.1

Par exemple, la valeur octale 3 725o peut être décomposée ainsi :

3 725 = 3 000 + 700 + 20 + 5

Cette valeur octale peut être exprimée sous forme de chiffres correspondant à des puissances de 8 :

3 725 = 3 × 1 000 +

7 × 100 +

2 × 10 +

5×1

Voici les puissances de 8 en jeu :

3 725 = 3 × 8³ +

7 × 8² +

2 × 8¹ +

5 × 8⁰

L’équivalent en décimal de cette valeur est égal à :

3 × 512 + 7 × 64 + 2 × 8 + 5

Si vous faites le calcul, vous verrez que la valeur est égale à 2 005 en décimal. C’est ainsi que l’on convertit des valeurs octales en valeurs décimales.

On peut ajouter et multiplier des valeurs octales comme on le fait pour des valeurs décimales. La seule différence est que les tables d’addition et de multiplication ne sont pas les mêmes. Voici la table d’addition en octal :

Tableau 10.1

+
0
1
2
3
4
5
6
7

0

0

1

2

3

4

5

6

7

1

1

2

3

4

5

6

7

10

2

2

3

4

5

6

7

10

11

3

3

4

5

6

7

10

11

12

4

4

5

6

7

10

11

12

13

5

5

6

7

10

11

12

13

14

6

6

7

10

11

12

13

14

15

7

7

10

11

12

13

14

15

16

On voit que 5 + 7 = 14. Les valeurs plus grandes sont calculées comme pour les valeurs décimales. Faisons un petit exercice qui ressemble à une addition décimale. Servez-vous du tableau précédent pour additionner chaque colonne de chiffres :

Dans les trois colonnes, le total est supérieur à la valeur 7, ce qui provoque l’apparition d’une retenue propagée dans la colonne suivante. Au final, on obtient la valeur 1 000o, soit 512d.

En octal, 2 × 2 est égal à 4 mais 3 × 3 ne fait pas 9, mais 11o. Voici le tableau des multiplications en octal :

Tableau 10.2

×
0
1
2
3
4
5
6
7

0

0

0

0

0

0

0

0

0

1

0

1

2

3

4

5

6

7

2

0

2

4

6

10

12

14

16

3

0

3

6

11

14

17

22

25

4

0

4

10

14

20

24

30

34

5

0

5

12

17

24

31

36

43

6

0

6

14

22

30

36

44

52

7

0

7

16

25

34

43

52

61

Le tableau permet d’apprendre notamment que 4 × 6 vaut 30o, soit 24d.

Quatre par quatre : le quartal

La base octale est donc une base de numérotation aussi fonctionnelle que la base décimale, et nous pouvons aller plus loin (plus près à vrai dire). Puisque nous avons retrouvé le système de numérotation des personnages de dessins animés, intéressons-nous à celui qu’utilisent les homards. Un homard n’a pas de doigts, mais deux pinces au bout de ses deux longues pattes de devant, comme tout digne représentant de l’espèce Homarus americanus. Le système de numérotation le plus pratique pour les homards est le système à base 4 :

Voici, par exemple, comment compte un homard en base 4 :

0, 1, 2, 3,

10, 11, 12, 13,

20, 21, 22, 23,

30, 31, 32, 33,

100, 101, 102, 103,

110, 111, 112, 113, 120,

etc.

Je ne vais pas m’étendre sur la base 4, et nous allons rapidement passer à une base encore plus restreinte, mais combien essentielle. Voyons néanmoins à quoi correspond le chiffre de chaque position d’une valeur quaternaire. Bien sûr, ce sont des puissances de 4 :

Figure 10.2

La valeur quaternaire 31 232 peut se décomposer ainsi :

31 232 = 3 × 10 000 +

1 × 1 000 +

2 × 100 +

3 × 10 +

2×1

Chaque chiffre est multiplié par la puissance de 4 correspondante :

31 232 = 3 × 4⁴ +

1 × 4³ +

2 × 4² +

3 × 4¹ +

2 × 4⁰

Si vous faites le calcul en décimal, vous verrez que la valeur quartale 31 232q équivaut à :

3 × 256d + 1 × 64d + 2 × 16d + 3 × 4d + 2

Soit :

768d + 64d + 32d + 12d + 2

C’est-à-dire 878d.

Deux : la base du dauphin

Nous allons maintenant découvrir la base de numérotation ultime. Supposons que nous sommes des dauphins ; nous n’avons que nos deux nageoires latérales pour compter. Il s’agit dans ce cas du système de numérotation en base 2, ou système binaire (« deux par deux » en latin). Nous n’avons que deux chiffres à notre disposition, le 0 et le 1.

Nous avons déjà vu comment utiliser le 0 et le 1 en algèbre booléenne pour distinguer le vrai du faux, le oui du non, le chat acceptable et le chat non qualifié. Nous allons nous servir de ces deux seuls chiffres pour compter.

Bien sûr, envisager de compter avec seulement deux chiffres va demander un minimum de temps d’acclimatation. En effet, le principal problème est que vous arrivez à bout des chiffres très rapidement. Voyons comment un dauphin fait pour compter :

Oui, en base 2, le nombre qui suit 1 s’écrit 10. Vous avez bien lu : lorsque l’on compte en binaire, après 1 vient 10. Aussi étonnant que cela puisse sembler, cela ne devrait pas l’être. Dans toutes les bases de numérotation, lorsque vous n’avez plus de symbole pour la quantité directement supérieure, vous passez au premier groupe de deux chiffres, qui s’écrit 10. Lorsque vous comptez en binaire, vous arrivez à ceci :

0, 1,

10, 11,

100, 101, 110, 111,

1 000, 1 001, 1 010, 1 011, 1 100, 1 101, 1 110, 1 111,

10 000, 10 001…

Les valeurs semblent croître rapidement, mais ce n’est qu’une apparence. En réalité, les nombres binaires accumulent rapidement beaucoup de chiffres :

Un humain possède 1 tête.

Un dauphin possède 10 nageoires pectorales.

Un triangle possède 11 côtés.

Il y a 100 côtés dans un carré.

Une main humaine possède 101 doigts.

Un insecte a 110 pattes.

Il y a 111 jours dans une semaine.

Dans un octuor, les musiciens sont au nombre de 1 000.

Une équipe de football comprend 1 011 joueurs.

La grossesse humaine dure 1 001 mois.

Etc.

Dans un nombre binaire, les positions des chiffres correspondent à des puissances de 2 :

Figure 10.3

Lorsque vous voyez un nombre binaire constitué du chiffre 1 suivi de un ou plusieurs zéros, la valeur est une puissance de 2 et la puissance correspond au nombre de zéros. Voici la table des puissances de 2 qui confirme cette règle :

Puissance de 2
Décimal
Octal
Quartal
Binaire

2⁰

1

1

1

1

2¹

2

2

2

10

2²

4

4

10

100

2³

8

10

20

1 000

2⁴

16

20

100

10 000

2⁵

32

40

200

100 000

2⁶

64

100

1 000

1 000 000

2⁷

128

200

2 000

10 000 000

2⁸

256

400

10 000

100 000 000

2⁹

512

1 000

20 000

1 000 000 000

₂¹⁰

1 024

2 000

100 000

10 000 000 000

₂¹¹

2 048

4 000

200 000

100 000 000 000

₂¹²

4 096

10 000

1 000 000

100 0000 000000

Partons de la valeur binaire 1011 0101 1010. Nous pouvons la développer ainsi :

1011 0101 1010 = 1 × 1 000 0000 0000 +

0 × 100 0000 0000 +

1 × 10 0000 0000 +

1 × 1 0000 0000 +

0 × 1 000 0000 +

1 × 100 0000 +

0 × 10 0000 +

1 × 1 0000 +

1 × 1 000 +

0 × 100 +

1 × 10 +

0×1

Reformulons la valeur sous forme de puissances de 2 :

1011 0101 1010 = 1 × 2¹¹ +

0 × 2¹⁰ +

1 × 2⁹ +

1 × 2⁸ +

0 × 2⁷ +

1 × 2⁶ +

0 × 2⁵ +

1 × 2⁴ +

1 × 2³ +

0 × 2² +

1 × 2¹ +

0 × 2⁰

Si on additionne les valeurs correspondant aux puissances, on obtient en décimal 2 048 + 512 + 256 + 64 + 16 + 8 + 2, ce qui donne 2906d.

Pour convertir une valeur binaire en valeur décimale de façon plus confortable, on peut se servir d’un gabarit :

Avec ce gabarit, on peut convertir une valeur comportant jusqu’à huit chiffres binaires, mais vous pouvez l’agrandir au besoin. Il suffit d’écrire les chiffres binaires dans les huit cases du haut, de réaliser les huit multiplications et d’inscrire le résultat dans les huit cases du bas. Vous additionnez ensuite les valeurs de ces huit cases pour obtenir le résultat. Voici, par exemple2, comment trouver l’équivalent en décimal de la valeur binaire 1001 0101 0110 :

La conversion inverse, de décimal vers binaire, est moins facile, mais vous pouvez vous servir d’un gabarit qui permet de convertir une valeur jusqu’à 255d. Voici les étapes successives de la conversion :

La conversion est moins rapide dans ce sens. Vous inscrivez d’abord la valeur décimale entière (elle doit être inférieure ou égale à 255) dans la case en haut à gauche :

Vous divisez alors mentalement la valeur par la puissance de 2 du rang concerné, soit 128 ici. Si la division est possible, notez le quotient et le reste. Dans l’exemple, 150 / 128 donne 1 avec un reste de 22. Inscrivez le quotient dans la première case du bas (qui ne peut contenir que 1 ou 0) et le reste dans la prochaine case du haut :

Il suffit ensuite de diviser le reste par le diviseur suivant, ici 22 / 64. Comme cela ne fonctionne pas, le quotient est directement égal à 0 et le même reste 22 est propagé. Vous inscrivez 0 dans la deuxième case du bas puis copiez le reste dans la prochaine case du haut :

Vous continuez ainsi, le quotient ne pouvant être que 0 ou 1. Lorsque vous avez terminé, vous obtenez une suite de chiffres binaires dans la rangée du bas :

L’équivalent binaire de la valeur décimale 150 est donc 100 1010.

Les conversions entre bases décimale et binaire sont donc laborieuses. Vous serez heureux d’apprendre qu’aussi bien Windows que macOS disposent d’une application de calculatrice avec un mode Programmeur qui assure ce genre de conversion aisément.

Dans les débuts de l’informatique, les valeurs binaires n’étaient pas incontournables. Certaines des premières machines à calculer étaient conçues en sorte d’utiliser des valeurs décimales. Par exemple, la machine analytique conçue au début des années 1830 par le mathématicien anglais Charles Babbage (1791-1871) gérait des nombres décimaux implantés sur des roues dentées. (Il n’avait, hélas, pas réussi à construire sa machine.) Dans les premières machines fonctionnelles, en particulier le Harvard Mark I (mis en service en 1944) et l’ENIAC de 1946, la base numérique utilisée était toujours décimale. Même certains ordinateurs IBM des années 1960 se fondaient sur une architecture à nombres décimaux.

C’est néanmoins l’adoption de la base binaire qui a lancé véritablement la révolution numérique. La simplicité du binaire devient évidente lorsque l’on étudie les opérations fondamentales que sont l’addition et la multiplication. Nous entrons ainsi dans la partie du livre que vous allez certainement apprécier plus que toute autre. Imaginez à quelle vitesse vous auriez été capable de maîtriser les additions si vous n’aviez que cette table à mémoriser :

+

0

1

0

0

1

1

1

10

Servons-nous de cette table pour additionner deux valeurs binaires :

Nous commençons toujours par la colonne la plus à droite (le bit de poids faible) : 1 + 0 = 1. Dans la deuxième colonne, 0 + 1 =1 ; dans la troisième, 1 + 1 = 0 mais avec une retenue de 1. Dans la quatrième colonne, la retenue + 0 + 0 donne 1. Dans la cinquième colonne, 0 + 1 donne 1. Dans la sixième, 1 + 1 donne 0 avec une retenue et, dans la septième colonne, la retenue plus 1 plus 0 donne 10.

La table de multiplication est encore plus simple parce qu’elle découle directement des deux règles de base de la multiplication : multiplier n’importe quoi par 0 donne 0 et multiplier n’importe quoi par 1n’a aucun effet sur la valeur.

×

0

1

0

0

0

1

0

1

Essayons de multiplier la valeur 13d (donc décimale) qui s’écrit 1101b (en binaire) par la valeur 11d, soit 1011b. Nous n’allons pas passer par toutes les étapes, mais le processus reste le même que pour la multiplication décimale :

Le résultat en décimal est égal à 143.

Lorsque l’on note des nombres binaires, il est d’usage d’ajouter des zéros non significatifs à gauche pour obtenir un nombre de chiffres réguliers, par exemple 0011 pour 11. La valeur ne change évidemment pas. Voici, par exemple, les seize premières valeurs binaires avec leur équivalent en décimal :

Binaire
Décimal

0000

0

0001

1

0010

2

0011

3

0100

4

0101

5

0110

6

0111

7

1000

8

1001

9

1010

10

1011

11

1100

12

1101

13

1110

14

1111

15

Prenons quelques instants pour scruter cette liste à la recherche de motifs remarquables. Nous constatons des alternances régulières dans l’apparition des chiffres :

Le chiffre le plus à droite alterne entre 0 et 1.

Le chiffre suivant alterne entre deux 0 et deux 1.

Le chiffre suivant alterne entre quatre 0 et quatre 1.

Le chiffre suivant alterne de huit en huit.

L’évolution des valeurs semble très régulière, n’est-ce pas ? Effectivement, la règle est tellement stricte que c’est elle qui rend possible la conception de circuits électroniques capables de « compter », de générer des séquences de valeurs binaires croissantes de façon automatique (ce que nous verrons dans le Chapitre 17).

Nous pouvons facilement écrire les seize valeurs binaires suivantes de 16 à 31 par simple répétition des seize premières en ajoutant un chiffre 1 à gauche :

Binaire
Décimal

10000

16

10001

17

10010

18

10011

19

10100

20

10101

21

10110

22

10111

23

11000

24

11001

25

11010

26

11011

27

11100

28

11101

29

11110

30

11111

31

Une autre façon de considérer l’évolution des valeurs est la suivante : en binaire, le chiffre le plus à droite (que l’on nomme le chiffre le moins significatif ou de poids faible) alterne entre 0 et 1. Chaque fois qu’il repasse de 1 à 0, il déclenche le changement d’état du chiffre qui est à sa gauche, de 0 à 1 ou de 1 à 0. De façon globale, lorsqu’un chiffre binaire passe de 1 à 0, le chiffre situé à sa gauche change d’état.

Une valeur binaire peut rapidement comporter un nombre de chiffres énorme. Voici, par exemple, comment s’écrit la valeur décimale 12 millions en binaire :

1011 0111 0001 1011 0000 0000

Une technique pour rendre les valeurs binaires plus faciles à lire et à noter consiste à utiliser la base 8 (octale). On profite du fait que chaque groupe de trois chiffres binaires correspond à une valeur sur un chiffre en octal :

Binaire
Octal

000

0

001

1

010

2

011

3

100

4

101

5

110

6

111

7

Partons de cet énorme nombre binaire valant 12 millions et distribuons-le en paquets de trois chiffres en octal, en commençant par la droite :

101 101 110 001 101 100 000 000

Nous avions dit que chaque groupe de trois chiffres binaires correspondait à un chiffre octal :

La valeur décimale 12 millions s’écrit 5561 5400 en octal. Nous verrons une autre base de numérotation encore plus compacte dans le Chapitre 12.

En réduisant le nombre de symboles de numérotation aux seuls deux chiffres 0 et 1, nous avons atteint une limite. Il n’y a que les entailles sur les os qui seraient plus élémentaires. L’adoption de la base binaire est ce qui permet de créer une passerelle fonctionnelle entre arithmétique et électricité. Un interrupteur, un fil conducteur ou une lampe peut représenter la différence entre les deux chiffres binaires selon qu’une tension est présente ou pas. Ces valeurs numériques élémentaires peuvent être manipulées en utilisant des portes logiques. Voilà pourquoi les nombres binaires sont les briques fondamentales des ordinateurs.

Nous venons de découvrir un petit tableau montrant la correspondance entre des valeurs binaires sur trois chiffres et des valeurs octales. Il est possible de construire un circuit électronique capable d’effectuer la conversion entre octal et binaire en utilisant des interrupteurs et des portes logiques. Des lampes affichent les résultats :

Figure 10.4

De prime abord, ce schéma semble incompréhensible. Il fait penser à une sorte de plan des autoroutes d’une mégalopole inconnue dont les panneaux indicateurs sont effacés. En réalité, le circuit est assez simple : les points ronds désignent des fils qui sont en contact. Lorsque deux fils se croisent sans un tel point de soudure, c’est qu’ils ne sont pas interconnectés.

Dans le haut du circuit, nous trouvons trois interrupteurs d’entrée qui servent à coder une valeur binaire sur trois chiffres. Un interrupteur fermé correspond à la valeur 1. Dans l’exemple, la valeur binaire définie est 110, soit 4d (ou 4o). Dans le bas du circuit, nous trouvons huit lampes numérotées de 0 à 7. Une seule lampe peut s’allumer à la fois.

Le fonctionnement est sans doute plus facile à comprendre en lisant le circuit en partant de la sortie et en remontant. Chacune des huit lampes est contrôlée par une porte ET (AND) à trois entrées. La sortie de cette porte ne vaut 1 que si les trois entrées valent 1. Chacune des trois entrées est connectée à l’un des trois interrupteurs, soit directement, soit en passant par un inverseur. Rappelons que si un interrupteur est ouvert, le signal en sortie d’inverseur est à l’état 1.

Dans l’exemple, les trois interrupteurs sont en configuration Fermé, Ouvert, Ouvert, ce qui correspond bien à la valeur binaire 100. Nous suivons les lignes qui sont sous tension (en rouge). On voit que l’interrupteur de gauche est relié directement à l’une des trois entrées de la porte du chiffre 4. Pour les deux autres interrupteurs, l’entrée d’abord passe par un circuit inverseur (NON). C’est pourquoi la porte logique du chiffre 4 est la seule dont les trois entrées sont à 1, ce qui permet de faire briller la lampe de la sortie 4.

Si vous suivez les lignes des sept autres portes, vous constatez que chacune bénéficie d’un câblage différent pour ses entrées.

Ce petit montage correspond au composant nommé décodeur 3-vers-8. Son nom rappelle qu’il utilise en entrée une valeur binaire sur trois chiffres qui est un code pouvant représenter une possibilité parmi huit.

L’opération de conversion en sens inverse correspond au circuit nommé encodeur 8-vers-3. Pour présenter le principe de ce circuit, nous prévoyons un interrupteur rotatif permettant de sélectionner une valeur parmi huit. On peut tout à fait créer une version minimaliste de ce genre de sélecteur avec des clous sur une planchette et une pièce de métal mobile :

Figure 10.5

Chacun des trois chiffres rendus lisibles par les lampes en bas est contrôlé par une porte OU à quatre entrées. La sortie d’une telle porte est à 1 à partir du moment où une de ses quatre entrées est à l’état 1. Dans l’exemple, nous avons sélectionné la valeur octale 6o. Dans ce cas, les première et seconde portes OU ont au moins une entrée à 1, ce qui force leur sortie à 1. On obtient au final la valeur binaire 110. Vous remarquez que la position 0 du sélecteur en haut n’est connectée à rien. En effet, la valeur octale 0o correspond à la valeur binaire 000b pour laquelle aucune des trois lampes ne doit être allumée.

C’est vers 1947 qu’un mathématicien américain nommé John Wilder Tukey (1915-2000) avait prévu que l’expression binary digit (« chiffre binaire ») allait prendre de plus en plus d’importance au fur et à mesure de la démocratisation des ordinateurs. Il avait alors décidé de chercher un mot plus bref. Après avoir envisagé les termes bigit et binit, il avait fini par proposer ce mot bref, simple et élégant que nous connaissons tous : le bit.

1 N.d.T. : dans toute la suite du livre, je prends la décision d’ajouter, lorsque c’est nécessaire, une lettre suffixe pour la base de la valeur numérique. Ainsi, 39d signifie 39 en décimal et 47o signifie 47 en octal. Nous rencontrerons aussi les suffixes b et h.

2 N.d.T. : pour plus de confort, nous choisissons dans la suite de séparer les chiffres des valeurs binaires par paquets de quatre avec un espace d’effet visuel (non significatif) entre chaque groupe de quatre. En de rares moments, nous regrouperons les chiffres binaires par trois, uniquement lorsque nous parlerons de base octale.

Chapitre 11  Bits après bits

Voici une histoire qui remonte au moins aux années 1950. Il était une fois un homme sortant de prison et s’apprêtant à rentrer chez lui après des années d’absence. N’ayant pas réussi à savoir s’il allait être bienvenu ou pas, il avait demandé de répondre en accrochant un bout de tissu à la branche d’un arbre. Dans une variante de l’histoire, l’homme rentre chez lui en train et cherche un ruban blanc accroché un pommier. Dans une autre variante, il rentre en autocar retrouver sa femme et scrute le vieux chêne devant chez lui à la recherche d’un mouchoir jaune. Dans les deux variantes, l’homme arrive devant chez lui pour découvrir des centaines de petits morceaux de tissu, ce qui ne lui laisse aucun doute sur l’accueil qui lui sera réservé.

L’histoire a fait l’objet en 1973 d’une chanson à succès1, Tie a Yellow Ribbon Round the Ole Oak Tree. Il est devenu d’usage aux États-Unis d’accrocher un ruban jaune pour montrer son attachement à un membre de la famille ou une personne aimée absente car mobilisée à la guerre.

L’homme qui demandait d’accrocher un ruban jaune ne désirait pas un avis circonstancié ni une discussion élaborée. Il n’avait que faire des si et des hésitations. Quelle que soit l’intensité des sentiments et des souvenirs en jeu, l’homme ne désirait qu’une seule réponse : oui ou non. S’il voyait un ruban jaune, cela devait signifier « Oui, même si tu as fait une énorme bêtise et perdu trois ans de ta vie en prison, je te veux toujours avec moi sous mon toit ». En l’absence du ruban jaune, cela devait simplement signifier « N’imagine même pas t’arrêter devant chez moi ».

Les deux branches de l’alternative sont mutuellement exclusives. En remplacement du ruban jaune (mais cela serait moins facile à placer dans les paroles d’une chanson), on peut envisager un panneau de circulation routière dans le jardin avec une mention « sens unique » ou bien « sens interdit ». Ou un panneau « Ouvert » ou « Fermé » accroché à la porte.

Ou alors une lampe posée sur le rebord de la fenêtre, allumée ou éteinte.

Lorsqu’il ne s’agit que de dire oui ou non, il y a bien des solutions. Inutile de chercher à faire une phrase complète, même un mot est de trop, même une seule lettre. Le minimum dont on a besoin est 1 bit, c’est-à-dire un élément d’information qui vaut soit 0, soit 1.

Nous avons vu au cours des deux chapitres précédents que le système de numération décimale auquel nous sommes habitués n’a rien de particulier. Nous avons choisi la base 10 parce que c’est le nombre de doigts dont nous disposons. Rien ne nous empêche de choisir une base 8, comme l’ont fait les personnages de dessins animés, une base 4 si nous étions des homards, voire une base 2 comme les dauphins.

Alors que le système décimal n’a rien de plus que les autres bases possibles, le système binaire est particulier parce que c’est le plus simple des systèmes de numération possibles. Il ne comprend que deux chiffres, le 0 et le 1. Si nous voulions encore simplifier les choses, nous supprimerions le 1. Il ne resterait que le 0 qui ne permet pas de véhiculer une différence à lui seul.

Le mot « bit », contraction du terme anglais binary digit, est véritablement l’une des plus belles trouvailles de la terminologie informatique. Il possède son sens habituel en anglais qui est celui d’une petite portion, d’une bouchée, ce qui convient parfaitement parce qu’un chiffre binaire est vraiment une quantité très ténue.

L’introduction d’un nouveau mot le dote parfois d’une signification supplémentaire. C’est tout à fait le cas ici. Le mot « bit » désigne le chiffre binaire avec lequel peuvent compter les dauphins, mais il est également devenu la brique de base des systèmes de traitement d’information.

Cette déclaration quelque peu solennelle ne doit pas laisser penser que seuls des bits peuvent transmettre de l’information. Les lettres de l’alphabet, le code morse, le code braille, les chiffres de la base décimale transmettent aussi de l’information. La particularité du bit est de véhiculer un très faible volume d’information, le volume d’information le plus petit que l’on puisse imaginer, même si l’impact de cette information est énorme, comme dans le cas du ruban jaune. On ne peut pas imaginer une quantité d’informations inférieure à celle que transporte 1 bit. Mais ce plancher de quantité d’informations permet d’envisager toutes sortes de constructions regroupant plusieurs bits pour symboliser et transmettre des informations complexes.

Le grand poète américain Henry Wadsworth Longfellow avait écrit un poème à propos d’un certain Paul Revere qui avait décidé de prévenir les colonies américaines de la façon dont les Britanniques s’apprêtaient à les envahir. Son poème n’était peut-être pas absolument fidèle en termes historiques, mais il a constitué une belle illustration de l’efficacité des bits pour transmettre des informations stratégiques.

Il dit à son ami : “Si les Britanniques

Attaquent depuis la ville par terre ou par mer en pleine nuit,

Pendez une lampe sous l’arche du beffroi

De la tour de l’église nord en guise de signal, —

Une si c’est par terre, et deux si c’est par mer.

L’ami de Paul Revere possédait deux lanternes. Si les Britanniques s’apprêtaient à attaquer en passant par les terres, il n’allumerait qu’une lanterne dans la tour de l’église. S’ils attaquaient par la mer, il allumerait les deux.

Longfellow n’avait pas passé toutes les possibilités en revue, notamment le cas où les Britanniques n’attaqueraient pas encore pour l’instant. Il avait supposé que dans ce cas, il n’y aurait aucune lanterne allumée dans la tour.

Supposons que les deux lanternes ont été installées de façon définitive dans la tour. En temps normal, elles sont éteintes toutes deux :

Cela signifie que les Britanniques ne s’apprêtent pas à attaquer. Si l’une des deux lanternes est allumée, c’est que les Britanniques vont attaquer par les terres.

ou

Les deux lanternes allumées préviennent qu’ils vont attaquer par la mer :

Chaque lanterne correspond à 1 bit, qui peut prendre la valeur 0 ou 1. Comme le montre l’histoire du ruban jaune, un seul bit suffit à transmettre un choix entre deux possibilités. Si les troupes américaines avaient seulement besoin de savoir si les Britanniques allaient attaquer ou pas, quelle que soit la voie qu’ils emprunteraient, une lanterne aurait suffi, allumée en cas d’attaque et éteinte pour informer que la soirée serait paisible.

Il faut une seconde lanterne lorsqu’il s’agit de communiquer une possibilité parmi trois. En fait, dès qu’il y a deux lanternes, les deux bits correspondants permettent de communiquer une possibilité parmi quatre :

00 = Les Britanniques n’attaquent pas cette nuit.

01 = Ils vont attaquer par les terres.

10 = Ils vont attaquer par la mer.

En ne consommant que trois des quatre possibilités théoriques, Paul Revere a utilisé une technique assez évoluée. En effet, en théorie de la communication, la non-utilisation directe d’un cas possible sert à la redondance pour limiter l’impact des bruits de communication. Par « bruit », on désigne tout ce qui peut mettre en péril la qualité de la communication. Tout le monde connaît les problèmes de réception sur son téléphone portable. Dans une communication vocale, un certain niveau de dégradation du signal reste acceptable parce que les langages humains offrent énormément de redondance. Il n’est pas nécessaire d’avoir entendu chacune des syllabes de chaque mot pour comprendre une phrase.

Dans le cas des deux lanternes de l’église, le bruit peut être une obscurité insuffisante ou une distance trop importante depuis laquelle Paul observe la tour. De mauvaises conditions de visibilité pourraient empêcher Paul de bien distinguer la lampe droite de la gauche. Rappelons le passage crucial du poème de Longfellow :

Et voici qu’au sommet du clocher, il aperçoit

Une pâle lueur, puis un rai de lumière !

Il s’élance en selle, tourne la bride,

Puis s’attarde et observe, jusqu’à distinguer

Une seconde lampe qui brûle dans le beffroi !

Le texte laisse clairement deviner que Paul Revere n’était vraiment pas en mesure de voir à coup sûr si c’était la lampe de gauche ou celle de droite qui était allumée.

Le concept que met en lumière cette histoire est que toute information constitue un choix parmi au moins deux possibilités. Lorsque l’on parle avec quelqu’un, nous choisissons chaque terme parmi tous les mots du dictionnaire. En théorie, il serait possible de numéroter les mots du dictionnaire de 1 à 350 000 (par exemple) puis de dialoguer en s’échangeant des nombres. (Bien sûr, il faut que les deux participants disposent d’un dictionnaire dans lequel les mots sont numérotés exactement de la même façon, et se montrer très patients pour dialoguer.)

Une lecture en sens inverse du même concept permet de déclarer que toute information peut être réduite à un choix entre deux possibilités, et donc exprimée par des bits. Un grand nombre de modes de communication entre humains ne permettent pas de choix binaire, et sont néanmoins essentiels à nos existences. C’est pour cette raison que les gens n’ont pas tendance à former de relations sentimentales avec des ordinateurs (espérons-le, tout du moins). Ce que vous ne pouvez pas exprimer par des mots, des dessins ou des sons, vous ne pourrez pas l’encoder sous forme d’informations binaires, et vous ne le voudrez d’ailleurs pas.

À la fin du XXe siècle, les deux critiques de cinéma américains Gene Siskel et Robert Ebert ont animé pendant plus de dix ans une émission de télévision à propos des sorties de films, intitulée At the Movies. Après avoir déroulé leur argumentaire pour ou contre chaque film, ils émettaient un verdict final se limitant à un pouce levé ou baissé.

Les deux pouces considérés comme des bits d’information permettent de symboliser quatre possibilités. La valeur 0 pour un pouce baissé et la valeur 1 s’il est levé :

00 = Tous deux ont détesté.

01 = Siskel a détesté ; Ebert a adoré.

10 = Siskel a adoré ; Ebert a détesté.

11 = Tous deux ont adoré.

Le premier bit est le vote de Siskel et le second, celui de Ebert.

Si, à l’époque où était diffusée cette émission, un de vos amis vous demandait quel avait été le verdict de Siskel et Ebert à propos du nouveau long-métrage Rencontres impolies, vous pouviez répondre que Siskel avait donné un pouce levé et Ebert un pouce baissé, ou bien que Siskel avait adoré et Ebert détesté. Mais vous pouviez également vous contenter de dire « zéro-un » ou, en base quaternaire, « deux ». Il suffisait à votre copain de savoir quel était le bit de Siskel et lequel était celui de Ebert, et qu’un bit à 1 signifiait « pouce levé ». Il fallait néanmoins que votre ami et vous partagiez des conventions.

Rien n’empêche de décider que le bit à 1 signifie « pouce baissé » et que le bit à 0 signifie « pouce levé », mais cela semble contre-intuitif. On considère naturellement que l’état 1 désigne quelque chose de positif ou de vrai. Au contraire d’un bit à 0. Ce n’est pourtant qu’une affaire de convention. La seule contrainte est que toutes les personnes qui partagent l’information doivent savoir à quoi correspondent les deux états.

En effet, la signification d’un ou de plusieurs bits dépend toujours du contexte. Ce que signifie la présence d’un ruban jaune autour d’un chêne dans un jardin en particulier n’est connu que par la personne qui l’a posée et celle qui doit voir le ruban en passant devant. Il suffit de changer la couleur, l’arbre ou le jour et le tissu n’est qu’un bout de chiffon sans signification. De même, pour que les gestes de vote de Siskel et Ebert prennent sens, il faut au moins savoir quel est le film concerné.

Si vous aviez décidé pendant l’émission At the Movies d’ajouter votre vote à celui de Siskel et Ebert, il aurait suffi d’ajouter 1 bit aux deux premiers. Avec 3 bits, le nombre de possibilités monte à huit :

000 = Siskel a détesté ; Ebert a détesté ; j’ai détesté.

001 = Siskel a détesté ; Ebert a détesté ; j’ai adoré.

010 = Siskel a détesté ; Ebert a adoré ; j’ai détesté.

011 = Siskel a détesté ; Ebert a adoré ; j’ai adoré.

100 = Siskel a adoré ; Ebert a détesté ; j’ai détesté.

101 = Siskel a adoré ; Ebert a détesté ; j’ai adoré.

110 = Siskel a adoré ; Ebert a adoré ; j’ai détesté.

111 = Siskel a adoré ; Ebert a adoré ; j’ai adoré.

L’utilisation de bits pour incarner une information permet de s’assurer que l’on tient compte de toutes les possibilités. Dans l’exemple, nous savons qu’il ne peut exister que huit situations. Avec 3 bits, on peut compter de 0 à 7. Il n’y a pas d’autres valeurs binaires sur trois chiffres. Nous avions vu à la fin du chapitre précédent que l’on pouvait écrire toute valeur binaire constituée de trois chiffres sous la forme d’une valeur octale avec les chiffres de 0 à 7.

Lorsque l’on parle de bits, on précise généralement le nombre de bits concerné. Plus il y en a, plus vaste est le champ de possibilités de la communication.

C’est déjà le cas avec les valeurs décimales, bien sûr. Prenons en guise d’exemple le système de numérotation téléphonique par région aux États-Unis. Le code de zone utilise trois chiffres décimaux. On dispose ainsi de mille codes de 000 à 999. Dans le code de zone 212, combien de numéros de téléphone sur sept chiffres sont possibles ? Cela correspond à 10⁷, soit 10 millions. Dans cette même zone 212, combien de numéros peut-on trouver avec le préfixe 260 ? La réponse est 10⁴, soit 10 000.

Dans la base binaire, le nombre de possibilités est toujours égal à 2 à la puissance équivalente au nombre de bits :

Nombre de bits
Nombre de codes

1

2¹ = 2

2

2² = 4

3

2³ = 8

4

2⁴ = 16

5

2⁵ = 32

6

2⁶ = 64

7

2⁷ = 128

8

2⁸ = 256

9

2⁹ = 512

10

2¹⁰ = 1 024

Chaque chiffre binaire supplémentaire double le nombre de codes possibles.

Lorsque vous savez de combien de codes différents vous avez besoin, il faut déterminer le nombre de bits nécessaire. Il s’agit de travailler en sens inverse, en partant du résultat pour trouver la largeur en nombre de bits.

L’astuce consiste à utiliser les logarithmes en base 2. Je rappelle qu’un logarithme est l’inverse d’une puissance. Lorsque 2 à la puissance 7 vaut 128, c’est que le logarithme de 128 en base 2 est égal à 7. En langage mathématique, l’équation suivante

2⁷ = 128

équivaut à celle-ci :

log₂128 = 7

Lorsque nous savons que le logarithme en base 2 de 128 est 7 et que celui de 256 est 8, quel peut être le logarithme d’une valeur entre 128 et 256, par exemple la valeur décimale 200 ? Cette valeur est égale à 7,64, mais ce détail ne nous intéresse pas. Il nous suffit de savoir que pour coder 200 situations différentes avec des bits, il nous faut 8 bits. Il en va comme avec Paul Revere qui avait besoin de deux lanternes pour signaler une possibilité parmi trois. En toute rigueur mathématique, le nombre de bits qu’il faut pour trois possibilités est le logarithme de 3 en base 2, c’est-à-dire 1,6, mais en pratique, on ne coupe pas les bits en deux, et il nous faut donc 2 bits.

En général, les bits sont cachés tout au fond de nos appareils électroniques. Nous ne pouvons pas directement les voir dans nos ordinateurs, ni les voir circuler dans les câbles de nos réseaux ou dans les ondes électromagnétiques de nos routeurs Wi-Fi et émetteurs de téléphonie. Mais dans certains cas, les bits deviennent clairement visibles.

Des bits ont été clairement visibles le 18 février 2021, au moment où le Rover Perseverance atterrissait sur la planète Mars. Sur une photographie de l’opération, on distinguait le parachute qui avait été cousu à partir de 320 bandes de tissus orange et blancs organisées en quatre cercles concentriques :

Les passionnés n’ont pas mis longtemps à discuter sur Twitter pour finir par décoder le motif caché. La lecture suppose de séparer les bandes de tissus en plusieurs groupes de sept, chaque groupe contenant des bandes orange et des bandes blanches. Chaque groupe de sept est séparé du suivant par trois bandes blanches. Les zones qui ne contiennent que des bandes orange sont ignorées. La figure suivante met en exergue les regroupements au moyen de traits épais :

Chaque groupe de 7 bits est une valeur binaire, une bande blanche correspondant à l’état 0 et une bande orange à l’état 1. Le premier groupe se trouve juste au-dessus du cercle central. Les sept premières bandes donnent la valeur 000 0100, soit 4 en décimal et la quatrième lettre de l’alphabet, la lettre D. Le groupe suivant vaut 000 0001, soit 1 qui correspond à A. Le troisième groupe vaut 001 0010, soit 18 (lettre R), et le dernier groupe qui nous intéresse au centre vaut 000 0101, soit 5, la lettre E. Le mot reconstitué se lit DARE (« osez »).

On passe ensuite au cercle suivant. Le premier groupe se lit 000 1101, soit 13 en décimal qui est la lettre M. En poursuivant ainsi, on aboutit à trois mots : DARE MIGHTY THINGS. Cette célèbre exclamation (« osez viser haut ») de Teddy Roosevelt est devenue le slogan officieux du Jet Propulsion Laboratory de la Nasa.

Le cercle extérieur contient quelques valeurs numériques qui fournissent les coordonnées en latitude et en longitude du laboratoire JPL : 34o11’58 » N et 118o10’31»W. Ce système de codage simple ne permet pas de savoir si le prochain élément est une lettre ou un chiffre. Les deux valeurs 11 et 10 dans les coordonnées pourraient être interprétées comme les lettres J et K ; seul le contexte permet d’affirmer que ce sont des chiffres.

Codes-barres

La plus répandue des occasions de contact direct avec des valeurs binaires est le fameux code à barres UPC que l’on trouve sur tous les produits du commerce de nos jours. UPC (Universal Product Code) est un standard de code-barres. Si vous avez la version imprimée de ce livre, vous trouverez sur la quatrième de couverture un autre code-barres qui encode le numéro international du livre ISBN (International Standard Book Number).

Lors de son lancement en 1974, le code-barres UPC avait suscité certaines craintes, alors que ce n’est qu’un petit dispositif innocent qui n’a d’autre but que de permettre l’automatisation du passage en caisse et de la réalisation des inventaires. UPC répond raisonnablement aux attentes. Il était impossible avant son invention d’imprimer les tickets de caisse avec le libellé des articles alors que c’est devenu la norme de nos jours.

Ce qui nous intéresse dans le code UPC est qu’il s’agit d’un code binaire, même si cela ne saute pas aux yeux. Essayons de décoder un code-barres UPC pour en comprendre le fonctionnement.

Dans son format le plus répandu, le code UPC est constitué de 30 traits verticaux de largeur variable, séparés par des espaces de largeur variable aussi et accompagné de quelques chiffres lisibles en bas. Voici, par exemple, le code-barres UPC de la boîte de soupe de nouilles au poulet Campbell de 305 grammes :

Je remarque au passage que ce même code-barres était présent dans la première édition américaine du livre. Le code n’a donc pas changé en plus de vingt ans !

Lorsque l’on observe un code-barres UPC, on est tenté de séparer les barres fines des barres épaisses, et les espaces minces des espaces larges. Il est vrai que les barres du code peuvent avoir quatre largeurs différentes, qui sont deux, trois et quatre fois plus larges que la barre de base. De même, les espaces sont deux, trois ou quatre fois plus larges que l’espace de base.

Mais un code-barres UPC peut être interprété sous la forme d’une suite de bits. Il faut savoir que le scanner qui lit un code-barres ne cherche pas à voir toute la surface rectangulaire et ne tente même pas de lire les chiffres imprimés en clair en bas (cela supposerait un logiciel de reconnaissance optique de caractères OCR qui demande des traitements plus lourds). Ce que voit le scanner est un simple bandeau échantillonné dans le corps du code-barres. Si le code-barres possède une certaine hauteur, c’est pour simplifier le travail de la personne qui manipule la douchette du scanner en sorte qu’elle puisse viser plus facilement dans les limites du code. Voici la tranche que voit le scanner et qui lui suffit pour interpréter la valeur :

Cela ressemble à du code morse. D’ailleurs, les premières propositions de code-barres étaient inspirées en partie de ce dernier.

Lors de l’analyse de ce code, la lecture se fait de gauche à droite. La première zone noire est considérée comme valant 1, et la première zone vide est considérée comme valant 0. Les espaces et les barres suivants sont ensuite lus en tant que groupes de 1 à 4 bits en fonction de la largeur de l’espace (barre blanche) ou de la barre (barre noire). La lecture du code-barres d’exemple et son interprétation en tant que séquence de bits sont très simples :

La totalité d’un code-barres UPC constitue une série de 95 bits. Dans notre exemple, les bits se distribuent en plusieurs groupes ainsi :

Les trois premiers bits sont toujours égaux à 101, ce qui constitue le motif de garde gauche permettant à l’analyseur de se caler en découvrant la largeur des barres et des espaces correspondant à un bit-unité. Cette évaluation à la volée évite de devoir utiliser systématiquement la même taille de code-barres quel que soit le support.

Le motif de zone de garde gauche est suivi de six groupes de 7 bits. Nous allons voir un peu plus loin comment chaque groupe correspond à un chiffre décimal entre 0 et 9.

Nous arrivons ensuite à un motif de garde central sur 5 bits qui vaut toujours 01010 et constitue un système de protection contre les erreurs internes. Si l’analyse ne trouve pas ce motif central, au moment où il est attendu, la lecture du code UPC est considérée comme invalide. Le motif central constitue une des mesures de sécurité pour se protéger en cas de code-barres qui aurait été falsifié ou endommagé.

Le motif central est suivi d’un second bloc de six groupes de 7 bits, lui-même suivi d’un motif de zone de garde droit qui vaut toujours 101 comme celui de gauche. Ce motif de garde droit permet de lire le code-barres UPC en sens inverse, de droite à gauche.

La totalité du code UPC code donc douze chiffres décimaux. Six de chaque côté, avec 7 bits pour chaque chiffre. Le tableau suivant permet de décoder les bits du côté gauche :

Codes du côté gauche

0001101 = 0

0110001 = 5

0011001 = 1

0101111 = 6

0010011 = 2

0111011 = 7

0111101 = 3

0110111 = 8

0100011 = 4

0001011 = 9

Vous constatez que chacun de ces dix codes sur 7 bits commence par un bit à 0 et se termine par un bit à 1. Si l’analyseur détecte du côté gauche une barre commençant par un 1 ou se terminant par un 0, il sait que soit il y a eu erreur de lecture du code, soit le code-barres a été altéré. Notez également que chaque code ne comporte que deux groupes de suites de 2 bits à 1. La conséquence est que chaque chiffre est codé dans le code UPC par deux barres.

Si nous étudions ces codes plus en détail, nous constatons qu’ils possèdent tous un nombre pair de bits à 1, ce qui constitue une autre mesure de protection contre les erreurs qui est la parité. Un groupe de bits est dit à parité paire s’il contient un nombre pair de bits à 1 ; il est en parité impaire dans le cas contraire. Tous les codes de la partie gauche sont donc en parité impaire.

Passons maintenant aux codes à 7 bits de la partie droite du code-barres :

Codes du côté droit

1110010 = 0

1001110 = 5

1100110 = 1

1010000 = 6

1101100 = 2

1000100 = 7

1000010 = 3

1001000 = 8

1011100 = 4

1110100 = 9

Si vous comparez les codes des mêmes chiffres entre côté gauche et côté droit, vous constatez que les codes de la partie droite sont les compléments des précédents : là où il y a un bit à 0 à gauche, il y a un bit à 1 du côté droit. Les codes commencent toujours par 1 et se terminent toujours par 0, fort logiquement. Bien sûr, le nombre de bits est pair, et donc la parité est paire aussi.

Nous savons maintenant tout ce qu’il faut connaître pour décoder un code UPC. Grâce aux deux tableaux précédents, nous découvrons la même séquence de chiffres que celle des douze chiffres décimaux imprimés dans le bas du code-barres de la boîte de soupe Campbell :

0 51000 01251 7

Ce résultat vous déçoit peut-être, mais c’est tout à fait normal : lorsque le scanner ne parvient pas à lire le code-barres, la caissière ou le caissier peut saisir le code à la main (vous avez certainement été témoin de la chose). Cette redondance nous épargne en tant qu’humains d’apprendre à décrypter le code-barres, et l’opération ne nous a rien appris de secret. Il n’y a vraiment rien d’autre dans le code UPC : trente barres qui correspondent à douze chiffres décimaux.

Le premier des douze chiffres qui est très souvent un 0 correspond à la catégorie de code qui englobe tous les articles ordinaires. Pour les produits vendus au poids tels que la viande ou les légumes, ce premier chiffre est un 2 et les coupons et rabais sont distingués par un 5.

Les cinq chiffres suivants identifient le fabricant. Dans l’exemple, 51000 est le code de l’entreprise Campbell Soup. Les cinq chiffres suivants 01251 identifient le produit qui est ici la boîte de conserve de taille standard de soupe de nouilles au poulet. Cette référence dépend du code du fabricant. La soupe aux nouilles d’un autre fabricant aura un autre code produit et le code 01251 peut désigner une chose différente chez un autre fournisseur.

Contrairement à ce qu’on que l’on pourrait croire, il n’y a aucune mention du prix dans un code-barres. Cette information est obtenue auprès du système informatique du magasin en réponse à la fourniture du code produit.

Le dernier chiffre, qui est un 7 dans l’exemple, est la clé de contrôle. En recalculant sa valeur, on peut détecter une erreur de lecture. Pour tester cette clé, nous attribuons une lettre de l’alphabet dans l’ordre à chacun des onze premiers chiffres (051000 01251) :

A BCDEF GHIJK

Nous appliquons la formule de calcul suivante :

3 × (A + C + E + G + I + K) + (B + D + F + H + J)

Il ne reste plus qu’à soustraire la valeur trouvée du prochain multiple de 10. Voici l’application de ce calcul pour notre soupe de nouilles :

3 × (0 + 1 + 0 + 0 + 2 + 1) + (5 + 0 + 0 + 1 + 5)

= 3 × 4 + 11

= 23

Le prochain multiple de 10 supérieur à 23 est 30. Le résultat est donc :

30 – 23 = 7

C’est effectivement la clé de contrôle 7 qui est indiquée en fin de code UPC. Cette clé constitue une technique de redondance. Si l’ordinateur qui a lu le code-barres ne trouve pas la même clé, le code-barres est considéré comme invalide.

En théorie, il suffirait de 4 bits pour coder un chiffre décimal entre 0 et 09 alors que le code UPC consomme 7 bits pour chaque chiffre. De ce fait, 95 bits sont consommés pour onze chiffres décimaux utiles. Pour être exact, le code UPC comporte également des espaces vides qui sont l’équivalent de 9 bits à 0 du côté gauche et du côté droit des motifs de zone de garde. Au total, un code UPC consomme donc 113 bits alors qu’il ne code que onze chiffres décimaux, ce qui fait plus de 10 bits par chiffre. Quelle gabegie !

Une partie de cette surconsommation sert au contrôle des erreurs comme déjà mentionné. Les codes-barres n’auraient sans doute pas eu un grand succès s’il avait été possible à n’importe quel client de les détourner avec un simple coup de feutre.

Par ailleurs, il faut savoir qu’un code UPC est lisible dans les deux sens, tête-bêche. Lorsque les premiers chiffres qu’il détecte ont une parité paire (un nombre de bits à 1 pair dans chaque code sur 7 bits), le scanner sait qu’il est en train de lire le code-barres en sens inverse, de droite à gauche. L’ordinateur se sert alors du tableau suivant pour décoder les chiffres du côté droit :

Codes du côté droit lus à l’envers

0100111 = 0

0111001 = 5

0110011 = 1

0000101 = 6

0011011 = 2

0010001 = 7

0100001 = 3

0001001 = 8

0011101 = 4

0010111 = 9

Il se sert du tableau suivant pour les chiffres du côté gauche lus à l’envers :

Codes du côté gauche lus à l’envers

1011000 = 0

1000110 = 5

1001100 = 1

1111010 = 6

1100100 = 2

1101110 = 7

1011110 = 3

1110110 = 8

1100010 = 4

1101000 = 9

Aucun de ces codes sur 7 bits ne peut être confondu avec un des codes utilisés pour la lecture du code-barres de gauche à droite ; aucune confusion n’est possible.

Pour stocker un plus grand nombre d’informations dans un code lisible par un scanner, il faut travailler en deux dimensions au lieu d’une seule. La séquence de barres et d’espaces de largeur variable laisse place à une matrice de carrés blancs et noirs.

Codes QR

Le standard de code-barres à deux dimensions le plus répandu est le code QR (Quick Response). Conçu par un japonais en 1994, il est utilisé dorénavant dans des domaines très divers.

On peut facilement créer un code-barres QR gratuitement en se rendant sur l’un des sites prévus à cet effet. On trouve également des logiciels pour lire et décoder les codes QR au moyen d’un appareil photo ou d’une caméra. Dans les milieux industriels, on utilise des scanners à code QR spécifiques, par exemple pour le suivi des livraisons et les inventaires logistiques.

Voici le code QR qui contient l’adresse URL du site Web du livre dans sa version américaine (www.CodeHiddenLanguage.com) :

Si vous avez votre téléphone près de vous et s’il est doté d’une application appropriée, vous pouvez directement lire ce code-barres et rendre visite au site correspondant.

Un code QR est une matrice de petits carrés que l’on appelle des modules. Le code QR d’exemple comporte 25 modules dans le sens horizontal et vertical, ce qui correspond à la taille de la version 2. Il existe 40 tailles différentes de code QR ; la version 40 comprend 177 modules dans les deux sens.

Chaque petit bloc correspond à un bit : il vaut 0 s’il est blanc et 1 s’il est noir. Une matrice telle que celle de l’exemple contient donc 25 × 25, soit 625 bits. La capacité d’encodage nette n’est cependant que d’un tiers de cette valeur théorique. De nombreuses informations sont consacrées à un mécanisme complexe de détection et correction des erreurs. Les codes QR sont ainsi protégés contre les altérations, et cela facilite la récupération de données, même en cas de code endommagé. Nous n’entrerons pas dans les détails des techniques de correction d’erreur de code QR.

Vous remarquez immédiatement que le code contient plusieurs motifs fixes, notamment pour que le scanner du code détermine l’orientation de la matrice. Dans l’image suivante, nous voyons les motifs fixes en noir sur blanc, les autres données étant estompées en gris :

Les trois plus gros carrés dans les angles sont les motifs de positionnement et le carré plus petit vers le bas à droite est le motif d’alignement. Ces éléments aident le lecteur de code à connaître l’orientation de la matrice et à réparer d’éventuelles distorsions optiques. Les deux séquences alternant noir et blanc dans les sens horizontal et vertical sont les motifs de timing ; ils servent à connaître le nombre de cellules que contient le code QR. Enfin, il faut que la matrice du code soit entourée par une zone vierge, qui est une bordure blanche et qui doit être d’une taille au moins égale à quatre fois la taille d’une cellule.

Lors de la création d’un code QR, plusieurs options sont disponibles, notamment parmi plusieurs systèmes de correction d’erreur. L’information de correction dont doit disposer le lecteur du code pour réaliser la correction d’erreur (parmi d’autres tâches) correspond aux 15 bits de l’information de format et apparaissent à deux reprises dans le code. Dans la figure suivante, cela correspond aux 15 bits numérotés de 0 à 14 sur deux côtés du motif de positionnement de l’angle supérieur gauche. Ils sont répétés sous le motif de positionnement en haut à droite et en bas à gauche :

Il arrive que l’on fasse apparaître les nombres comme dans l’exemple pour aider au repérage. Le bit 0 est le moins significatif, celui qui est tout à fait à droite dans le nombre. Inversement, le bit 14 est le plus significatif. Si on considère que les cellules blanches sont des bits à 0 et que les cellules noires sont des bits à 1, voici comment s’écrit le nombre complet sur 15 bits :

111 0010 1111 0011

Le bit 0 est le moins significatif, le bit de poids faible ou bit LSB (least significant bit) parce que c’est celui qui correspond à la position du chiffre 2 à la puissance 0, c’est-à-dire la valeur 1. (Nous avons montré les puissances de 2 en fin de chapitre précédent.)

La valeur de ce nombre sur 15 bits n’a pas d’importance. Ici, il suffit de savoir qu’il réunit trois informations. Les 2 bits de poids fort (les plus significatifs) servent à définir un niveau de correction d’erreur parmi quatre possibles. Les 10 bits les moins significatifs constituent un code BCH sur 10 bits servant à la correction d’erreur. (BCH rappelle les initiales de ses inventeurs, Bose, Chaudhuri et Hocquenghem. J’avais pourtant indiqué que je ne parlerais pas en détail de la correction d’erreur des codes QR.)

Entre ces deux groupes de bits (type de correction d’erreur et BCH), il reste 3 bits qui ne servent pas à la correction d’erreur. Les voici distingués par une mise en gras :

111 0010 1111 0011

La pratique a montré que les lecteurs de code QR étaient plus efficaces lorsque le nombre de petits carrés blancs et noirs était à peu près équilibré. Cela ne peut pas être le cas pour toutes les données encodées. Pour pallier ce souci, le programme qui génère le code QR va sélectionner un motif de masque qui va rétablir l’équilibre entre carrés blancs et noirs. Le masque est appliqué au code QR initial, et inverse l’état de certaines cellules de blanc vers noir et inversement.

La documentation de la norme QR définit huit motifs de masque qui sont sélectionnés au moyen d’une séquence sur 3 bits parmi les 8 possibles : 000, 001, 010, 011, 100, 101, 110 et 111. Dans le code QR d’exemple, le motif de masque correspond au type 100, qui stipule une série de lignes horizontales alternant entre blanc et noir :

Les cellules du code QR initial qui tombent dans une zone blanche du masque restent telles quelles. Celles qui tombent sous une partie noire du masque voient leur état inversé de noir vers blanc ou de blanc vers noir. Vous remarquez que le masque évite d’avoir un effet sur les zones fixes et la zone d’information de gestion du code. Voici le résultat de l’application de ce masque au code QR initial :

Si vous comparez l’image résultante avec celle du code QR de départ, vous remarquerez que la première ligne voit ses couleurs inversées, la deuxième ligne ne change pas, la troisième ligne voit ses couleurs inversées, etc.

Voyons maintenant comment sont codées les données utiles. Nous attaquons par les 4 bits de l’angle inférieur droit. Dans la figure suivante, ce sont les cellules numérotées de 0 à 3, la cellule 3 correspondant au bit de poids fort, le plus significatif :

Ces 4 bits correspondent à l’indicateur de type de données, ce qui permet de savoir quel type d’information est contenu dans le code QR. Voici une sélection des valeurs possibles :

Indicateur de type
Signification

0001

Chiffres seulement

0010

Lettres et chiffres

0100

Texte codé en valeurs sur 8 bits

1000

Idéogrammes kanji du japonais

Le code QR de notre exemple possède une valeur de type égale à 0100, ce qui signifie qu’il contient des valeurs sur 8 bits correspondant à du texte.

Les huit cellules situées au-dessus de l’indicateur de type de données constituent l’élément suivant. Elles sont numérotées de 0 à 7 dans la figure suivante :

La valeur dans l’exemple est égale à 0001 1010, soit 26d. C’est le nombre de caractères codés dans ce code QR.

L’ordre dans lequel se présentent les caractères qui constituent la « charge utile » est normalisé, mais particulier. En effet, le premier caractère se trouve juste au-dessus du nombre de caractères. Chaque caractère, mais pas toujours, occupe une zone de deux cellules de large sur quatre de haut. Les caractères se suivent pour balayer la zone de données de la matrice de la façon suivante :

Nous avons dit que tous les caractères n’occupaient pas une zone de deux cellules de large. La spécification QR officielle définit de façon stricte comment les bits sont répartis lorsque la zone du caractère n’est pas rectangulaire. Dans l’image suivante, les cellules des 26 caractères sont délimitées en rouge et les cellules sont numérotées de 0 à 7 :

Selon la norme QR, le texte est codé sous forme de valeurs sur 8 bits conformément au standard ISO/IEC 8859. Il s’agit d’une variante du standard de codage des caractères ASCII, sur lequel nous allons revenir en détail dans le Chapitre 13.

Le premier caractère vaut 0111 0111, ce qui correspond au code ASCII de la lettre w. Le caractère suivant est la même lettre et le troisième aussi, même si ce caractère est dans une matrice horizontale. On descend alors dans les deux colonnes suivantes pour tomber sur le caractère 0010 1110 qui correspond au signe point. Il est suivi de 0100 0011 qui est la lettre C, et qui est suivi par 0110 1111, soit la lettre o. Le caractère suivant est à cheval sur les deux colonnes ; c’est la valeur 0110 0100, soit d minuscule. Le caractère suivant est trouvé en remontant dans la double colonne ; c’est 0110 0101, soit la lettre e. On poursuit ainsi pour reconstituer l’adresse www.CodeHiddenLanguage.com.

Et voilà, vous avez décodé votre premier code QR. Ce qui reste à extraire du code ne concerne que la correction d’erreur.

Les codes tels que le code-barres UPC/EAN et le code QR semblent mystérieux au premier contact, et il n’y a rien d’étonnant à constater que bien des gens sont persuadés qu’ils cachent des données secrètes, voire sataniques. En réalité, pour que ces codes soient acceptés à vaste échelle, leur constitution doit être très bien documentée et largement accessible. Plus ils sont utilisés, plus ils gagnent de valeur en tant qu’éléments de notre trousse à outils de communication.

On trouve donc des bits individuels partout dans ce que nous avons vu, mais je viens de terminer ma présentation des codes QR en parlant de valeurs sur 8 bits. Il existe un mot consacré pour ce format de regroupement de bits ; je suis persuadé que vous le connaissez déjà.

1 Du groupe américain Tony Orlando and Dawn.

Chapitre 12  Octets et hexadécimal

Individuellement, un bit d’information peut avoir un gros impact : oui ou non, vrai ou faux, réussite ou échec. Néanmoins, on utilise généralement les bits de façon groupée pour symboliser des nombres et différentes catégories de données telles que du texte, des sons, des images fixes ou animées. Construire un circuit capable d’additionner les valeurs de deux bits est un exercice remarquable, mais c’est à partir du moment où le circuit devient capable d’additionner plusieurs bits en même temps que l’on s’approche vraiment de la possibilité de créer un ordinateur.

Lorsqu’ils ont à déplacer et à traiter des bits, les ordinateurs les regroupent dans un ensemble que l’on appelle un mot (word). La taille du mot, c’est-à-dire le nombre de bits qu’il regroupe, constitue une caractéristique fondamentale de l’architecture de chaque ordinateur. En effet, tous les traitements et déplacements de données dans la machine se font avec un ou plusieurs mots à la fois.

Certains des premiers ordinateurs utilisaient une longueur de mot qui était un multiple de 6, par exemple 12, 18 ou 24 bits. Ces longueurs semblaient intéressantes parce qu’elles permettaient une utilisation confortable de la base de numération octale. Vous vous souvenez que les huit chiffres en octal sont 0 à 7, et que chaque valeur octale traduit directement une valeur binaire sur 3 bits comme le rappelle le tableau suivant :

Binaire
Octal

000

0

001

1

010

2

011

3

100

4

101

5

110

6

111

7

Un mot de 6 bits peut être représenté par deux chiffres en octal, et les tailles de mots supérieures, 12, 18 et 24, en sont des multiples. Huit chiffres en octal suffisent pour un mot sur 24 bits.

En réalité, le développement de l’informatique a emprunté une autre voie. En effet, une fois que les nombres binaires ont été reconnus pour leur importance, on a compris qu’il ne serait pas judicieux d’utiliser des tailles de mots multiples de 3 tels que 6, 12, 18, 24 qui, par ailleurs, ne sont pas des puissances de 2.

C’est ainsi qu’est apparu l’octet (byte).

Le terme anglais byte, traduit en « octet », est apparu chez IBM vers 1956. Au départ, il s’agissait en fait du mot bite (morceau), mais on a remplacé le i par un y afin d’éviter toute confusion avec le mot bit. Au départ, le terme byte désignait un nombre de bits dans un chemin de transfert de données. Au milieu des années 1960, IBM avait lancé sa fameuse gamme de superordinateurs pour entreprises System/360, et en avait profité pour stabiliser la largeur à 1 byte pour 8 bits1.

Ce choix de 8 bits est devenu pérenne ; dorénavant, la largeur du groupement de bits élémentaire est de 8 bits.

Un octet peut donc représenter une valeur binaire entre 0000 0000 et 1111 1111, c’est-à-dire une valeur décimale entre 0 et 255, soit une chose parmi 256. La pratique a montré que le groupement par huit aboutit à une taille de mots ni trop grande, ni trop petite. Nous verrons au long des chapitres à venir que l’octet convient au stockage de texte simple, du fait que de nombreux alphabets du monde peuvent être codés avec moins de 256 caractères différents. Pour les langues qui en demandent plus, notamment les idéogrammes chinois, japonais et coréens, on utilise 2 octets, ce qui permet de représenter 2¹⁶ ou 65 536 dessins de lettres2. L’octet convient par ailleurs bien au codage des niveaux de luminosité d’une image en nuances de gris telle qu’une photographie parce que les humains distinguent au mieux à peu près 256 nuances différentes. Pour l’affichage en couleur des écrans, 3 octets permettent de coder le niveau des trois couleurs primaires rouge, vert et bleu.

La révolution de l’informatique personnelle qui a débuté à la fin des années 1970 a commencé avec des ordinateurs à 8 bits. Les progrès technologiques ont permis ensuite de doubler la largeur utile : on est passé à 16 bits, puis à 32, puis à 64 bits. Certaines utilisations scientifiques ont recours à des ordinateurs travaillant sur 128, voire 256 bits.

On s’intéresse parfois à la moitié d’un octet, c’est-à-dire un bloc de 4 bits. On désigne cette unité sous le terme nibble ou nybble, mais ce mot est beaucoup moins fréquent que octet.

Les octets constituent donc la « matière » fondamentale d’un ordinateur. Il est indispensable de trouver une technique pour noter, désigner les valeurs de façon plus confortable que sous forme de longues chaînes de chiffres binaires. On peut bien sûr recourir à la base octale : l’octet ayant la valeur 1011 0110 peut être subdivisé en groupes de trois en commençant par la droite, puis en convertissant chaque groupe en un chiffre octal comme ceci :

Il est évident qu’il est plus facile de noter la valeur octale 266 que la valeur binaire 1011 0110. Il y a cependant un problème d’incompatibilité entre les deux bases comme déjà suggéré : on ne peut pas diviser 8 par 3 sans reste. De ce fait, la représentation octale d’une valeur sur 16 bits

est différente de la valeur octale des 2 octets pris individuellement à partir de la valeur sur 16 bits :

Pour que la représentation d’une valeur composée de plusieurs octets reste homogène par rapport à celle d’un seul octet, il faudrait trouver un système de numérotation dans lequel chaque octet peut être divisé en un nombre entier de groupes de bits.

On pourrait diviser chaque octet en quatre groupes de 2 bits, ce qui nous amènerait à la base 4 dont nous avons rapidement parlé dans le Chapitre 10. Mais nous ne faisons ainsi que diviser par deux l’encombrement total de la notation.

Ne pourrions-nous pas envisager de diviser l’octet en deux valeurs sur 4 bits ? Cela reviendrait à utiliser la base de numération 16.

La base 16 ? Voilà bien quelque chose que nous n’avons pas du tout envisagé, et il y avait de bonnes raisons à cela ! La base 16 porte le nom de base hexadécimale, et rien que ce nom peut faire peur. La plupart des mots qui commencent par le préfixe hexa (hexagone, hexapode ou hexamètre) font référence au chiffre 6. Le terme hexadécimal signifie « six et dix ». En tant que fidèle utilisateur du guide de style Microsoft, je prends soin de ne pas trop fréquemment employer l’abréviation « hexa », mais il m’arrive de devoir le faire.

Ce n’est pas seulement son nom qui singularise le système hexadécimal. Dans la base décimale, nous comptons ainsi :

0 1 2 3 4 5 6 7 8 9 10 11 12...

Dans la base octale, nous n’avons plus besoin des chiffres 8 et 9 :

0 1 2 3 4 5 6 7 10 11 12...

En hexadécimal, il nous faut six symboles de chiffres de plus qu’en décimal. Si nous voulions compter en hexadécimal, nous procéderions ainsi :

0 1 2 3 4 5 6 7 8 9 (il nous manque six symboles ici) 10 11 12…

Ici, le nombre 10, que l’on doit maintenant prononcer « un-zéro », correspond à la valeur décimale 16. Que pourrions-nous inventer comme symbole pour les six positions manquantes ? Nous ne pouvons pas profiter ici d’un héritage de symboles de numération comme pour les bases plus petites que la décimale. Nous devons donc choisir six nouveaux symboles, par exemple :

À la différence des dessins représentant les chiffres auxquels nous sommes habitués, ces nouveaux symboles seraient choisis pour être faciles à mémoriser, en ayant une relation sémantique avec la quantité correspondante.

On parle ainsi aux États-Unis de chapeau de cow-boy de 10 gallons.

Une équipe de football américain est constituée de 11 joueurs.

Une boîte de beignets en contient une douzaine, donc 12.

Le chat noir est associé au chiffre 13.

La pleine lune se montre 14 jours après la nouvelle lune.

Le poignard nous rappelle l’assassinat de Jules César un 15 de mars.

Nous n’allons pas continuer dans cette voie. Vous serez peut-être ravi d’apprendre que nous n’utiliserons pas de dessins de ballon et de beignets pour symboliser les chiffres supplémentaires de la base hexadécimale. Ce qui a été choisi pour la notation hexadécimale garantit que tout le monde sera confus dans un premier temps, et même dans un deuxième : pour reconnaître les six chiffres hexadécimaux spécifiques, on a en effet choisi de réutiliser les six premières lettres de l’alphabet latin :

0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12...

Le tableau suivant montre l’équivalence entre les trois bases binaire, hexadécimale et décimale :

Binaire
Hexadécimal
Décimal

0000

0

0

0001

1

1

0010

2

2

0011

3

3

0100

4

4

0101

5

5

0110

6

6

0111

7

7

1000

8

8

1001

9

9

1010

A

10

1011

B

11

1100

C

12

1101

D

13

1110

E

14

1111

F

15

Être forcé d’utiliser des lettres pour symboliser des chiffres n’est pas très agréable au départ, et la confusion s’accroît lorsque l’on emploie aussi des nombres pour des lettres. Pourtant, la notation hexadécimale est là, et pour toujours. Sa raison d’être essentielle est évidente : elle permet de représenter la valeurs des octets de la façon la plus compacte imaginable, et elle le fait assez bien.

En effet, 1 octet étant constitué de 8 bits, sa valeur est rendue par deux chiffres hexadécimaux entre 00 et FF. La valeur 1011 0110 correspond au nombre hexadécimal B6, alors que l’octet 0101 0111 correspond à 57 en hexadécimal (on peut écrire 57h).

On devine que la valeur B6 est en hexadécimal à cause de la présence de la lettre, ce qui n’est pas le cas de la valeur 57 qui pourrait être décimale ou octale. Pour éviter toute confusion, il faut imaginer un qualificateur. En réalité, il existe plus de vingt techniques différentes pour distinguer les valeurs hexadécimales selon le langage de programmation et l’environnement concernés. Dans la suite du livre, je vais utiliser la lettre h minuscule accolée en suffixe de la valeur, comme dans B6h et 57h.

Voici quelques valeurs hexadécimales sur 1 octet avec l’équivalent décimal :

Binaire
Hexadécimal
Décimal

00000000

00h

0

00010000

10h

16

00011000

18h

24

00100000

20h

32

01000000

40h

64

01100100

64h

100

10000000

80h

128

11000000

C0h

192

11001000

C8h

200

11100000

E0h

224

11110000

F0h

240

11111111

FFh

255

Comme pour les valeurs binaires, on ajoute souvent des zéros non significatifs en début de valeur hexadécimale pour harmoniser le nombre de chiffres. Dans le cas des grandes valeurs, chaque groupe de quatre chiffres binaires correspond directement à un seul chiffre hexadécimal. Une valeur sur 16 bits (2 octets) correspond à quatre chiffres hexadécimaux et une valeur sur 32 bits, à huit chiffres hexadécimaux.

L’adoption à large échelle de la notation en base 16 a entraîné une nouvelle convention consistant à séparer les groupes de quatre chiffres par des espaces. Partons de la valeur binaire énorme suivante :

0010010001101000101011001110

Nous constatons qu’il est plus facile de la lire en écrivant ainsi :

0010 0100 0110 1000 1010 1100 1110

ou encore ainsi :

0010-0100-0110-1000-1010-1100-1110

La correspondance par rapport à la valeur hexadécimale devient bien plus évidente :

Notez que cette valeur correspond au nombre hexadécimal 2468ACE, qui est la suite des valeurs hexadécimales paires en ordre croissant. (Imaginez toute une promo chantant à tue-tête « 268ACE, la promo alpha est bénie des dieux ! »).

Si vous avez eu l’occasion de vous frotter au langage HTML utilisé dans les pages Web, vous avez peut-être déjà aperçu des valeurs exprimées en hexadécimal. En effet, les points de couleur que sont les pixels de l’écran sont exprimés par une combinaison des trois couleurs primaires, rouge, vert et bleu (RGB). La luminosité de chacune des trois composantes est exprimée par une valeur sur 1 octet ; il faut 3 octets pour définir une nuance à afficher. Dans les pages HTML, les couleurs sont précisées au moyen d’une valeur hexadécimale sur six chiffres qui est précédée d’un signe dièse. Par exemple, une teinte rouge vif correspondrait à la valeur de couleur #E74536. Cela correspond à une valeur de rouge égale à E7h, une valeur de vert de 45h et une valeur de bleu de 36h. On peut également spécifier la même couleur dans les pages HTML sous la forme d’un trio de valeurs décimales, comme ceci : rgb (231, 69, 54).

Une fois que l’on sait qu’il faut 3 octets pour définir la couleur de chaque pixel d’un écran informatique, on peut en déduire d’autres informations de dimensionnement. Si l’écran de l’ordinateur sait afficher 1 920 pixels en largeur sur 1 080 pixels en hauteur (le format standard en haute définition pour la télévision), il faut prévoir 1 920 × 1 080 × 3 octets, soit 6 220 800 octets, pour stocker une image.

La luminosité de chaque composante de couleur peut varier entre 0 et 255. Le nombre total de nuances possibles est donc de 256 × 256 × 256 ou 16 777 216 nuances de couleur. En base hexadécimale, cela correspond à 100h fois 100h fois 100h, soit 1 million hexa.

Dans un nombre hexadécimal, les chiffres correspondent à des puissances de 16 :

Décomposons la valeur hexadécimale 9A48Ch :

9A48Ch = 9 × 10 000h +

A × 1 000h +

4 × 100h +

8 × 10h +

C × 1h

Reformulons la décomposition avec des puissances de 16 :

9A48Ch = 9 × 16⁴ +

A × 16³ +

4 × 16² +

8 × 16¹ +

C × 16⁰

Nous pouvons convertir ces puissances en décimal :

9A48Ch = 9 × 65 536 +

A × 4 096 +

4 × 256 +

8 × 16 +

C×1

Vous remarquez au passage qu’il n’y a pas à craindre de confusion lorsque les chiffres sont présentés isolément comme dans 9, A, 4, 8 et C. En effet, la valeur 9 vaut autant en décimal qu’en hexadécimal. La valeur A ne peut être qu’hexadécimale et correspond à 10d.

Nous pouvons même simplifier le calcul en décimal en convertissant toutes les lettres-chiffres hexa en valeurs décimales :

9A48Ch = 9 × 65 536 +

10 × 4 096 +

4 × 256 +

8 × 16 +

12 × 1

Le résultat est 631 948d. Nous venons de voir comment convertir une valeur hexadécimale en décimal.

Voici un gabarit permettant de convertir n’importe quelle valeur hexadécimale sur quatre chiffres en décimal :

Essayons ce gabarit pour convertir la valeur 79ACh. Je rappelle que les deux chiffres hexadécimaux A et C correspondent à 10d et 12d respectivement :

L’opération inverse consistant à convertir du décimal vers l’hexadécimal demande en général de faire des divisions. Lorsque la valeur décimale est inférieure ou égale à 255, nous savons que nous pouvons la représenter sur 1 octet, donc deux chiffres hexa. Pour les calculer, il faut diviser le nombre par 16 et récupérer le quotient et le reste. En partant de la valeur décimale 182d, nous la divisons par 16 pour obtenir 11d, ce qui correspond à B en hexa avec un reste égal à 6. L’équivalent de 182d en hexa est donc B6h.

Si la valeur décimale à convertir est inférieure à 65 536, la valeur hexa sera composée de quatre chiffres au maximum. Voici le gabarit permettant de convertir une telle valeur vers la base hexadécimale :

Nous commençons par placer la valeur décimale initiale dans la case en haut à gauche :

Nous divisons le nombre par 4096d pour récupérer le quotient et le reste. On inscrit le quotient dans la case en bas à gauche et le reste dans la prochaine case en haut :

Nous pouvons diviser ce reste par 256d pour récupérer le quotient 9 et un nouveau reste 172d. Nous progressons ainsi :

Puisque les deux valeurs décimales 10 et 12 correspondent aux valeurs hexa A et C, le résultat s’écrit 79ACh.

Une autre solution pour convertir une valeur décimale d’au maximum 65 535 consiste d’abord à subdiviser la valeur en 2 octets au moyen d’une division par 256. On procède ensuite dans chaque octet à une division par 16 selon le gabarit suivant :

Nous commençons en haut. Après chaque division, le quotient est inscrit dans la case de gauche et le reste dans la case de droite. Voici, par exemple, la conversion de la valeur 51966d :

Les quatre chiffres hexadécimaux résultants ont les valeurs décimales 12, 10, 15 et 14, c’est-à-dire CAFE. (On dirait plus le nom d’une boisson qu’une valeur numérique, mais vous ne serez sans doute pas d’accord pour payer votre café 51 966 €.).

Comme toute base numérique qui se respecte, il y a une table des additions en hexadécimal :

+
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F

0

0

1

2

3

4

5

6

7

8

9

A

B

C

D

E

F

1

1

2

3

4

5

6

7

8

9

A

B

C

D

E

F

10

2

2

3

4

5

6

7

8

9

A

B

C

D

E

F

10

11

3

3

4

5

6

7

8

9

A

B

C

D

E

F

10

11

12

4

4

5

6

7

8

9

A

B

C

D

E

F

10

11

12

13

5

5

6

7

8

9

A

B

C

D

E

F

10

11

12

13

14

6

6

7

8

9

A

B

C

D

E

F

10

11

12

13

14

15

7

7

8

9

A

B

C

D

E

F

10

11

12

13

14

15

16

8

8

9

A

B

C

D

E

F

10

11

12

13

14

15

16

17

9

9

A

B

C

D

E

F

10

11

12

13

14

15

16

17

18

A

A

B

C

D

E

F

10

11

12

13

14

15

16

17

18

19

B

B

C

D

E

F

10

11

12

13

14

15

16

17

18

19

1A

C

C

D

E

F

10

11

12

13

14

15

16

17

18

19

1A

1B

D

D

E

F

10

11

12

13

14

15

16

17

18

19

1A

1B

1C

E

E

F

10

11

12

13

14

15

16

17

18

19

1A

1B

1C

1D

F

F

10

11

12

13

14

15

16

17

18

19

1A

1B

1C

1D

1E

On peut se servir de cette table et des règles de retenue habituelle pour faire l’addition de deux nombres hexadécimaux :

Si vous voulez vous épargner ce genre de calcul mental, sachez que aussi bien Windows que macOS disposent d’une calculatrice scientifique avec un mode programmeur pour effectuer toutes vos conversions entre binaire et hexadécimal.

Vous pouvez également envisager de construire vous-même un additionneur ordinaire sur 8 bits, comme nous le verrons dans le Chapitre 14.

1 N.d.T. : avec le mot français octet, la largeur du byte devient visible dans le nom même. On aurait du mal à accepter un octet englobant 7 ou 9 bits.

2 N.d.T. : on appelle glyphes les dessins des lettres.

Chapitre 13  ASCII et Unicode

Dès que l’on est assis devant un ordinateur ou que l’on tapote sur une tablette ou sur son téléphone, on manipule du texte. Soit on le lit, soit on en saisit, ou on le copie puis le colle d’un endroit à un autre, depuis une page Web vers un traitement texte, depuis un courriel vers un réseau social, depuis une notification vers le SMS pour un ami.

Rien de tout cela ne serait possible s’il n’y avait pas eu une convention pour représenter les lettres du texte sous forme de bits et d’octets dans les ordinateurs. Les règles de codage des caractères sont donc sans conteste un standard informatique indispensable. C’est grâce à lui qu’il est possible de faire communiquer des machines utilisant des systèmes et des applications différentes, avec des matériels et des logiciels d’origines diverses, et d’un pays et donc d’une culture à l’autre.

Cela dit, la restitution du texte sur un ordinateur subit encore parfois quelques échecs. Alors que j’étais en train de relire le présent chapitre, courant 2021, j’avais reçu un courriel d’un fournisseur d’hébergement Web dont le message comportait la ligne suivante :

Lâ€™acompte nous est bien parvenu, merci.

Vous avez certainement été témoin au moins une fois de ce genre d’interprétation erronée de certains caractères. Vers la fin du chapitre, vous saurez exactement comment de telles choses peuvent encore se produire.

Au début de ce livre, nous avions présenté deux systèmes pour représenter du texte avec des codes binaires. À vrai dire, le code morse n’est pas un vrai code binaire, puisqu’il demande d’utiliser des points et des traits avec différentes durées de pause entre les éléments. Cela dit, tous les constituants du code morse sont des multiples de la longueur d’un point : un trait vaut trois fois la durée d’un point, les pauses entre les lettres valent la durée d’un trait et les pauses entre les mots, la durée de deux traits. Si on considère que le point équivaut à 1 bit, le trait vaut 3 bits d’affilée et les pauses sont des séquences de bits à 0. Voici par exemple l’encodage en code morse de l’expression de bienvenue « HI THERE » avec son équivalent sous forme de chiffres binaires :

Le code morse est un code à nombre de bits variable : ce nombre de bits n’est pas le même pour tous les caractères.

Le code braille est beaucoup plus simple puisque chaque caractère est symbolisé par une matrice constituée de six positions pouvant chacune être en relief ou pas. Le braille est donc vraiment un code sur 6 bits puisque chaque caractère est symbolisé par une valeur comportant 6 bits. En revanche, des caractères supplémentaires sont nécessaires en braille pour symboliser les chiffres et les lettres majuscules. Vous vous souvenez que pour exprimer un chiffre en braille, il faut utiliser un code d’échappement Shift, un caractère braille spécial qui change la signification des caractères qui le suivent.

Le code Baudot

Les codes Shift sont utilisés dans un autre ancien code binaire, celui qui avait été inventé pour les télégraphes dans les années 1870. Ce code avait été défini par un officier du service télégraphique français, Émile Baudot ; il continue à être désigné sous ce nom. Le code Baudot est resté en vigueur jusque dans les années 1960, par exemple par la société Western Union pour émettre et recevoir des télégrammes. Même de nos jours, vous pouvez encore rencontrer des personnes qui parlent d’un débit de données binaires en tant que débit en bauds.

Le code Baudot a énormément été utilisé avec les téléscripteurs, qui sont des sortes de machines à écrire dotées d’un clavier de trente touches et d’une barre d’espace. Chaque touche est un interrupteur qui provoque la génération d’un code binaire transmis par le câble de sortie du télétype, un bit après l’autre. Ces appareils possèdent également un mécanisme pour imprimer sur papier les codes reçus par le câble d’entrée. L’impression utilise des électro-aimants.

Le code Baudot utilise 5 bits, ce qui ne donne que 32 possibilités correspondant aux valeurs hexadécimales 00h à 1Fh. Voici le tableau d’affectation de ces 32 valeurs disponibles pour les lettres de l’alphabet :

Tableau 13.1

Code hexa
Lettre Baudot
Code hexa
Lettre Baudot

00

10

E

01

T

11

Z

02

Retour chariot (Carriage Return)

12

D

03

O

13

B

04

Espace

14

S

05

H

15

Y

06

N

16

F

07

M

17

X

08

Saut de ligne (Line Feed)

18

A

09

L

19

W

0A

R

1A

J

0B

G

1B

Shift Chiffres

0C

I

1C

U

0D

P

1D

Q

0E

C

1E

K

0F

V

1F

Shift Lettres

Le code 00h est laissé vacant. Parmi les 31 codes restants, 26 correspondent directement aux lettres de l’alphabet anglais. Les cinq restants sont mis en valeur dans le tableau par une mise en italique.

Le codes spécial 04h correspond à l’espace entre les mots. Les deux codes 02h et 08h correspondent aux deux actions Retour chariot et Saut de ligne, qui sont des actions héritées de l’époque des télétypes et téléscripteurs : lorsque sur une machine à écrire, vous arrivez à la fin de la ligne, vous poussez sur un levier pour ramener le chariot qui porte la feuille à gauche. Cette action a deux effets : elle provoque le renvoi du chariot avec le papier pour replacer le point d’impression devant la première colonne. C’est l’opération de retour chariot, Carriage Return, abrégé en CR. Simultanément, le levier provoque la rotation de la valeur d’une ligne du tambour pour pouvoir frapper sur la ligne suivante. C’est l’action de saut de ligne, Line Feed, abrégé en LF. Les deux actions sont clairement indépendantes dans le code Baudot. Une imprimante de téléscripteur Baudot sait réagir comme attendu à la réception de ces deux codes d’action.

Vous aurez beau chercher, il n’y a aucun signe de ponctuation dans le tableau précédent. Pour y accéder, il faut utiliser le code 1Bh de l’action Shift Chiffres (Figure Shift). Une fois ce code reçu, tous les codes suivants sont considérés comme des chiffres ou des signes de ponctuation jusqu’à recevoir le code de basculement complémentaire Shift Lettres (Letter Shift) 1Bh qui rebascule l’interprétation vers les caractères alphabétiques. Voici le tableau des codes pour la partie chiffres et ponctuation :

Tableau 13.2

Code hexa
Lettre Baudot
Code hexa
Lettre Baudot

00

10

3

01

5

11

+

02

Retour chariot (CR)

12

Who Are You ?

03

9

13

?

04

Espace

14

‘

05

#

15

6

06

,

16

$

07

.

17

/

08

Saut de ligne (LF)

18

-

09

)

19

2

0A

4

1A

Sonnerie

0B

&

1B

Shift Chiffres

0C

8

1C

7

0D

0

1D

1

0E

:

1E

(

0F

=

1F

Shift Lettres

Le tableau est celui qui s’applique aux États-Unis. Dans d’autres pays, notamment en Europe, les codes 05h, 0Bh et 16h servaient aux lettres accentuées. Le code de sonnerie Bell servait à faire retentir une sonnerie sur le téléscripteur. Le code 12h servait à demander au téléscripteur de renvoyer son identificateur.

Tout comme le code morse, il n’y a pas de distinction entre majuscules et minuscules dans le code Baudot. La phrase

JE DEPOSE 25$ DEMAIN.

correspond au flux de données hexadécimal suivant :

J

E

D

E

P

O

S

E

2

5

$

D

E

M

A

I

N

.

1A

10

04

12

10

0D

03

14

10

04

1B

19

01

16

1F

04

12

10

07

18

0C

06

1B

07

02

08

Notez bien les trois codes d’échappement Shift : il y a un code 1Bh pour pouvoir imprimer le signe $ et le montant, un code 1Fh après ce montant pour revenir en alphabétique, et un second code 1Bh juste pour imprimer le point final. La séquence se termine par le binôme retour chariot-saut de ligne.

Si vous transmettez ce flux de données deux fois de suite à un téléscripteur, voici ce qu’il va imprimer :

JE DEPOSE 25$ DEMAIN.

3 ?309’3 25$ DEMAIN.

Comment est-ce possible ? C’est tout simplement lié au fait que le dernier code d’échappement avant le début de la seconde réception était un code de basculement en mode chiffres. Tous les codes du début de la seconde ligne ont donc été interprétés comme des chiffres, jusqu’à revenir au prochain code d’échappement de retour aux lettres.

Ce genre de problème de décalage est classique dans les systèmes qui utilisent des codes d’échappement Shift. Lorsqu’il a fallu chercher un remplaçant au code Baudot, quelque chose de plus moderne et de plus polyvalent, on a pris soin d’éviter tout recours à ces codes d’échappement en attribuant des codes différents pour chacune des lettres majuscules, minuscules et pour chaque chiffre.

Mais de combien de bits a-t-on besoin pour un tel codage complet ? Au départ, on a cherché à répondre aux besoins de la langue anglaise. En considérant les lettres majuscules et minuscules de l’alphabet de l’anglais américain, il faut 52 codes. En y ajoutant les 10 codes des chiffres, on arrive à 62. Si l’on prévoit quelques signes de ponctuation, on dépasse déjà la valeur 64, qui est le nombre maximal de codes possibles sur 6 bits. En revanche, c’est un boulevard qui s’ouvre avant d’atteindre la prochaine limite suivante (128) à partir de laquelle il faudrait 8 bits.

La réponse est donc 7. Il nous faut 7 bits pour représenter tous les caractères, chiffres et signes de ponctuation utilisés dans un texte en anglais, sans employer aucun code d’échappement.

Le standard ASCII

Le bon vieux code Baudot a donc été remplacé par un code sur 7 bits appelé ASCII (prononcez « à ski »), qui est l’acronyme de l’American Standard Code for Information Interchange. Depuis 1967, ce code reste le standard le plus important dans le monde de l’informatique. Dès qu’il y a du texte en jeu dans un ordinateur, le code ASCII est certainement concerné, même si son remplaçant, que je décris un peu plus loin, est en train de se répandre.

Puisque c’est un code sur 7 bits, ASCII utilise les valeurs entre 00h et 7Fh, soit en binaire de 000 0000 à 111 1111. Nous allons découvrir les 128 codes ASCII prédéfinis en étudiant quatre groupes de 32. Nous commençons directement avec le deuxième groupe de 32 parce que le premier réunit des codes moins faciles à comprendre. Dans le deuxième groupe de 32, on trouve les signes de ponctuation et les dix chiffres. Le tableau suivant présente les codes de 20h à 3Fh avec les caractères correspondants :

Tableau 13.3

Code hexa
Caractère ASCII
Code hexa
Caractère ASCII

20

Espace

30

0

21

!

31

1

22

“

32

2

23

#

33

3

24

$

34

4

25

%

35

5

26

&

36

6

27

‘

37

7

28

(

38

8

29

)

39

9

2A

*

3A

:

2B

+

3B

;

2C

,

3C

<

2D

-

3D

=

2E

.

3E

>

2F

/

3F

?

Notez que le code 20h correspond à l’espace entre les mots et entre les phrases.

Le prochain groupe de 32 codes englobe toutes les lettres en majuscules, avec quelques autres signes de ponctuation. Hormis le signe @ et le caractère de soulignement _ (code 5Fh), ces signes n’existaient pas sur les téléscripteurs, mais sont devenus incontournables sur les claviers informatiques.

Tableau 13.4

Code hexa
Caractère ASCII
Code hexa
Caractère ASCII

40

@

50

P

41

A

51

Q

42

B

52

R

43

C

53

S

44

D

54

T

45

E

55

U

46

F

56

V

47

G

57

W

48

H

58

X

49

I

59

Y

4A

J

5A

Z

4B

K

5B

[

4C

L

5C

\

4D

M

5D

]

4E

N

5E

^

4F

O

5F

−

Le dernier groupe de 32 caractères englobe toutes les lettres en minuscules et quelques autres signes de ponctuation spécifiques à l’informatique :

Tableau 13.5

Code hexa
Caractère ASCII
Code hexa
Caractère ASCII

60

`

70

p

61

a

71

q

62

b

72

r

63

c

73

s

64

d

74

t

65

e

75

u

66

f

76

v

67

g

77

w

68

h

78

x

69

i

79

y

6A

j

7A

z

6B

k

7B

{

6C

l

7C

|

6D

m

7D

}

6E

n

7E

∼

6F

o

Vous remarquez l’absence du dernier code tout en bas à droite, celui de la valeur 7Fh. Nous y revenons bientôt.

Le texte :

Salut, toi !

sera codé selon la norme ASCII avec les codes hexadécimaux suivants :

S

a

l

u

t

,

t

o

i

!

53

61

6C

75

74

2C

20

74

6F

69

21

Vous remarquez l’utilisation d’une virgule (code 2Ch), d’une espace (code 20h) et d’un point d’exclamation (code 21h). Voici une autre phrase très brève :

J’ai 12 ans.

Et voici sa représentation ASCII :

J

’

a

i

1

2

a

n

s

.

4A

27

61

69

20

31

32

20

61

6E

73

2E

Vous remarquez que le nombre 12 est codé avec les deux valeurs hexadécimales 31h et 32h qui correspondent bien aux codes des deux chiffres. Lorsque le nombre 12 est cité dans un texte, il ne doit évidemment pas être codé avec les codes 01h et 02h, ni avec le code 0Ch. Ces codes ont une signification tout à fait différente dans la norme ASCII.

Dans le codage ASCII, la lettre E en majuscule se distingue de la même lettre en minuscule par un écart de 20h dans les valeurs. Ce n’est pas une coïncidence, car cela permet très facilement de réaliser des conversions entre majuscules et minuscules dans les logiciels. Il suffit d’ajouter la valeur 20h au code de la lettre majuscule pour obtenir la minuscule, et de soustraire cette valeur pour l’opération inverse. Pour être exact, il n’est même pas nécessaire de faire l’addition, car il suffit de changer l’état d’un bit pour basculer entre les deux casses (majuscules/minuscules). Nous verrons les techniques correspondantes un peu plus loin dans ce livre.

Les 95 codes ASCII que nous venons de voir correspondent aux caractères imprimables et affichables, ceux qui ont une représentation visuelle directe. La table prévoit aussi 33 caractères de contrôle, qui n’ont pas de représentation visuelle utile, mais correspondent à des actions. Voici la liste complète de ces 33 codes ASCII ; ne vous inquiétez pas si vous ne comprenez pas leur raison d’être. Lorsque la norme ASCII a été créée, elle devait pouvoir servir d’abord aux téléscripteurs, ce qui fait que la plupart de ces codes de contrôle ont perdu de nos jours leur usage1.

Les caractères de contrôle conservent leur intérêt pour réaliser quelques opérations de mise en forme rudimentaire du texte. Les équipements qui en bénéficient au mieux sont les téléscripteurs et les imprimantes, des machines qui impriment les caractères reçus sous forme de flux de codes ASCII. Le fonctionnement normal d’un tel appareil consiste à déplacer d’une position vers la droite la tête d’impression après avoir imprimé un caractère. Certains caractères de contrôle permettent de modifier ce comportement standard.

Tableau 13.6

Code hexa
Sigle
Nom du car. de contrôle

00

NUL

Null (rien)

01

SOH

Start of Heading

02

STX

Start of Text

03

ETX

End of Text

04

EOT

End of Transmission

05

ENQ

Enquiry (i.e., Inquiry)

06

ACK

Acknowledge

07

BEL

Sonnerie

08

BS

Retour arrière (Backspace)

09

HT

Tabulation horizontale

0A

LF

Saut de ligne (Line Feed, LF)

0B

VT

Tabulation verticale

0C

FF

Saut de page (Form Feed, FF)

0D

CR

Retour chariot (Carriage Return, CR)

0E

SO

Shift-Out

0F

SI

Shift-In

10

DLE

Data Link Escape

11

DC1

Device Control 1

12

DC2

Device Control 2

13

DC3

Device Control 3

14

DC4

Device Control 4

15

NAK

Negative Acknowledge

16

SYN

Synchronous Idle

17

ETB

End of Transmission Block

18

CAN

Cancel

19

EM

End of Medium

1A

SUB

Substitute Character

1B

ESC

Échappement

1C

FS

File Separator or Information Separator 4

1D

GS

Group Separator or Information Separator 3

1E

RS

Record Separator or Information Separator 2

1F

US

Unit Separator or Information Separator 1

7F

DEL

Delete

Intéressons-nous, par exemple, à la chaîne de codes hexadécimaux suivante :

41 09 42 09 43 09

Le caractère ayant le code 09 correspond au saut de tabulation horizontale que l’on a présenté. On considère que les colonnes dans lesquelles apparaissent les caractères imprimés sont numérotées à partir de 0 depuis la marge gauche. L’action déclenchée par le code Tab consiste à faire avancer par un saut la tête d’impression jusqu’à la prochaine position horizontale qui est un multiple de 8. Dans notre exemple, le résultat est le suivant :

A

B

C

Ces codes de tabulation sont très pratiques pour préserver l’alignement vertical en colonnes.

Certaines imprimantes (même encore de nos jours) répondent aux codes de saut de page 0Ch (Form Feed) en provoquant sur-le-champ l’éjection de la page en cours pour poursuivre l’impression sur une nouvelle page.

Le code de retour arrière Backspace sert parfois sur de vieilles imprimantes pour imprimer un caractère à partir de plusieurs éléments. Si vous voulez par exemple imprimer un e accent grave è alors que cette lettre n’est pas disponible dans votre jeu de caractères, vous pouvez transmettre la séquence de trois codes hexadécimaux 65h 08h 60h.

Les deux codes de contrôle les plus importants sont le Retour chariot CR et le Saut de ligne LF. Leur effet est le même que dans le code Baudot. Sur les anciennes imprimantes informatiques, le code Retour chariot ramène le chariot qui porte la tête d’impression sur la marge gauche sans changer de ligne, et le code Saut de ligne fait descendre la tête d’une ligne. Pour poursuivre sur la ligne suivante, il faut en général envoyer les deux codes. En effet, le code Retour chariot envoyé seul fait imprimer en surfrappe sur la ligne déjà imprimée. Quant au code Saut de ligne, s’il est utilisé seul, il fait descendre d’une ligne sans changer de colonne d’impression2.

Les textes, les images, la musique et les vidéos sont des données. Dans un ordinateur, ces données sont stockées sous forme de fichiers. Un fichier est une séquence d’octets associée à un nom unique. Les noms de fichiers sont en général construits à partir d’un nom qui évoque le genre de contenu du fichier et d’une extension de nom constituée de trois ou quatre lettres qui détermine le type ou format du fichier. Les fichiers qui contiennent uniquement des caractères en codage ASCII reçoivent en général l’extension de nom de fichier .txt qui est une abréviation de texte. « ASCII pur » signifie qu’aucun code spécial n’a enrichi le texte en affichant certaines portions en italique ou en gras, ou en jonglant parmi plusieurs polices et corps de caractères. Ces éléments d’enrichissement de texte correspondent justement au format dit « formaté » ou « texte enrichi » en contraste avec le format de texte ASCII brut ou simple. Dans un ordinateur fonctionnant sous Windows, le programme utilitaire Bloc-notes (Notepad) permet de créer des fichiers dans ce format simple. Sous macOS, l’outil équivalent est le programme TextEdit (mais il faut le forcer à sauvegarder dans ce format simplifié). Les deux outils permettent de choisir une police et une taille de caractères, mais uniquement pour l’affichage du contenu. Ces informations de personnalisation ne sont pas stockées avec le texte.

Aussi bien Notepad que TextEdit réagissent à la frappe de la touche Entrée ou Retour en insérant un saut de ligne et un retour chariot. Mais ces deux programmes savent également insérer des sauts de ligne doux (word wrapping). Lorsque ce que vous saisissez vous amène à atteindre la marge droite dans la fenêtre d’édition, le programme poursuit automatiquement l’affichage des caractères suivants au début de la ligne suivante, sans que vous l’ayez demandé. La totalité du texte reste néanmoins dans le même paragraphe. Pour passer à un nouveau paragraphe, vous utilisez la touche Entrée ou Retour.

Dès que vous frappez cette fameuse touche, le Bloc-notes Windows insère dans le fichier les deux codes hexadécimaux 0Dh et 0Ah, c’est-à-dire un retour chariot et un saut de ligne. En revanche, l’outil TextEdit sous macOS se contente d’insérer le code 0Ah de saut de ligne et l’ancien système Mac que l’on appelle Mac OS Classic (qui existait de 1984 jusqu’en 2000) n’insérait que le code 0Dh du retour chariot. Cette incohérence continue à poser problème lorsque l’on veut réutiliser un fichier créé sur un autre système. Les programmeurs ont cherché à réduire l’impact de ces incohérences assez récemment, mais il reste scandaleux que l’on n’ait pas encore réussi à se mettre d’accord sur un standard pour symboliser les fins de lignes et de paragraphes dans un fichier texte simple.

Codage EBCDIC

Le standard ASCII a commencé à dominer peu après son introduction, sauf chez IBM. En effet, IBM avait inventé son propre codage de caractères pour fonctionner avec ses grosses machines System/360. Ce codage porte le nom EBCDIC (Extended BCD Interchange Code). Il s’agit d’une extension sur 8 bits d’un ancien codage sur 6 bits qui portait le nom BCDIC, lui-même provenant du codage en vigueur sur les cartes perforées IBM. Rappelons qu’une carte perforée permettait de stocker 80 caractères de texte. Ces cartes avaient été inventées par IBM en 1928 et ont servi pendant plus de cinquante ans.

Les rectangles noirs dans la figure représentent les trous ou perforations. Les cartes perforées souffraient d’une limitation matérielle : lorsqu’il y avait trop de trous dans une carte, elle devenait trop fragile, au point de provoquer des bourrages dans les lecteurs.

Le codage de chaque caractère sur une telle carte consiste à combiner des trous rectangulaires dans la même colonne. Le caractère lisible est rappelé en général dans le coin supérieur gauche de la carte. Les dix lignes du bas sont les lignes des chiffres et sont identifiées par un numéro : ligne-0, ligne-1, jusqu’à ligne-9. C’est là un héritage des premiers ordinateurs qui fonctionnaient directement en base décimale. Les deux lignes non numérotées dans la partie supérieure sont les lignes de zone et portent les noms ligne-11 et ligne-12, cette dernière étant celle située tout en haut. Il n’y a pas de ligne-10.

Le code de chaque caractère EBCDIC est une combinaison de trous de zone et de trous de chiffres. Les codes des dix chiffres décimaux en EBCDIC correspondent aux valeurs F0h jusqu’à F9h. Les lettres en majuscules sont réparties dans trois groupes entre C1h et C9h, puis D1h jusqu’à D9h et, enfin, E2h jusqu’à E9h. Les lettres minuscules sont codées dans trois autres groupes de 81h à 89h, puis de 91h à 99h et, enfin, de A2h à A9h.

Alors qu’en ASCII, les majuscules et les minuscules forment des plages continues, rendant les tris alphabétiques faciles, cela n’est pas possible en codage EBCDIC à cause des non-continuités dans les séquences de lettres. Les tris en sont rendus plus complexes. Mais heureusement, de nos jours, le codage EBCDIC n’est plus qu’une curiosité historique ; vous risquez peu de le rencontrer dans votre vie aussi bien personnelle que professionnelle.

Extension de l’ASCII

À l’époque où a été définie la convention ASCII, les circuits mémoire étaient très coûteux. C’est pourquoi certaines personnes avaient suggéré de restreindre le codage ASCII à 7 bits, en ayant recours à un caractère d’échappement Shift pour jongler entre majuscules et minuscules, notamment. L’idée a été rapidement rejetée. D’autres pionniers avaient suggéré d’adopter directement le format sur 8 bits parce que l’on pressentait que les ordinateurs allaient sans doute converger vers une largeur de mots de 8 bits plutôt que 7. C’était bien pressenti, puisque les largeurs de 8 bits que sont les octets sont devenus un standard. Pourtant, ASCII reste un codage sur 7 bits au départ, même s’il est stocké dans les machines sous forme de valeurs sur 8 bits.

Cette équivalence entre largeur d’un octet et largeur du codage ASCII est très pratique, parce qu’elle permet d’avoir une idée approximative de la quantité d’espace mémoire requis pour charger un texte simplement en comptant le nombre de caractères. Prenons comme exemple le roman Moby Dick de Herman Melville ; nous constatons qu’il comprend environ 1,25 million de caractères, et va donc demander 1,5 million d’octets d’espace de stockage. Cette mesure permet même de connaître grossièrement le nombre de mots que contient le texte en anglais. Dans cette langue, la longueur moyenne des mots est de cinq caractères. Si l’on y ajoute les espaces entre mots, le roman Moby Dick contient environ 200 000 mots anglais3.

Vous pouvez vous procurer une version en texte simple de Moby Dick sur le site Web libre d’accès du projet Gutenberg (gutenberg.org). Le site regorge de grands ouvrages classiques tombés dans le domaine public. Au départ, le projet ne proposait que des livres au format texte simple, mais dorénavant, on les trouve également dans plusieurs formats plus évolués de type livre électronique, ainsi qu’au format HTML typique du Web.

Justement, le format HTML (HyperText Markup Language), qui est le standard pour les pages Web sur Internet, constitue un format texte enrichi universel. Avec HTML, vous pouvez ajouter des options de mise en forme de votre texte brut en y insérant ce que l’on appelle des balises de marquage (tags). Ce que HTML a de formidable, c’est qu’il utilise des caractères ASCII normaux pour le balisage. Un fichier HTML est d’abord et surtout un fichier texte normal. Lorsque vous le visualisez en tant que fichier texte, le contenu présente à peu près l’aspect suivant :

Voici du texte en <b>gras</b> et voici de l’<i>italique</i>.

Les chevrons gauche et droit correspondent aux codes ASCII 3Ch et 3Eh. Si ce texte est chargé par un logiciel capable d’interpréter le code HTML, par exemple un navigateur Web, vous verrez apparaître le texte sous son aspect enrichi :

Voici du texte en gras et voici de l’italique.

Le texte est le même, mais il est présenté de façon plus riche.

Le standard ASCII a été et reste encore le plus important dans toute l’industrie informatique, mais dès le départ, on savait qu’il avait des faiblesses. Son principal défaut est d’être un standard résolument nord-américain ! Il n’est même pas bien adapté à d’autres pays dont la langue officielle est l’anglais. On trouve, par exemple, un code pour le signe dollar en ASCII, mais aucun pour la livre sterling des Britanniques. C’est encore pire pour les alphabets d’Europe de l’Ouest et de l’Est qui n’utilisent pas l’alphabet latin : le grec, l’arabe, l’hébreu et le cyrillique. Idem pour les écritures indiennes et d’Asie du Sud-Est telles que le devanagari, le bengali, le thaï et le tibétain. Comment pourrait-on imaginer, avec 7 bits, accueillir les dizaines de milliers d’idéogrammes pour satisfaire les Chinois, les Japonais et les Coréens, et ne parlons même pas des plus de 10 000 syllabes de l’ancien alphabet coréen Hangul ?

Il aurait été trop ambitieux au milieu des années 1960 de prévoir d’incorporer toutes les langues du monde dans le standard ASCII. Pour autant, les besoins spécifiques de certains pays ont été pris en compte a minima. Selon le standard ASCII tel que publié, 10 codes ASCII ont été réservés pour être définis par chaque pays utilisateur (40h, 5Bh, 5Ch, 5Dh, 5Eh, 60h, 7Bh, 7Ch, 7Dh et 7Eh). De plus, le signe dièse # peut être remplacé par la livre britannique £ et le signe dollar $ peut être remplacé par le symbole de devise générique (¤). Bien sûr, remplacer des symboles par d’autres n’a d’intérêt que lorsque toutes les personnes concernées par l’utilisation du document connaissent l’existence de ces redéfinitions.

La plupart des systèmes informatiques stockent leurs caractères sur 8 bits, ce qui permet d’envisager une sorte de complément du jeu de caractères ASCII sous la forme d’une extension pour coder 256 caractères au lieu de 128. Dans un tel jeu étendu, les 128 premiers codes (valeurs de 00h à 7Fh) restent les mêmes que ceux du jeu ASCII initial, mais les 128 codes suivants (de 80h à FFh) peuvent être librement définis. Cette technique a été utilisée pour prendre en compte dans un premier temps les lettres accentuées et les alphabets non latins. Le problème est que cette procédure d’extension ASCII a été réalisée plusieurs fois sans coordination et abouti à des extensions non compatibles.

Par exemple, Microsoft Windows a été lancé en proposant une extension de ASCII qui avait été désignée par Microsoft sous le nom ANSI. Pourtant, elle n’avait pas encore été approuvée par l’institut des standards américains ANSI. Les caractères complémentaires pour les codes de A0h à FFh correspondaient à des lettres accentuées et à des symboles utiles en Europe. Dans le tableau suivant, la ligne d’en-tête contient le quartette fort du code de caractère en hexadécimal et la colonne de gauche contient le quartette faible.

A-
B-
C-
D-
E-
F-

- 0

o

À

Ð

à

ð

- 1

¡

±

Á

Ñ

á

ñ

- 2

¢

²

Â

Ò

â

ò

- 3

£

³

Ã

Ó

ã

ó

- 4

¤

´

Ä

Ô

ä

ô

- 5

¥

µ

Å

Õ

å

õ

- 6

¦

¶

Æ

Ö

æ

ö

- 7

§

·

Ç

×

ç

÷

- 8

¨

¸

È

Ø

è

ø

- 9

©

¹

É

Ù

é

ù

- A

ª

º

Ê

Ú

ê

ú

- B

«

»

Ë

Û

ë

û

- C

¼

Ì

Ü

ì

ü

- D

-

½

Í

Ý

í

ý

- E

®

¾

Î

Þ

î

þ

- F

¯

¿

Ï

ß

ï

ÿ

Parmi les nouveaux caractères, le code A0h est celui de l’espace insécable. Normalement, lorsqu’un logiciel provoque un saut de ligne, il choisit l’espace comme point de rupture (césure), celui dont le code ASCII est 20h. Avec le code A0h, ce que l’on voit est un espace, mais cet espace ne peut pas servir pour passer à la ligne suivante. Les espaces insécables sont conseillés, par exemple, pour éviter que le numéro du jour et le mois ne soient sur deux lignes différentes comme dans 2 février.

Le code ADh est un trait d’union conditionnel (on dit « doux »). Il s’agit d’un tiret qui permet de séparer deux syllabes d’un mot pour forcer la césure à cet endroit. Il ne sera visible à l’impression que s’il y a vraiment eu besoin de couper le mot concerné parce qu’il ne tenait pas sur la fin de ligne.

Ce jeu de caractères ANSI s’est répandu d’abord parce qu’il était intégré au système Windows, mais ce n’est qu’une des nombreuses extensions ASCII apparues au cours du temps. Pour se repérer dans la jungle qu’était devenu ASCII, on a décidé d’utiliser des chiffres et des identifiants. C’est ainsi que le jeu de caractères ANSI de Windows est devenu le standard international ISO portant la référence ISO-8859-1, c’est-à-dire l’alphabet latin numéro 1. Ce jeu a lui-même été étendu pour accepter des caractères pour les codes 80h à 9Fh et rebaptisé Windows-1252 :

8-
9-
A-
B-
C-
D-
E-
F-

- 0

€

o

À

Ð

à

ð

- 1

‘

¡

±

Á

Ñ

á

ñ

- 2

‚

’

¢

²

Â

Ò

â

ò

- 3

ƒ

“

£

³

Ã

Ó

ã

ó

- 4

„

¤

´

Ä

Ô

ä

ô

- 5

…

•

¥

µ

Å

Õ

å

õ

- 6

†

–

¦

¶

Æ

Ö

æ

ö

- 7

‡

—

§

·

Ç

×

ç

÷

- 8

ˆ

˜

¨

¸

È

Ø

è

ø

- 9

‰

™

©

¹

É

Ù

é

ù

- A

Š

š

ª

º

Ê

Ú

ê

ú

- B

‹

›

«

»

Ë

Û

ë

û

- C

Œ

œ

¼

Ì

Ü

ì

ü

- D

-

½

Í

Ý

í

ý

- E

Ž

ž

®

¾

Î

Þ

î

þ

- F

Ÿ

¯

¿

Ï

ß

ï

ÿ

La référence 1252 est un identifiant de page de code. Il s’agit d’un terme inventé chez IBM pour gérer plusieurs versions du codage EBCDIC. Chaque pays se voyait attribuer une page de code différente pour répondre à ses besoins de lettres accentuées, voire d’un alphabet spécifique tel que le grec, le cyrillique ou l’arabe. Il fallait connaître l’identifiant de page de code pour afficher correctement les caractères reçus. L’information est devenue indispensable sur Internet, et tout fichier HTML commence par un en-tête qui contient le numéro de page de code permettant de restituer la page Web.

Le codage ASCII a également été étendu de façon plus radicale pour pouvoir accueillir les idéogrammes chinois, japonais et coréens. Un des codages les plus répandus dans ces pays est le Shift – JIS (Japanese Industrial Standard). Il utilise les codes de 81h à 9Fh comme premier octet d’un code sur 2 octets. La convention Shift – JIS permet ainsi de coder plus de 6 000 nouveaux caractères. Le problème est que ce n’est pas la seule norme qui utilise cette technique. Il existe en Asie trois autres standards courants qui emploient des jeux de caractères sur 2 octets appelés DBCS (Double Byte Character Set).

Ces conflits entre plusieurs jeux de caractères sur 2 octets ne sont qu’un des nombreux problèmes qui se posent. Un autre est que certains caractères, notamment les caractères ASCII de base, sont codés sur 1 octet pendant que les idéogrammes sont codés sur 2. Cela rend l’utilisation de données utilisant ce codage beaucoup plus difficile.

Tout ce que nous venons de présenter peut vous laisser croire que nous sommes dans un vrai capharnaüm. Sachez que nous ne sommes pas les seuls. Est-ce que quelqu’un aurait la gentillesse de nous proposer une solution ?

Le grand jeu : Unicode

Plusieurs des plus importantes entreprises informatiques se sont réunies en 1988 pour tenter de définir un système de codage universel, capable de répondre aux besoins de toutes les langues du monde, actuelles et passées. Ce système a été baptisé Unicode. Alors que ASCII demande 7 bits, Unicode en demande 16 dès sa première version (et ce n’était qu’un début). Dans sa première mouture, chaque caractère Unicode devait être codé sur 2 octets, donc avec une valeur entre 0000h et FFFFh, ce qui offre un espace pour 65 536 symboles différents. On avait pensé à cette époque que cela suffirait à toutes les langues du monde qui auraient besoin d’une forme électronique, et on laissait même un espace en réserve.

Unicode n’est pas parti de rien. Les 128 premiers caractères Unicode de 0000h à 007Fh sont strictement les mêmes que dans le jeu ASCII de base. En poursuivant, les codes Unicode de 00A0h à 00FFh restent les mêmes que dans l’extension d’alphabet latin numéro 1 décrite précédemment. D’autres standards ayant eu une acceptation mondiale ont été repris dans Unicode.

Chaque code Unicode est une valeur hexadécimale. Mais la convention établie consiste à noter la valeur en ajoutant en préfixe une lettre U suivie d’un signe +. Voici la représentation de quelques caractères Unicode :

Code hexa
Caractère
Description

U+0041

A

Lettre capitale A

U+00A3

£

Signe livre (pound)

U+03C0

π

Lettre Pi minuscule du grec

U+0416

Ж

Capitale Zhe du cyrillique

U+05D0

א

Lettre Aleph de l’hébreu

U+0BEB

௫

Chiffre 5 en tamil

U+2018

‘

Apostrophe courbe gauche

U+2019

’

Apostrophe courbe droite

U+20AC

€

Signe euro

U+221E

∞

Infini

N’hésitez pas à visiter le site Web officiel du consortium Unicode à l’adresse unicode.org. Vous découvrirez ainsi la richesse des langues du monde et des symboles correspondants. Dans le bas de la page d’accueil, vous trouverez un lien Code Charts qui donne accès à des dessins de caractères absolument bouleversants.

Le passage d’un code sur 8 bits à un code sur 16 bits est problématique : d’un ordinateur à l’autre, la lecture d’une valeur sur 16 bits n’est pas faite dans le même sens. Prenons comme exemple les octets suivants :

20h ACh

Certains ordinateurs lisent cette séquence sur 2 octets telle qu’elle se présente, c’est-à-dire 20ACh, qui est le code Unicode du symbole de l’euro. Les ordinateurs qui travaillent ainsi sont des machines gros-boutistes. Cet étrange qualificatif signifie que l’octet parmi les deux qui est le plus significatif, l’octet de poids fort, est lu en premier. D’autres machines sont petit-boutistes. Précisons que le boutisme est inspiré des Voyages de Gulliver, dans lequel Jonathan Swift relate une guerre qui se déclenche à propos du bout de l’œuf dur par lequel il faut l’écaler. Sur une machine petit-boutiste, la valeur est lue en inversant les deux octets, donc AC20h, qui en Unicode est le code d’un caractère de l’alphabet coréen Hangul ().

La norme Unicode contourne cette source de problèmes en stipulant un caractère spécial, le caractère de marquage de l’ordre des octets BOM (Byte Order Marker). Ce code spécial possède la valeur U+FEFF. Il doit être placé au tout début d’un fichier contenant des valeurs Unicode sur 16 bits. Si les deux premiers octets du fichier sont FEh puis FFh, c’est que le fichier a été stocké dans l’ordre gros-boutiste. S’ils sont intervertis, le fichier est stocké dans l’ordre petit-boutiste.

À l’époque où Unicode a commencé à se répandre (milieu des années 1990), il est devenu évident qu’il fallait plus de 16 bits pour pouvoir accueillir les signes des nombreuses langues qui, bien que mortes, sont réclamées par les travaux historiques. De nombreux nouveaux symboles sont également apparus récemment. Certains connaissent un succès énorme puisqu’il s’agit des émojis.

Au moment où j’écris ce livre (en 2021), Unicode est devenu encore plus accueillant puisqu’il se code sur 21 bits, les codes de valeur pouvant aller jusqu’à 10FFFF, soit assez pour plus d’un million de signes différents. Voici quelques caractères qui ne pouvaient pas être représentés sur 16 bits (parce que tous les codes possibles étaient déjà affectés) :

Code hexa
Caractère
Description

U+1302C

Hiéroglyphe égyptien A039

U+1F025

Tuile Chrysanthème du Mahjong

U+1F3BB

Violon

U+1F47D

Alien

U+1F614

Visage pensif

U+1F639

Chat pleurant de joie

Faire de la place dans la palette des possibilités Unicode pour coder des signes aussi peu sérieux que des émojis peut sembler léger. Cela ne l’est plus si l’on réfléchit aux incompréhensions ou même vexations qui peuvent se produire lorsqu’un émoji est interprété différemment et sous un aspect offensant sur la machine du destinataire. Cela pourrait endommager les bonnes relations entre les humains.

D’ailleurs, les besoins des gens par rapport à Unicode sont variables. Unicode est bien sûr indispensable pour gérer les idéogrammes des langues orientales. Néanmoins, un volume conséquent de documents et de pages Web peut tout à fait se contenter de la plage ASCII de base. C’est pour cette raison que plusieurs méthodes ont été définies pour le stockage et le transport des textes Unicode. On parle dans ce cas de format de transformation Unicode, UTF (Unicode Transformation Format).

Le plus évident des formats de transformation est UTF-32. Dans ce format, tous les caractères sont codés sur 32 bits. Chaque caractère occupe donc 4 octets qui peuvent être stockés dans l’un des deux ordres gros-boutisme ou petit-boutisme.

L’inconvénient de UTF-32 est son énorme gabegie d’espace. Un fichier texte contenant la version originale de Moby Dick voit sa taille passer de 1,25 million d’octets en ASCII à 5 millions d’octets en Unicode 32 bits. Et cela sans tenir compte du fait que Unicode ne va employer que 21 de ces 32 bits, laissant donc 11 bits inutilisés pour chaque caractère !

Un premier compromis a été trouvé avec UTF-16. Dans ce format, la plupart des caractères occidentaux (notamment) sont définis sur 2 octets, et tous ceux dont le code a une valeur supérieure à U+FFFF le sont sur 4 octets. La plage de valeurs entre U+D800 et U+DFFF avait été prévue à cette intention dès le début de la spécification Unicode.

Le format de transformation Unicode le plus répandu est UTF-8. C’est le plus utilisé sur Internet. Des statistiques récentes montrent que 97 % de toutes les pages Web sont stockées dans ce format UTF-8, ce qui permet de le considérer comme un standard universel de fait. Les fichiers de texte simple du projet Gutenberg sont tous stockés dans ce format UTF-8. Les outils Bloc-notes de Windows et TextEdit de macOS stockent leurs fichiers dans ce format par défaut.

En effet, UTF-8 propose un compromis entre souplesse et compacité. Son principal avantage est de maintenir une compatibilité avec le bon vieux ASCII des origines. Un fichier qui ne contient que des codes de caractères ASCII sur 7 bits est automatiquement aussi un fichier UTF-8.

Pour que cette compatibilité fonctionne, tous les autres caractères Unicode sont stockés sur 2, 3 ou 4 octets, en fonction des besoins. Le tableau suivant présente les règles de codification UTF-8 :

Plage Unicode
Nb de bits
Séquence d’octets

U+0000 à U+007F

7

0xxxxxxx

U+0080 à U+07FF

11

110xxxxx 10xxxxxx

U+0800 à U+FFFF

16

1110xxxx 10xxxxxx 10xxxxxx

U+10000 à U+10FFFF

21

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Dans chaque plage Unicode de la première colonne, les caractères sont codés avec le nombre de bits indiqué dans la deuxième colonne. Cette « charge utile » est préfixée par un masque de 1 et de 0 selon le schéma montré dans la troisième colonne. On obtient ainsi une séquence de 1 à 4 octets. Le nombre de x dans la troisième colonne correspond au nombre de bits de la charge utile dans le deuxième code.

La première plage (première ligne) concerne les caractères connus depuis les origines du codage ASCII sur 7 bits. Le codage UTF-8 de ces caractères est constitué d’un zéro suivi des 7 bits du caractère ASCII.

Tous les caractères dont la valeur Unicode est supérieure ou égale à U+0080 vont consommer au moins 2 octets. Par exemple, le signe de la livre britannique £ possède la valeur Unicode +U00A3. Cette valeur est supérieure à 0080 mais inférieure à 07FF. La deuxième plage du tableau nous indique qu’en UTF-8, elle doit être codée sur 2 octets avec 11 bits dédiés. On ne conserve que les 11 bits de poids faible du code initial pour aboutir à un code sur 2 octets selon le mécanisme suivant :

La valeur encodée est indiquée en haut, 00A3. Chacun des quatre chiffres hexa est rendu par une valeur sur 4 bits juste sous le chiffre. Puisque la valeur est inférieure ou égale à 07FFh, nous sommes certains que les 5 bits de poids fort seront égaux à 0 et peuvent donc être oubliés. Les 5 bits suivants sont préfixés par le masque 110 (ce que l’on voit dans le bas du schéma). On obtient ainsi l’octet valant C2h. Les 6 bits de poids faible sont à leur tour préfixés, mais par la valeur 10, ce qui produit le second octet de valeur A3h.

Le résultat est que le codage UTF-8 de la livre britannique correspond aux 2 octets C2h et A3h. Vous pourriez trouver absurde de consommer 2 octets pour coder 1 octet d’information, mais c’est sous ces conditions que la totalité de UTF-8 peut fonctionner.

Prenons un autre exemple : nous partons de la lettre Aleph de l’hébreu (א), dont le code Unicode est U+05D0. Cette valeur reste dans la même deuxième plage (0080 à 07FF) et le processus reste le même qu’avec le signe livre :

Ici aussi, les 5 bis de poids fort de 05D0h peuvent être oubliés. Nous ajoutons 110 en préfixe des 5 bits suivant puis nous ajoutons 10 en préfixe des 6 derniers bits. On obtient ainsi les octets D7h et 90h.

L’émoji d’un chat pleurant de joie est assez improbable, mais il est prévu, Unicode lui ayant attribué la valeur U+1F639. Cette valeur énorme vous laisse deviner qu’il va falloir 4 octets en UTF-8. Le diagramme suivant montre comment ces octets sont construits à partir des 21 bits du code de ce caractère :

En ayant choisi une approche à nombre d’octets variables, UTF-8 a perturbé la clarté et la pureté initiales de Unicode. Par le passé, ce genre de convention créait des problèmes en relation avec le code ASCII normal. UTF-8 n’est pas une solution magique. Mais le standard a été défini de façon tout à fait intelligente. Lors du décodage d’un fichier au format UTF-8, on peut identifier assez précisément chacun des octets :

Si l’octet commence par 0, c’est tout simplement le code d’un caractère en ASCII simple sur 7 bits.

Si l’octet commence par 10, il fait partie d’une séquence sur plusieurs octets, mais ce n’est pas le premier octet de la séquence.

Sinon, l’octet commence nécessairement par au moins 2 bits à 1, 11, et c’est le premier octet d’un code multi-octets. Le nombre d’octets du caractère correspondant peut être connu en comptant le nombre de bits à 1 dans le début de ce premier octet, avant de rencontrer le premier bit à 0. Ce nombre peut être égal à 2, 3 ou 4.

Tentons une dernière conversion UTF-8 ; choisissons celle du caractère apostrophe fermante (Right Single Quotation Mark). La valeur Unicode est U+2019. Comme cette valeur se situe entre 0800h et FFFFh, nous utilisons la troisième ligne du tableau. Voici la représentation UTF-8 sur 3 octets :

Dans cet exemple, les 16 bits de la valeur Unicode de départ sont conservés pour construire les 3 octets. Pour les 4 premiers bits, nous ajoutons en préfixe la valeur 1110. Nous préfixons avec 10 les 6 bits suivants puis encore avec 10 les 6 derniers. Nous obtenons une séquence sur 3 octets : E2h, 80h et 99h.

Ce dernier exercice avait pour but de répondre au problème soulevé en début de chapitre à propos de la mauvaise interprétation d’une phrase dans un courriel :

Lâ€™acompte nous est bien parvenu, merci.

On devine assez aisément que le premier mot aurait dû se lire « L’acompte ». Le problème est que l’émetteur n’a pas utilisé l’apostrophe droite dont le code ASCII est 027h (Unicode U+0027h). Il a utilisé l’apostrophe typographique courbe de fermeture codée sur les 3 octets E2h, 80h et 99h, comme nous venons de le voir.

De prime abord, il n’y a pas de problème sauf quand le fichier HTML du courrier électronique reçu déclare utiliser le jeu de caractères « windows-1252 ». Il aurait fallu déclarer « UTF-8 », puisque c’est le codage réellement utilisé par le texte. En indiquant « windows-1252 », le logiciel lecteur des courriels sur la machine destinataire a sélectionné ce jeu de caractères pour l’appliquer au contenu, et a donc mal interprété ces 3 octets. Si vous consultez le tableau des codes du jeu Windows-1252 (un peu plus haut dans ce chapitre), vous verrez que les 3 octets 02h, 80h et 99h correspondent bien individuellement aux trois caractères â, € et ™ qui ont été affichés.

Le mystère est donc résolu.

En rendant l’informatique capable d’offrir une expérience universelle et multiculturelle, le standard Unicode a pris une importance énorme. Mais comme toute chose en ce bas monde, cela ne fonctionne correctement qu’à condition de l’utiliser correctement.

1 N.d.T. : ces codes sont tellement peu usités que je ne vais même pas en proposer un équivalent en français. Seuls ceux en gras servent fréquemment et nous les avons déjà présentés.

2 N.d.T. : les trois codes CR, LF et Tab sont souvent les seuls que les programmeurs connaissent et ont besoin de connaître.

3 N.d.T. : en français, la longueur moyenne de mots se situe entre six et sept caractères, mais cela ne change rien au raisonnement.

Chapitre 14  Additions et portes logiques

La plus simple des opérations arithmétiques est bien l’addition. Si nous envisageons de construire un ordinateur (et c’est mon objectif clairement affiché pour ce livre), il nous faut commencer par découvrir comment on peut concevoir un appareillage capable d’additionner deux nombres. Au sens le plus strict, l’addition est la seule opération qu’un ordinateur sait faire. À partir du moment où nous savons faire additionner par une machine, nous pouvons envisager de nous servir de cette capacité pour effectuer des soustractions, des multiplications, des divisions, des calculs de mensualités d’emprunt, des pilotages de fusées martiennes, des partenaires de jeu d’échecs ou des logiciels de réseaux sociaux avec lesquels nous partageons nos dernières trouvailles en matière de danse, de recettes culinaires et les aventures de nos compagnons quadrupèdes.

La machine à additionner que nous allons concevoir dans ce chapitre va être volumineuse, lente et bruyante, en comparaison des ordinateurs modernes. Ce qui est intéressant, c’est que nous allons construire cette machine uniquement en nous servant des composants électriques rencontrés dans le chapitre précédent : des interrupteurs, des lampes, des bouts de fils, une pile et des relais précâblés sous forme de portes logiques. Tous ces composants étaient disponibles avant le XXe siècle. Un autre avantage de notre projet est qu’il n’est même pas nécessaire de perdre du temps à le fabriquer matériellement dans nos cuisines ; nous allons concevoir la machine sur papier et pouvoir vérifier qu’elle est opérationnelle.

Cette machine va fonctionner seulement avec des nombres binaires, et ne sera pas dotée de tout le confort moderne. Vous n’aurez pas de clavier pour saisir les valeurs à additionner : vous devrez les configurer sur une série d’interrupteurs. Vous n’aurez pas non plus d’affichage numérique des résultats : vous devrez décoder la valeur à partir d’une série de lampes.

Malgré ses faiblesses, cette machine va être vraiment capable d’additionner deux nombres, et elle va procéder de façon très proche de ce que fait un ordinateur.

L’addition de valeurs binaires ressemble beaucoup à celle de valeurs décimales. Lorsque vous additionnez deux valeurs décimales telles que 245 et 673, vous décomposez le problème en étapes. Dans chaque étape, vous vous limitez à l’addition de deux chiffres. Dans notre exemple, nous faisons d’abord l’addition 5 + 3. Le problème est évidemment facilité si vous n’avez pas oublié les tables d’addition apprises pendant votre enfance.

L’énorme avantage des nombres binaires est la simplicité de la table d’addition :

+

0

1

0

0

1

1

1

10

Si vous aviez été élevé par des dauphins, vous auriez sans doute appris cette table à l’école des dauphins, et vous auriez mémorisé la table à l’aide d’une comptine, par exemple :

0 plus 0 égale 0.

0 plus 1 égale 1.

1 plus 0 égale 1.

1 plus 1 égale 0, avec 1 de retenue.

Pour que chaque résultat soit présenté sur 2 bits, nous pouvons reformuler la table d’addition en ajoutant des zéros en préfixe :

+

0

1

0

00

01

1

01

10

Cette présentation permet de bien voir que le fait d’additionner deux nombres binaires produit 2 bits, un bit de somme (comme attendu) et un bit de retenue Carry1.

Nous pouvons maintenant répartir cette table d’addition dans deux tables, la première pour le bit de somme :

Sum

0

1

0

0

1

1

1

0

La seconde table sert au bit de retenue Carry (lisez-la lentement) :

Carry

0

1

0

0

0

1

0

1

Le fait de décomposer l’addition binaire nous permet de bien comprendre comment fonctionne la machine. Nous allons concevoir un circuit qui réalise les opérations de ces deux tables. Notre problème est beaucoup plus simple en binaire parce que tous les éléments du circuit correspondent à des chiffres binaires : les interrupteurs, les lampes, les fils et les piles.

Comme dans le cas d’une addition décimale, l’addition de deux valeurs binaires est réalisée en progressant colonne par colonne et en commençant par la colonne du bit de poids faible, le moins significatif, la colonne de droite :

Vous constatez que l’addition dans la troisième colonne fait naître une retenue qui est propagée dans la colonne suivante. Une nouvelle retenue apparaît ensuite dans la sixième, la septième et la huitième colonne.

Quelle largeur de valeur binaire voulons-nous pouvoir gérer ? Puisque nous n’allons faire que concevoir la machine sans la construire matériellement, nous pourrions choisir de gérer des nombres très grands. Mais ne nous emportons pas. Optons pour des valeurs binaires sur 8 bits, soit 1 octet. Nous pourrons ainsi additionner des valeurs entre 0000 0000 et 1111 1111. En hexadécimal, ce sont les valeurs 00h à FFh et, en décimal, de 0 à 255. Lorsque l’on additionne deux nombres sur 8 bits, le résultat s’élève au plus à 510d (décimal), soit 1FEh ou 10 en binaire.

Voici à quoi pourrait ressembler le panneau de commande de notre additionneur :

Nous voyons deux séries de huit interrupteurs : ils constituent le périphérique d’entrée. En positionnant ces interrupteurs, nous allons encoder les deux nombres sur 8 bits. Bien sûr, un interrupteur baissé définit un bit à 0. Le bit le moins significatif (le bit de poids faible) se trouve du côté droit et celui de poids fort, du côté gauche. Le périphérique de sortie est incarné par les neuf lampes en bas. Une lampe éteinte correspond à 0 et une lampe allumée, à 1. Notez qu’il nous faut neuf lampes parce que la somme de deux nombres sur 8 bits peut être un nombre sur 9 bits. La lampe tout à fait à gauche ne sera allumée que si le résultat est supérieur à la valeur décimale 255.

Derrière ce panneau de commande, nous trouverons un savant assemblage de portes logiques. Les interrupteurs vont contrôler les relais qui vont contrôler les lampes. Pour additionner, par exemple, les deux valeurs binaires 0110 0101 et 1011 0110 (les deux valeurs de l’exemple précédent), nous configurons les interrupteurs d’entrée comme ceci :

En lisant les états des lampes en bas, on découvre que la réponse est 1 0001 1011 (en espérant que c’est ce qui se produira lorsque nous aurons construit cet additionneur !).

Je vous avais prévenu dans un précédent chapitre que j’allais avoir besoin d’un grand nombre de relais dans la suite. En ce qui concerne la machine à additionner sur 8 bits que nous allons créer dans ce chapitre, il nous faudra 144 relais, soit 18 pour chaque couple de bits à additionner. Si je vous montrais le schéma électrique complet du résultat, vous risqueriez de prendre peur. En effet, comment pourriez-vous vous y retrouver sans explications dans un labyrinthe interconnectant 144 relais de façon obscure ? Pour simplifier les choses, je vais décrire le problème de façon progressive.

Vous avez peut-être déjà remarqué qu’il y avait un rapport entre les portes logiques et l’addition binaire en découvrant la table du bit de retenue Carry2 de l’addition de deux valeurs sur 8 bits :

Carry

0

1

0

0

0

1

0

1

Vous avez peut-être remarqué que la table était la même que celle de l’opération logique AND, ET. Rappelons la table de vérité de la porte AND vue dans le Chapitre 8 :

AND

0

1

0

0

0

1

0

1

La revoici en distinguant les deux entrées :

A

B

AND

0

0

0

0

1

0

1

0

0

1

1

1

Pour ne pas avoir à dessiner toute une ribambelle de relais, les électroniciens ont adopté un symbole pour la porte AND :

Les entrées A In et B In sont à gauche et correspondent aux deux bits à additionner. La sortie Out fournit le bit de retenue Carry pour l’addition des deux chiffres binaires.

Nous faisons donc des progrès. La mécanisation de l’autre calcul, celui de la somme, est moins simple :

Sum

0

1

0

0

1

1

1

0

C’est l’autre calcul à réaliser lors de l’addition de deux chiffres binaires. Le bit de somme Sum n’est pas aussi simple à produire que le bit de retenue, mais nous allons y arriver.

Nous constatons d’abord que l’opération demandée ressemble à l’opération logique OR, sauf dans le cas où les deux bits sont à 1 (case de l’angle inférieur droit) :

OR

0

1

0

0

1

1

1

1

Rappelons qu’au Chapitre 8, nous avions montré qu’une porte logique OR était symbolisée de la façon suivante :

Une autre porte dont les résultats ressemblent à ce que nous cherchons est la porte NAND (une porte AND suivie d’un inverseur NOT). Sa sortie donne le résultat opposé à celui d’une porte AND3. La table de résultats est la même que celle que nous cherchons pour le bide de somme sauf lorsque les deux entrées sont à 0 (angle supérieur gauche) :

NAND

0

1

0

1

1

1

1

0

Voici le symbole d’une porte NAND :

Rappelons que c’est une porte AND avec un inverseur en sortie symbolisé par le petit rond sur la sortie de la porte logique.

Si nous connectons en parallèle une porte OR et une porte NAND aux deux mêmes entrées, nous obtenons le résultat désiré. Comme dans les circuits précédents, il n’y a connexion entre deux fils que lorsqu’il y a un point de soudure :

La grande table suivante indique l’état des sorties des deux portes OR et NAND, avec une comparaison par rapport aux résultats désirés pour notre additionneur :

A In
B In
OR Out
NAND Out
But visé

0

0

0

1

0

0

1

1

1

1

1

0

1

1

1

1

1

1

0

0

À la lecture des colonnes des sorties Out des deux portes, nous constatons que ce que nous désirons est une sortie à 1 lorsque les deux portes ont leur sortie à 1. Pourquoi ne pas connecter les deux portes en entrée d’une porte AND ?

Le circuit est devenu plus complexe, mais il continue à n’avoir que deux entrées et une sortie. Les deux entrées alimentent les deux portes OR et NAND ; la sortie de ces deux portes alimente la porte AND. Le résultat est celui attendu :

A In
B In
OR Out
NAND Out
AND Out (Sum)

0

0

0

1

0

0

1

1

1

1

1

0

1

1

1

1

1

1

0

0

Ce montage constitué de trois portes correspond à un circuit qui porte le nom OU exclusif, abrégé en XOR (en français, on prononce « KSOR »). Il s’agit d’un OU exclusif parce que la sortie est à 1 seulement si les deux sorties sont dans un état différent. Autrement dit, une seule doit être à 1. Nous évitons dans la suite de devoir dessiner les trois portes en adoptant le symbole qui a été trouvé par les électroniciens pour la porte XOR :

Le symbole ressemble beaucoup à celui de la porte OR auquel on aurait ajouté une ligne incurvée du côté des entrées. Voici le tableau de vérité de la porte XOR :

XOR

0

1

0

0

1

1

1

0

La porte XOR sera la dernière que je vais utiliser dans ce livre. Même s’il existe aussi une porte que l’on rencontre bien moins fréquemment, la porte de coïncidence. Sa sortie n’est à 1 que si les deux entrées sont dans le même état. Autrement dit, son comportement est l’inverse de celui d’une porte XOR. D’ailleurs, son symbole est le même, mais avec un petit cercle d’inverseur en sortie.

Récapitulons ce que nous avons appris jusqu’ici. Lorsque l’on additionne deux nombres binaires, on récupère un bit de somme et un bit de retenue Carry :

Sum

0

1

0

0

1

1

1

0

Carry

0

1

0

0

0

1

0

1

Pour obtenir ces deux bits, nous pouvons exploiter les deux portes logiques suivantes :

XOR

0

1

0

0

1

1

1

0

AND

0

1

0

0

0

1

0

1

La somme des deux nombres binaires s’obtient sur la sortie d’une porte XOR. La retenue éventuelle est fournie en sortie d’une porte AND. Nous pouvons donc combiner les deux portes pour prendre en charge l’addition de deux chiffres binaires portant les noms A et B :

Je rappelle que ce schéma est une simplification ! La porte XOR est en réalité la combinaison d’une porte OR, d’une porte NAND et d’une porte AND, chaque porte utilisant deux relais. On facilite la compréhension en masquant les détails. C’est un processus d’encapsulation : un assemblage complexe est simplifié. À tout moment, nous pouvons accéder aux détails si le besoin s’en fait sentir.

Profitons-en pour faire une autre encapsulation : au lieu de redessiner sans cesse la porte XOR et la porte AND, dessinons une boîte carrée en lui donnant le nom demi-additionneur :

Les mentions S et CO désignent la somme Sum Out et la retenue Carry Out. Ce genre de boîte de simplification est parfois appelé boîte noire. Le schéma montre les entrées à combiner et les sorties, mais le contenu reste caché. Dans notre cas, puisque nous savons ce qu’il y a à l’intérieur de notre boîte, il s’agit plutôt d’une boîte blanche (clear box).

Pourquoi n’est-ce qu’un demi-additionneur ? Bien sûr, le circuit sait additionner deux bits en fournissant un bit de somme et un bit de retenue. Mais ce circuit ne permet d’additionner que les deux chiffres de poids le plus faible d’une valeur qui en comporte plusieurs. Il n’est pas capable d’exploiter l’éventuelle retenue d’une addition de bits précédente. Supposons l’addition entre deux valeurs binaires suivante :

Notre demi-additionneur ne peut être utilisé que pour les deux bits de la colonne la plus à droite : 1 + 1 égale 0 avec une retenue de 1. Dans la deuxième colonne, nous avons besoin d’additionner trois bits, les deux bits des valeurs et le bit de retenue Carry. Il en va de même pour toutes les colonnes suivantes. Les additions de deux valeurs binaires doivent tenir compte du bit de retenue de la colonne précédente.

Pour réussir cette addition entre trois nombres binaires, nous devons réunir deux demi-additionneurs et une porte OR selon le schéma suivant :

Figure 14.1

Détaillons le fonctionnement de ce montage. Nous commençons avec les deux entrées A et B du premier demi-additionneur. Sur sa sortie, nous trouvons une somme et une retenue. La somme doit être additionnée avec la retenue de la colonne de chiffres précédente, Carry In. Cette entrée de retenue et la somme du premier demi-additionneur sont présentées en entrée du second demi-additionneur dont la sortie Sum Out est le résultat final.

Parallèlement, les deux sorties de retenue Carry Out des deux demi-additionneurs sont présentées en entrée d’un circuit OR dont la sortie donne la retenue à propager Carry Out. En théorie, on pourrait croire qu’il faudrait utiliser ici un autre demi-additionneur, et cela fonctionnerait effectivement. Mais si vous envisagez toutes les possibilités, vous constaterez que jamais les sorties de retenue Carry Out des deux demi-additionneurs ne peuvent être dans le même état en même temps. Voilà pourquoi nous pouvons utiliser une simple porte OR parce que ses résultats sont les mêmes que ceux d’une porte XOR lorsque les entrées ne sont jamais à 1 en même temps.

Simplifions ce circuit sous la forme d’une boîte qui va incarner un additionnel complet :

Figure 14.2

La table suivante montre toutes les combinaisons possibles d’entrée pour l’additionneur complet avec les sorties correspondantes :

A In
B In
Carry In
Sum Out
Carry Out

0

0

0

0

0

0

1

0

1

0

1

0

0

1

0

1

1

0

0

1

0

0

1

1

0

0

1

1

0

1

1

0

1

0

1

1

1

1

1

1

J’ai indiqué précédemment qu’il fallait 144 relais pour construire un additionneur binaire sur 8 bits. Voici comment je suis parvenu à ce chiffre : chaque porte AND, OR et NAND consomme 2 relais. Une porte XOR en réclame donc 6. Un demi-additionneur réunit une porte XOR et une porte AND ; il demande 8 relais. Chaque additionneur complet réunit deux demi-additionneurs et une porte OR, soit 18 relais. Du fait qu’il nous faut huit additionneurs complets, nous arrivons à 144.

Rappelons le panneau de commande avec ses interrupteurs et ses lampes :

Nous pouvons maintenant envisager de relier ces interrupteurs et ces lampes à nos additionneurs complets.

Nous commençons par le bit de poids faible : nous connectons les deux interrupteurs à l’extrémité droite et la lampe de droite à un premier additionnel complet :

Figure 14.3

Lorsque vous additionnez deux valeurs binaires, la première colonne de chiffres est différente des autres, parce qu’elle ne peut pas avoir de retenue propagée depuis une position plus basse. C’est pour cette raison que l’entrée de retenue CI de ce premier étage est reliée à la masse, c’est-à-dire à l’équivalent d’un bit à 0. L’addition des deux premiers chiffres binaires peut en revanche résulter en un bit de retenue. Cette sortie Carry Out va être reliée à l’entrée de retenue Carry In de l’étage suivant.

Pour l’addition des deux chiffres suivants, le branchement de la prochaine lampe se fait de la façon suivante avec un autre additionnel complet :

Figure 14.4

La sortie de retenue CO du premier additionneur sert d’entrée de retenue au suivant. Toutes les colonnes de chiffres suivantes sont câblées de la même façon avec propagation de la sortie de retenue d’une position sur l’entrée de retenue de la suivante.

Pour la dernière position (les deux derniers interrupteurs et les deux lampes tout à fait à gauche), le branchement est un peu différent :

Figure 14.5

La sortie de retenue CO de ce dernier étage est directement branchée sur la neuvième lampe.

Et notre projet est terminé.

Voici une vision synthétique de l’assemblage des huit additionneurs complets (FA pour Full Adder). On voit que chaque sortie de retenue CO est branchée sur l’entrée de retenue CI suivante :

Figure 14.6

Les additionneurs sont présentés ici dans le même ordre que les interrupteurs et les lampes du panneau de commande : les bits de poids faible sont à l’extrémité droite et ceux de poids fort à l’extrémité gauche, dans le sens d’écriture des chiffres. Notez bien l’injection de la sortie CO dans l’entrée CI de l’étage suivant. La première entrée de retenue CI est forcée à l’état 0 par branchement à la masse. La dernière sortie de retenue CO alimente directement la neuvième lampe.

Voici le même additionneur sur 8 bits sous la forme d’une boîte simplificatrice. Les entrées portent les noms A₀ à A₇ et B₀ à B₇ ; les sorties sont désignées par S₀ à S₇ (S signifiant « somme », bien sûr) :

Figure 14.7

La notation avec indices est courante pour désigner les constituants d’un nombre multibits. Les bits A₀, B₀ et S₀ sont les moins significatifs et les bits A₇, B₇ et S₇ sont les bits de poids fort. Voici comment ces lettres indices correspondent aux bits de la valeur exemple 0110 1001 :

A₇

A₆

A₅

A₄

A₃

A₂

A₁

A₀

0

1

1

0

1

0

0

1

Vous notez que les indices commencent à 0 et croissent exactement comme les puissances de 2 correspondantes:

2⁷

2⁶

2⁵

2⁴

2³

2²

2¹

2⁰

0

1

1

0

1

0

0

1

Si vous multipliez chaque puissance de 2 par le numéro de rang du bit de la colonne, puis additionnez toutes les valeurs, vous obtenez directement la valeur décimale qui équivaut à la valeur 0110 1001 : 64 + 32 + 8 + 1 = 105.

Un additionneur sur 8 bits peut être présenté de façon encore plus synthétique :

Figure 14.8

Les flèches larges comportent le chiffre 8 pour rappeler qu’elles représentent de façon collective huit signaux séparés. Ce sont des chemins de données sur 1 octet de largeur. Les mentions à indice des signaux élémentaires sont conservés.

Dès que vous savez construire un additionneur sur 8 bits, vous pouvez en construire un second et les interconnecter en cascade, afin d’obtenir un additionneur sur 16 bits :

Figure 14.9

Les deux valeurs d’entrée sur 16 bits sont réparties en 2 octets, un de poids faible et un de poids fort. La sortie de retenue CO de l’additionneur de droite est reliée à l’entrée de retenue CI de l’autre. Ce second circuit reçoit en entrée les deux fois 8 bits les plus significatifs et produit en sortie les 8 bits les plus significatifs du résultat.

Vous pouvez maintenant vous demander si c’est vraiment ainsi que les ordinateurs réalisent leurs additions.

C’est une excellente question.

1 N.d.T. : on prononce « carie », mais cela n’a rien de dentaire. Le mot anglais vient de charrier, donc transporter, et la retenue l’est d’une colonne à la suivante.

2 N.d.T. : nous conserverons le terme anglais Carry pour la retenue (mais cela n’a aucune connotation dentaire).

3 N.d.T. : dans la suite, nous conserverons les acronymes anglais pour les différentes portes logiques : AND, OR, NAND et, celle que nous allons découvrir bientôt, XOR.

Chapitre 15  Des sables qui comptent

Nous avons vu dans le chapitre précédent comment interconnecter des relais pour créer un additionneur 1 bit, puis comment en combiner huit pour additionner 2 octets. Nous avions même envisagé de connecter plusieurs additionneurs 8 bits en cascade pour traiter des nombres de plus en plus grands. Mais vous étiez sans doute resté interrogatif : est-ce de cette façon que les ordinateurs comptent ?

La réponse est normande : oui et non, d’abord parce que les ordinateurs ne sont pas basés sur des relais, même s’ils l’ont été dans un premier temps.

En novembre 1937, un chercheur des laboratoires Bell, George Stibitz (1904-1995), avait rapporté chez lui une poignée de relais pour ses recherches sur les centraux téléphoniques. Il les avait interconnectés sur sa table de cuisine en y ajoutant une pile, deux lampes et deux interrupteurs bricolés à partir de bandes métalliques découpées dans une boîte de conserve. Il avait ainsi créé un additionneur 1 bit, comme celui vu dans le chapitre précédent. Il avait baptisé son prototype « modèle K » tout simplement parce qu’il l’avait construit sur sa table de Kuisine.

Ce modèle K était devenu ce que l’on appelle une preuve de faisabilité ou de concept (Proof of Concept) : il était possible de faire faire des opérations arithmétiques à des relais. Les dirigeants de Bell Labs avaient alors autorisé une équipe à poursuivre dans cette voie et, en 1940, on avait réussi à mettre au point la machine nommée Complex Number Computer. Cette machine, qui utilisait plus de 400 relais, était destinée à multiplier des nombres complexes. Rappelons qu’un nombre complexe est constitué d’une partie réelle et d’une partie imaginaire, la partie imaginaire correspondant à la racine carrée d’un nombre négatif. Ces nombres sont indispensables dans les domaines scientifiques et l’ingénierie. Pour multiplier deux nombres complexes, il faut réaliser quatre multiplications et deux additions. Le Complex Number Computer savait gérer de tels nombres jusqu’à huit chiffres après la virgule. Il lui fallait environ une minute pour une opération.

Cette machine n’était pas la première à avoir été basée sur des relais. Chronologiquement, c’est Conrad Zuse (1910-1995) qui, le premier, avait créé une machine à calculer, fondée sur des relais. En 1935, alors qu’il était étudiant ingénieur, il s’était lancé dans la construction d’une machine dans l’appartement de ses parents à Berlin. Baptisée Z1, elle n’utilisait pas de relais, mais en simulait de façon mécanique. La machine suivante, la Z2, adoptait enfin les relais. Pour la programmer, on effectuait des perforations sur un support de type film de cinéma 35 mm.

Vers 1937, l’étudiant en master à Harvard nommé Howard Aiken (1901-1973) cherchait à réaliser un grand nombre de calculs répétitifs. Une collaboration entre Harvard et la société IBM en a résulté, produisant le calculateur contrôlé par séquence automatique ASCC (Automated Sequence Controlled Calculator). La machine, achevée en 1943, a été mieux connue sous le nom Harvard Mark I. L’assourdissant cliquetis produit par des centaines de relais faisait penser à un atelier de confection avec une foule de femmes en train de faire du crochet. Son successeur, le Mark II, fut et restera pour l’éternité la plus grosse machine à avoir été basée sur des relais : elle en comptait plus de 13 000. C’est ce même laboratoire de Harvard dirigé par Aiken qui fut le premier à organiser des cours de science informatique.

Ces ordinateurs fonctionnant à base de relais, que l’on appelle de ce fait des ordinateurs électromécaniques, combinaient électricité et mécanique. Ce furent vraiment les premiers ordinateurs numériques opérationnels.

Le terme anglais « digital » choisi pour désigner ces machines avait été trouvé par George Stibitz en 1942, car il voulait les distinguer des ordinateurs analogiques utilisés alors depuis plusieurs dizaines d’années1.

Justement, l’un des ordinateurs analogiques parmi les plus connus était l’analyseur différentiel construit entre 1927 et 1932 par le professeur du MIT Vannevar Bush (1890-1974) avec l’aide de ses étudiants. La machine fonctionnait avec des disques rotatifs, des axes et des engrenages, et servait à résoudre des équations différentielles, c’est-à-dire des équations qui supposent de pouvoir réaliser des calculs différentiels. La solution d’une équation différentielle n’est pas une valeur numérique, mais une fonction : cette machine créait une trace graphique de la fonction sur papier.

Des ordinateurs analogiques existaient déjà bien auparavant, notamment la machine à prévoir les marées Tide-Predicting Machine conçue par le physicien William Thomson (1824-1907), celui-là même qui devint célèbre en tant que Lord Kelvin. Au cours des années 1860, Thomson avait trouvé une solution pour analyser les marées montantes et descendantes et subdiviser les motifs d’évolution sous la forme d’une série de sinusoïdes de fréquence et d’amplitude variables. Selon les termes mêmes de Thomson, le but de sa machine avait été de « remplacer du cerveau par du métal dans le cadre du fastidieux travail de calcul des paramètres fondamentaux des marées montantes et descendantes ». Autrement dit, la machine se servait de roues, d’engrenages et de courroies pour combiner les sinusoïdes des composantes puis imprimer le résultat sur un rouleau de papier, annonçant ainsi les horaires des marées.

Les deux machines que sont l’analyseur différentiel et la machine à prédiction de marées savaient imprimer des graphiques, mais il faut remarquer qu’elles y parvenaient sans avoir eu besoin de calculer les nombres normalement à l’origine des graphiques ! Cette approche est caractéristique des ordinateurs analogiques.

Dès 1879, William Thomson avait appris à distinguer ordinateurs analogiques et ordinateurs numériques, même s’il n’employait par encore ces termes. Son prédicteur de marées avait été nommé par lui « machine de calcul continu » pour que personne ne la confonde avec les machines purement arithmétiques comme le « projet ambitieux mais inachevé de machine à calculer de Babbage ».

Thomson faisait ici référence aux fameux travaux du mathématicien anglais Charles Babbage (1791-1871). Rétrospectivement, Babbage a constitué une singularité dans le cours des événements, puisqu’il avait tenté de construire un ordinateur numérique bien avant que les ordinateurs analogiques aient connu leur heure de gloire transitoire.

En effet, à l’époque de Babbage, et encore longtemps par la suite, ce que l’on appelait un calculateur était un être humain qui recevait un salaire pour effectuer des calculs. Il se servait de tables de logarithmes pour simplifier les multiplications et de tables trigonométriques, notamment pour ce qui concernait la navigation maritime. Lorsque vous aviez le projet de publier un nouvel ensemble de tables mathématiques, vous commenciez par embaucher plusieurs calculateurs, puis vous ramassiez leurs copies pour faire la synthèse des résultats. Bien sûr, des erreurs pouvaient apparaître à tout moment pendant le processus, aussi bien dans les calculs initiaux que lors de la composition des pages chez l’imprimeur.

Bettmann/Getty Images

Babbage étant une personne très méticuleuse, il souffrait beaucoup de trouver des erreurs dans ses tables mathématiques. C’est vers 1820 qu’il eut l’idée de concevoir une machine capable de créer ces tables automatiquement, y compris en se chargeant de préparer la composition des pages à imprimer.

La première machine de Babbage fut son moteur différentiel (Differential Engine). Le nom est lié au fait que cette machine se chargeait d’un travail en particulier dans le processus de création des tables mathématiques. Déjà à l’époque, on savait que pour générer une table de logarithmes, il n’était pas nécessaire de calculer le logarithme de chacune des valeurs. On calculait celui de certaines valeurs et on trouvait les valeurs intermédiaires par interpolation, et c’est dans ce cadre que l’on exploitait des différences, ce qui demandait des calculs relativement simples.

Babbage avait conçu sa machine différentielle justement pour calculer ces différences. Les engrenages représentaient des chiffres décimaux, et permettaient de faire des additions et des soustractions. Hélas, et malgré un financement initial du gouvernement britannique, la machine n’a jamais pu être achevée, et Babbage avait abandonné ce projet en 1833.

En effet, Babbage avait alors eu une meilleure idée : son moteur analytique. Babbage travailla sur sa conception pendant de nombreuses années jusqu’à sa mort, en ne produisant concrètement que quelque maquettes et composants élémentaires. Son moteur analytique est resté la proposition la plus proche du concept d’ordinateur numérique qu’avait pu offrir le xixe siècle. Dans l’esprit de Babbage, la machine devait disposer d’un entrepôt (store) jouant le rôle de mémoire et d’un moulin (mill) réalisant les calculs arithmétiques. Les multiplications pouvaient être réalisées en répétant des additions et les divisions en répétant des soustractions.

Il est tout à fait remarquable de voir que cette machine analytique pouvait être programmée en utilisant des cartes perforées. Babbage avait repris cette idée des métiers à tisser automatisés du Français Joseph-Marie Jacquard (1752-1834). Le métier Jacquard apparu vers 1801 utilisait des cartes perforées pour déterminer le motif de tricotage du tissu. Jacquard avait créé son propre chef-d’œuvre sous la forme d’un autoportrait sur soie en noir et blanc, qui avait nécessité 10 000 cartes.

Babbage n’a jamais pu produire une description complète et cohérente de ce qu’il cherchait à faire avec son moteur analytique. Il était beaucoup plus doué pour rédiger des justifications mathématiques des miracles ou une virulente critique des musiciens de rue.

L’état d’inachèvement laissé par Babbage a été compensé en partie par Augusta Ada Byron, comtesse de Lovelace (1815-1852). Elle était la seule descendante légitime du grand poète Lord Byron, mais sa mère avait tout fait pour l’orienter vers les mathématiques, car elle pensait qu’il fallait étouffer en elle l’éventuel et dangereux tempérament poétique que Ada aurait pu recevoir en héritage de son père. Dame Lovelace avait ainsi étudié avec le grand logicien Augustus De Morgan (déjà cité dans les Chapitres 6 et 8) et était tombée en extase devant la machine de Babbage.

Hulton Archive/Stringer/Getty Images

Un jour, elle eut l’opportunité de traduire un article depuis l’italien à propos de de ce moteur analytique. Elle accepta ce travail et sa traduction fut publiée en 1843. Elle avait ajouté toute une série de notes au point que l’article était devenu trois fois plus long que la version italienne. Une des notes présentait un exemple de jeu d’instructions pour la machine de Babbage. De ce fait, Dame Lovelace pouvait être considérée non pas comme la première programmeuse (c’est Babbage qui aurait pu l’être), mais comme la première personne à avoir publié un programme d’ordinateur.

Tous ceux parmi nous qui ont envoyé des listings de code source pour apprendre à programmer aux rédactions des magazines ou dans des livres peuvent se considérer comme des enfants spirituels de Lady Lovelace !

Nous lui restons redevables pour ce qui restera sans doute la plus poétique description de la machine de Babbage : « Nous pouvons considérer que le moteur analytique réalise un tissage de motifs algébriques, tout comme le métier Jacquard effectue le tissage de fleurs et de feuilles sur du tissu. »

Dame Lovelace n’était pas seulement une passionnée des calculs numériques, mais aussi une véritable visionnaire quant à l’évolution possible de l’informatique. Elle pensait que tout ce qui pouvait s’exprimer sous forme numérique pourrait faire l’objet d’un traitement par le moteur analytique :

Si nous supposons par exemple que les relations fondamentales entre les notes musicales dans la science de l’harmonie et de la composition pouvaient être présentées sous forme d’expressions et d’adaptations, cela permettrait à ce moteur analytique de composer des œuvres musicales élaborées sur des bases scientifiques, sans limite de complexité, ni de richesse.

Quand on songe que Babbage et Samuel Morse ont été quasi contemporains, et que Babbage connaissait le travail de George Boole, il est bien dommage qu’ils n’aient pas songé à combiner les concepts de relais télégraphique et de logique mathématique booléenne. C’est seulement dans les années 1930 qu’un ingénieux ingénieur s’est lancé dans la construction des ordinateurs avec des relais. La première machine à imprimer des tables de mathématiques fut le Harvard Mark I ; il a ainsi fallu cent ans pour réaliser le rêve de Babbage.

Entre l’apparition des premiers ordinateurs numériques dans les années 1930 et aujourd’hui, toute l’histoire de l’informatique peut se résumer ainsi : toujours plus petit, toujours plus rapide, toujours moins cher.

On a cherché, et trouvé, mieux que les relais électromagnétiques pour construire un ordinateur. Les relais sont des composants mécaniques dont le fonctionnement suppose de faire se plier des lamelles métalliques, et celles-ci finissent par se rompre. Un relais peut également tomber en panne par accumulation de poussière ou de peluches de papier entre ses contacts. Lors d’une panne devenue fameuse en 1947, on avait retrouvé une mite dans un relais de l’ordinateur Harvard Mark II. Une certaine Grace Murray Hopper (1906-1992), qui avait rejoint l’équipe de Aiken en 1944, est devenue célèbre dans le milieu des programmeurs informatiques parce qu’elle avait scotché la mite sur le journal de bord de la machine en mentionnant « Premier vrai bug découvert »2.

Pour remplacer les relais, on a d’abord opté pour les tubes à vide ou « lampes » (que les Britanniques appellent aussi des valves). Ces tubes électroniques avaient été mis au point par John Ambrose Fleming (1849-1945) et Lee de Forest (1873-1961) dans le cadre des recherches sur la radio. Au début de la Seconde Guerre mondiale, les tubes à vide avaient été adoptés à vaste échelle pour amplifier les signaux des téléphones, et recevoir des émissions de radio, au point que de nombreux domiciles disposaient d’une TSF équipée de tubes luminescents qui redressaient et amplifiaient les signaux captés par une antenne. Un tube à vide pouvait éventuellement être utilisé en mode tout ou rien, comme un relais, pour constituer ainsi des portes logiques AND, OR, NAND et autres.

Que les portes logiques soient construites avec des relais ou avec des tubes à vide, cela ne change rien à la logique permettant de constituer des additionneurs et des circuits logiques plus complexes.

Évidemment, les tubes à vide avaient leurs faiblesses. Ils étaient onéreux, ils consommaient beaucoup d’énergie électrique et produisaient beaucoup de chaleur. Le principal inconvénient était leur durée de vie limitée, mais on avait pris l’habitude de faire avec. Ceux qui possédaient une radio TSF en ces temps-là savaient remplacer périodiquement leurs tubes. De même, les centraux téléphoniques étaient conçus avec de nombreux circuits de secours par redondance, ce qui leur permettait de survivre à des pannes sporadiques de tubes. (Et personne ne s’attendait à ce que le système téléphonique soit sans pannes.)

En revanche, lorsqu’un tube tombe en panne dans un ordinateur, on ne le détecte hélas pas immédiatement. D’ailleurs, il y en a tant pour constituer un ordinateur que, statistiquement, on peut s’attendre à ce qu’un de ces tubes rende l’âme toutes les quelques minutes.

Ce que les tubes à vide ont apporté par rapport aux relais est un saut en vitesse de fonctionnement. Dans le meilleur des cas, un relais ne peut commuter (changer d’état) qu’en une milliseconde, soit 1 ms. Un tube électronique peut changer d’état mille fois plus vite, en 1 µs. Notons cependant que la vitesse de travail n’était pas un souci essentiel dans les premières étapes de l’informatique ; la vitesse globale du traitement dépendait d’abord de la vitesse à laquelle la machine pouvait ingurgiter les instructions à exécuter (le programme) depuis une bande de papier ou un film. Tant que les ordinateurs utilisaient ce mode d’entrée, la vitesse à laquelle les tubes à vide pouvaient changer d’état, même mille fois plus vite que les relais, n’était pas un critère essentiel.

Au début des années 1940, les tubes ont commencé à remplacer les relais et, en 1945, l’ère des relais était close. Nous sommes alors passés des ordinateurs électromécaniques aux premiers ordinateurs électroniques.

L’université de Pennsylvanie hébergeait l’école Moore School of Electrical Engineering. C’est dans cette dernière que J. Presper Eckert (1919-1995) et John Mauchly (1907-1980) inventèrent la machine ENIAC (Electronic Numerical Integrator and Computer). Sa construction ne fut achevée qu’à la fin de 1945. La machine comprenait 18 000 tubes à vide. Par sa masse, de l’ordre de 30 tonnes, l’ENIAC a clairement été le plus gros ordinateur ayant jamais été construit (record sans doute définitif). En revanche, lorsque Eckert et Mauchly ont tenté de breveter leur invention, ils ont dû faire face à une demande de brevet identique de la part de John V. Atanasoff (1903-1995). Ce dernier avait effectivement conçu un ordinateur électronique, mais il n’avait jamais fonctionné correctement.

L’ENIAC avait attiré l’attention du mathématicien John von Neumann (1903-1957). Originaire de l’Empire austro-hongrois, il s’était installé dès 1930 aux États-Unis. Personne brillante, réputé pour savoir faire des calculs complexes de tête, von Neumann était professeur de mathématiques à l’Institut Princeton d’études avancées. Ses travaux concernaient tant la mécanique quantique que l’application de la théorie des jeux à l’économie.

BettmaNn/Getty Images

Von Neumann avait alors apporté sa contribution à la conception du successeur de l’ENIAC, nommé EDVAC (Electronic Discrete Variable Automatic Computer). Par le biais d’un article publié en 1946 à propos de la conception logique d’un instrument de calcul électronique (document coécrit avec Arthur W. Burks et Herman H. Goldstine), il avait décrit un certain nombre de caractéristiques que devait avoir un ordinateur, et qui allait faire de l’EDVAC un progrès indéniable par rapport à l’ENIAC. En effet, alors que l’ENIAC utilisait des nombres décimaux, l’EDVAC exploitait des nombres binaires en interne. Il fallait qu’il dispose du plus grand espace mémoire possible et que cet espace mémoire puisse stocker aussi bien les instructions du programme que les données pendant l’exécution de ce programme (ce n’était pas encore l’approche choisie dans l’ENIAC qui demandait de configurer des interrupteurs et de brancher des câbles.) Cette nouvelle approche, toujours en vigueur, est le concept de programme stocké. Ce sont des choix de conception suffisamment décisifs pour que l’on parle encore de nos jours d’architecture von Neumann pour les ordinateurs ainsi construits.

C’est en 1948 que la société Eckert-Mauchly Computer Corporation (rachetée plus tard par Remington Rand) commença à travailler sur ce qui allait devenir le premier ordinateur commercialisé, l’UNIVAC (Universal Automatic Computer). La production avait débuté en 1951 et le premier exemplaire avait été installé dans le bureau de gestion des élections (Bureau of the Census). Un UNIVAC a été utilisé pour la première fois sur la chaîne de télévision CBS pour prévoir les résultats de l’élection présidentielle de 1952. Le présentateur de l’émission, Walter Cronkite, en parlait comme d’un « cerveau électronique ». C’est au cours de l’année 1952 que la société IBM présenta son premier système informatique commercialisé, le 701.

C’est ainsi que s’était ouverte une longue ère d’investissements en équipements informatiques dans les grandes entreprises et les établissements publics. Aussi intéressante que soit cette histoire, nous allons nous concentrer sur une autre voie historique, celle qui a vu se réduire sans cesse le coût et la taille des ordinateurs, jusqu’à leur permettre d’entrer dans les foyers en tant qu’ordinateurs personnels. Pourtant, tout cela avait commencé par une découverte en électronique de 1947 presque passée sous silence.

Les laboratoires Bell Telephone avaient été créés en janvier 1925 lorsque la société ATT (American Telegraph and Telephone) avait choisi de rendre indépendante leur division de recherche scientifique et technique pour la séparer du reste de ses activités. La raison d’être principale des laboratoires Bell avait été de développer de nouvelles technologies pour améliorer le système téléphonique. Cet ordre de mission était suffisamment vague pour autoriser toutes sortes de recherches, mais un objectif clairement précisé par rapport au système téléphonique était la recherche de moyens pour amplifier les signaux vocaux transmis sur les fils avec le moins de distorsion possible.

Il a fallu attendre le 16 décembre 1947 pour que deux physiciens des Bell Labs, John Bardeen (1908-1991) et Walter Brattain (1902-1987), mettent au point le prototype d’un amplificateur totalement différent. Il était en effet bâti sur un morceau de germanium (un matériau semi-conducteur), ainsi qu’une bande de feuille d’or. Ils en firent la démonstration à leur chef, William Shockley (1900-1989), la semaine suivante. C’était là le premier transistor, le composant que certains considèrent comme l’invention majeure de tout le xxe siècle.

Le transistor ne vient pas de nulle part. Le 29 décembre 1939, soit huit ans auparavant, Shockley avait noté dans son carnet : « Je constate aujourd’hui qu’un amplificateur utilisant non pas des tubes à vide mais des semi-conducteurs est en principe réalisable. » Avoir démontré la faisabilité d’un premier transistor était une première étape : de nombreuses années ont été nécessaires pour le perfectionner. Shockley, Bardeen et Brattain n’ont reçu le prix Nobel de physique qu’en 1956 « pour leurs recherches sur les semi-conducteurs et leur découverte de l’effet transistor ».

J’avais déjà évoqué dans les chapitres précédents les conducteurs et les isolants. On dit qu’un matériau est conducteur s’il laisse facilement passer les charges électriques. Le cuivre, l’argent et l’or sont d’excellents conducteurs, ce qui n’a rien d’étonnant puisque ces trois matériaux se trouvent dans la même colonne du tableau périodique des éléments.

Le germanium et le silicium (et quelques autres) sont considérés comme semi-conducteurs, non pas parce qu’ils conduisent l’électricité deux fois moins bien qu’un conducteur, mais parce que leur capacité à conduire peut varier dans une certaine mesure. Un atome de matériau semi-conducteur comporte quatre électrons sur son orbite la plus externe, ce qui est la moitié de ce qu’il faut pour qu’une orbite soit saturée, complète. Lorsque le semi-conducteur est pur, tous les atomes sont liés entre eux de façon solide et stable, ce qui donne une structure cristalline proche de celle du diamant. Dans cet état, ce n’est pas un bon conducteur.

Un semi-conducteur peut être dopé, c’est-à-dire « pollué » en y injectant quelques atomes d’un autre matériau servant d’impureté. Un type d’impureté va ajouter des électrons supplémentaires. Le matériau devient un semi-conducteur de type N (négatif). Il existe un second type d’impureté qui, injecté dans le semi-conducteur, le transforme en semi-conducteur de type P.

On peut créer un composant amplificateur à partir d’un semi-conducteur en plaçant un morceau de matériau de type P entre deux de type N. Le résultat est un transistor NPN. Les trois contacts reliés aux trois morceaux sont appelés collecteur, base et émetteur.

Voici le schéma d’un transistor NPN :

Le principe est qu’une petite variation de la tension sur la base provoque une variation plus importante de la tension entre collecteur et émetteur. S’il n’y a aucune tension sur la base, le transistor est coupé, comme un relais.

Physiquement, les transistors se présentent d’abord dans de petits boîtiers métalliques de quelques millimètres de diamètre, avec les trois fils qui en sortent :

Avec le transistor, nous sommes entrés dans l’ère de l’électronique à l’état solide, contrastant avec les tubes à vide. Les transistors sont fabriqués à partir de matériaux semi-conducteurs, en particulier le silicium. Un transistor est beaucoup plus petit qu’un tube à vide, il demande bien moins d’énergie, chauffe moins, est moins fragile et dure plus longtemps. Il aurait été inconcevable de se promener avec une radio équipée de tubes à vide dans la poche. En revanche, on peut alimenter une radio à transistors avec une pile, et l’appareil ne chauffe jamais. Quelques veinards ont pu découvrir un « transistor » de poche sous leur sapin de Noël de 1954. Les transistors qui équipaient ces appareils étaient fabriqués par la société Texas Instruments, une société des plus importantes dans la révolution des semi-conducteurs.

Pourtant, ce sont les implants auditifs qui ont constitué la première application commerciale du transistor. La société ATT avait en effet autorisé les fabricants de prothèses auditives à utiliser sa technique du transistor sans lui verser de droits, en guise d’hommage à l’héritage spirituel d’Alexandre Graham Bell, qui avait sa vie durant travaillé avec des personnes malentendantes.

En 1960 est apparue la première télévision équipée de transistors. De nos jours, les appareils équipés de tubes à vide ont pratiquement disparu. (Pas totalement, ceci dit, car certains passionnés de haute-fidélité et guitaristes électriques persistent à considérer que le son d’un amplificateur à tubes est meilleur que celui d’un appareil à transistors.)

Shockley avait choisi en 1956 de quitter les Bell Labs pour former sa société Shockley Semiconductor Laboratory, installant ses locaux là où il avait grandi, à Palo Alto en Californie. C’était la première société à s’implanter dans cette région qui allait devenir célèbre. D’autres entreprises de semi-conducteurs et d’informatique allaient venir la rejoindre, au point que de nos jours la région au sud de San Francisco est connue dans le monde entier en tant que Silicon Valley.

Les tubes à vide avaient été conçus au départ pour amplifier des signaux, mais ils pouvaient être utilisés en mode commutation, simulant des relais dans les portes logiques. Il en va de même pour le transistor. Voici une porte logique AND construite avec deux transistors ; le fonctionnement est similaire à celui de la version basée sur des relais :

Il faut que les deux entrées A et B présentent une tension pour que les transistors conduisent tous les deux ce qui fera apparaître une tension sur la sortie. La résistance évite qu’un court-circuit apparaisse lorsque les transistors sont passants et que rien ne freine l’appel de courant.

Ces deux mêmes transistors peuvent être connectés de façon à constituer une porte OR : chacun des deux collecteurs est branché à l’alimentation V et les émetteurs sont interconnectés :

Tout ce que nous avons appris à propos de la conception des portes logiques au moyen de relais s’applique aux transistors. Relais, tubes à vide et transistors sont trois technologies conçues au départ pour faire de l’amplification, mais qui ont pu ensuite être utilisées en commutation pour créer des portes logiques, et donc des ordinateurs. Les premiers ordinateurs basés sur des transistors sont apparus en 1956. Il n’a fallu que quelques années pour que les tubes soient totalement abandonnés.

En adoptant les transistors, les ordinateurs devenaient plus fiables, plus compacts et moins énergivores ; cela ne les rendait néanmoins pas plus simples à concevoir et à construire. Grâce aux transistors, vous pouviez augmenter la densité de portes logiques dans le même espace, mais il fallait encore plus s’inquiéter des interconnexions entre composants. Il est aussi, voire plus, difficile de bien réaliser les interconnexions entre les transistors servant de portes logiques qu’avec des relais ou des tubes à vide.

Nous avons eu l’occasion de constater que certaines combinaisons de transistors revenaient souvent. On utilise souvent des paires de transistors pour construire des portes logiques. Avec ces portes, on construit des additionneurs ou des décodeurs et encodeurs (comme vu à la fin du Chapitre 10). Nous nous intéresserons dans le Chapitre 17 à une autre structure élémentaire d’utilisation des portes logiques, la bascule flip-flop, qui sert à mémoriser un bit d’information ; nous verrons également un circuit compteur binaire. Ces différents constituants deviendraient beaucoup plus faciles à réaliser si l’on pouvait disposer de transistors préconfigurés selon les schémas les plus demandés.

Il semble que cette idée ait été émise au départ par le physicien britannique Geoffrey Dummer (1909-2002). Lors d’un discours en mai 1952, il avait indiqué « vouloir émettre une hypothèse sur le futur » :

Grâce à l’invention du transistor et aux recherches réalisées sur les semi-conducteurs, il est devenu possible d’imaginer qu’un équipement électronique soit fabriqué sous la forme d’un bloc solide sans fils de connexion internes. Ce bloc pourrait être constitué de plusieurs couches de matériaux tour à tour isolants, conducteurs, rectificateurs et amplificateurs, les différentes fonctions électroniques étant connectées en découpant directement des régions dans les différentes couches.

Il aura cependant fallu attendre quelques années avant de voir un produit concret découlant de cette vision.

C’est en juillet 1958, et sans avoir eu vent de la prévision de Dummer, que Jack Kilby (1923-2005), de la société Texas Instruments, parvint à fabriquer un morceau de silicium hébergeant plusieurs transistors ainsi que des résistances et quelques autres composants électroniques. Dans ses grandes lignes, la même idée est venue six mois plus tard (janvier 59) à Robert Noyce (1927-1990). Noyce avait au départ travaillé pour la société Shockley Semiconductor, mais il l’avait quittée en 1957 avec sept collègues pour fonder Fairchild Semiconductor Corporation.

Dans l’histoire des sciences et techniques, il n’est pas rare que deux inventeurs découvrent la même chose à peu près en même temps. Alors que Kilby avait inventé le circuit intégré six mois avant Noyce, et que son employeur Texas Instruments avait fait une demande de brevet, donc avant Fairchild, c’est Noyce qui s’est vu attribuer en premier un brevet à ce sujet. S’ensuivit évidemment une bataille juridique qui mit dix ans à se résoudre de façon satisfaisante pour les deux parties. Bien qu’ils n’aient jamais travaillé ensemble, Kilby et Norris sont dorénavant considérés comme co-inventeurs du circuit intégré, qu’on appelle également puce ou chip.

La fabrication d’un circuit intégré est un processus complexe qui consiste à déposer des couches de silicium correctement dopé dans un sens ou l’autre (N ou P) et gravé sous forme de régions formant des composants microscopiques. La création du masque d’un nouveau circuit intégré est très coûteuse, mais la production en série permet ensuite de réduire les coûts unitaires en proportion des quantités.

La pastille de silicium étant très fragile, elle doit être correctement conditionnée dans un boîtier protecteur qui porte également les broches de connexion. Pour les premiers circuits intégrés, on avait choisi différents boîtiers, le plus répandu étant le boîtier rectangulaire à deux rangées de broches DIP (Dual In-line Package) doté de 14, 16 ou même 40 broches sortant des deux côtés :

La figure montre une puce à 16 broches. Si vous tenez le circuit avec la petite indentation du côté gauche, comme dans la figure, les broches sont numérotées de 1 à 16, en commençant en bas à gauche et en tournant dans le sens inverse des aiguilles d’une montre. De chaque côté, les broches sont séparées les unes des autres d’un dixième de pouce, soit environ 2,5 mm.

Au cours des années 1960, les premiers circuits intégrés ont fait l’objet d’une demande énorme de la part des programmes spatiaux et militaires. Dans le domaine civil, le premier produit commercialisé qui exploitait un circuit intégré était une prothèse auditive commercialisée par Zenith en 1964. Dès 1971, Texas Instruments avait commencé à proposer des calculatrices de poche et Pulsar, des montres numériques. (Il est évident que le circuit intégré dans une telle montre n’avait pas le même boîtier que celui qui a servi d’exemple ici.) De plus en plus de produits ont commencé à se baser sur des circuits intégrés.

Dès 1965, Gordon E. Moore, qui travaillait à l’époque chez Fairchild (cofondateur ensuite d’Intel Corporation), avait remarqué que les progrès technologiques étaient tels que le nombre de transistors que l’on pouvait intégrer sur une même puce doublait chaque année depuis 1959. Il prédisait que cette tendance allait se poursuivre longtemps. En réalité, le rythme s’est quelque peu érodé et la loi de Moore (qui tire son nom de monsieur Moore) prédit dorénavant un doublement de densité tous les dix-huit mois, ce qui reste une cadence de progression époustouflante ; on comprend ainsi pourquoi les ordinateurs personnels semblent dépassés en quelques années. La deuxième décennie du xxie siècle semble néanmoins montrer un ralentissement par rapport à la loi de Moore, mais pas au point de faire songer à un décrochage.

Les composants, notamment les transistors, utilisés pour créer un circuit intégré peuvent être choisis parmi plusieurs technologies, ce qui résulte en plusieurs famille de circuits intégrés. Au milieu des années 1970, il y en avait deux principales : la famille TTL et la famille CMOS (prononcer « cémosse »).

Le sigle TTL signifie Transistor-Transistor-Logic. Cette technologie était privilégiée par tous ceux qui avaient d’abord besoin de circuits rapides. L’autre technologie, CMOS (Complementary Metal Oxyde Semiconductor), est moins gourmande en énergie et plus tolérante aux variations de tension, mais au prix d’une vitesse d’exécution inférieure à celle de TTL.

Si vous aviez été concepteur de circuits électroniques au milieu des années 1970 (pour construire des équipements à partir de circuits intégrés), vous auriez certainement eu en permanence sur votre bureau un épais livre tel que celui publié pour la première fois en 1973 par Texas Instruments et intitulé Data Book TTL pour ingénieurs. Il s’agit d’une référence complète à toute la famille de circuits intégrés TTL de la gamme 7400 proposée par Texas Instruments et quelques autres fabricants. Tous les circuits de cette famille commencent par 74, d’où son nom.

Tous les circuits intégrés de la série 7400 contiennent des portes logiques configurées. Certaines puces contiennent une série de portes simples, permettant de créer des circuits complexes et d’autres fournissent des fonctions évoluées parmi les plus demandées.

Le premier circuit de la famille 7400 porte justement cette référence 7400. Le Data Book TTL indique que c’est une quadruple porte NAND positive à deux entrées. Autrement dit, la puce réunit quatre portes logiques NAND de base. Ce sont des portes NAND positives parce que l’état logique 1 correspond à une tension de 5 V environ et l’état logique 0, à l’absence de tension. L’ensemble est protégé par un boîtier 14 broches. Un synoptique dans le Data Book montre à quoi est reliée chacune des broches d’entrée ou de sortie :

La figure fournit une vue de dessus de la puce, donc avec les broches en bas et l’indentation du côté gauche (comme dans la figure précédente).

La broche 14 porte la mention Vcc et c’est elle qui reçoit le pôle positif de l’alimentation (comme le symbole V dans les schémas des chapitres précédents). La broche 7 porte la mention GND pour la masse (Ground). Toutes les puces doivent ainsi être connectées à une source de tension de 5 V et à une masse commune. Chacune des quatre portes de ce circuit comporte deux entrées et une sortie, et elles sont indépendantes les unes des autres.

Une caractéristique essentielle d’une famille de circuits intégrés est son temps de propagation, c’est-à-dire le temps nécessaire pour qu’un changement d’état sur une entrée provoque un changement sur la sortie.

En général, les temps de propagation des puces sont exprimés en nanosecondes (ns), ce qui n’est vraiment pas long. Rappelons qu’un millième de seconde est une milliseconde et qu’un millième de milliseconde est une microseconde. Une nanoseconde est encore mille fois plus brève qu’une microseconde. Les portes NAND du circuit 7400 garantissent un temps de propagation inférieur à 22 ns, soit 22 millièmes de millième de millième de seconde.

Si vous n’arrivez pas à vous représenter ce que vaut 1 ns, sachez que vous n’êtes pas le seul. Si vous tenez ce livre devant vous pour le lire, une nanoseconde est le temps qu’il faut à la lumière pour aller de la page du livre à vos yeux.

C’est cette minuscule nanoseconde qui rend les ordinateurs intéressants. Chaque étape de fonctionnement d’un ordinateur correspond en effet à une opération très simple. Pour que le résultat final du travail d’un ordinateur devienne intéressant, il faut que l’enchaînement de ces opérations élémentaires soit très rapide. Comme le dit Robert Norris : « Une fois que vous avez accepté la brièveté de la nanoseconde, vous constatez que le fonctionnement d’un ordinateur est en fait assez simple. »

Continuons à parcourir le fameux Data Book TTL Texas. Vous y trouvez de nombreux circuits très pratiques : le circuit 7402 propose quatre portes NOR à deux entrées ; le 7404, six inverseurs ; le 7408, quatre portes AND à deux entrées ; le 7432, quatre portes OR à deux entrées ; et le 7430, une unique porte NAND mais à huit entrées :

Dans cette figure, la mention NC signifie No Connection (broche non connectée).

En continuant à feuilleter le Data Book TTL, nous arrivons au 7483, un additionneur complet sur 4 bits, le 74151, un sélecteur de données huit voies vers une et le 74154, qui est un décodeur quatre voies vers seize.

Si vous vous demandez dans la suite du livre où j’ai pu trouver mes sources d’information pour sélectionner les composants, sachez que j’ai pioché dans le Data Book TTL.

Une puce particulièrement intéressante est le circuit 74182, un générateur de retenue anticipée. Ce circuit est prévu pour être utilisé avec le circuit 74181 qui se charge des opérations arithmétiques telles que l’addition. Vous avez appris au cours du Chapitre 14 que chacun des bits dans un additionneur 8 bits dépend de la retenue du bit précédent. Il s’agit dans ce cas de l’approche à retenue propagée (ripple carry). Plus les nombres additionnés sont grands, plus l’opération est lente.

La technique de la retenue anticipée tente de réduire ce défaut en prévoyant une circuiterie spécifique pour calculer le bit de retenue plus vite que si c’était l’additionneur qui devait le faire. Cela suppose un complément de portes logiques, mais le temps total de calcul est diminué.

Dans certaines situations, on améliore un circuit en supprimant quelques portes logiques, mais dans d’autres cas, un gain de performance suppose d’ajouter des portes logiques pour gérer des cas particuliers.

Les portes logiques ne sont pas des objets imaginaires, mais des composants très concrets. Au départ, on a créé des portes logiques et des additionneurs avec des relais, puis les relais ont été remplacés par des tubes à vide, puis les tubes ont fait place aux transistors et ces transistors, enfin, ont été intégrés dans de minuscules pastilles de silicium. Dans toutes ces étapes successives, les concepts en jeu sont restés strictement les mêmes.

1 N.d.T. : on emploie de préférence en France le terme « numérique » au lieu de « digital », mais ce dernier s’est imposé de force.

2 N.d.T. : le mot bug en anglais signifie « insecte » et, dorénavant aussi, « cause d’une erreur de fonctionnement d’un ordinateur ». En français, on parle de « bogue », qui est la coquille de la châtaigne, et les coquilles sont les erreurs du texte chez les imprimeurs.

Chapitre 16  Et les soustractions ?

Une fois que vous savez que l’on peut faire additionner des valeurs binaires par des relais, des tubes ou des transistors, vous allez sans doute vous demander ce qu’il en est des soustractions. Votre question est loin d’être déplacée. Les deux opérations que sont l’addition et la soustraction sont dans une certaine approche complémentaire, mais les détails de chacune sont assez différents. Une addition progresse toujours de la colonne de chiffres la plus à droite vers la colonne la plus à gauche, et on se contente de propager la retenue d’une colonne à la suivante. Dans le cas d’une soustraction, on pose une retenue négative (l’anglais est même plus précis puisqu’il parle d’un emprunt, borrow). Pour effectuer une soustraction, il faut utiliser un mécanisme un peu déstabilisant qui consiste à avancer et reculer parmi les colonnes de chiffres.

Choisissons en guise d’exemple une soustraction présentant de nombreuses retenues inverses :

Comme tout le monde, dès que vous lisez ici les chiffres de la colonne la plus à droite, vous comprenez que 6 est supérieur à 3, et que cela ne passe pas. Il vous faut enlever une unité de la colonne suivante à gauche qui, elle-même, a besoin d’emprunter à sa voisine de gauche. Sans entrer dans les détails, si vous faites les opérations correctement sans vous tromper, vous obtenez la réponse :

Comment pourrions-nous persuader un réseau de portes logiques de simuler ce raisonnement assez pervers qui, seul, permet d’obtenir le résultat ?

Nous n’allons même pas essayer. Nous allons adopter une astuce permettant d’effectuer la soustraction sans retenue inverse. Ce qui semble n’être rien qu’une astuce est en fait une étape essentielle de la compréhension du mode de stockage des valeurs négatives dans un ordinateur.

Vous avez certainement appris, au cours de votre scolarité, que soustraire revenait à additionner un nombre négatif. Notez que savoir cela ne vous aide en aucun cas à simplifier l’opération de soustraction. Cela signifie que nous allons pouvoir reformuler la soustraction sous la forme d’une valeur positive ajoutée à une valeur négative :

– 176 + 153 =

J’ajoute à l’équation deux autres nombres, un positif et un négatif, ce qui nous amène à une séquence de quatre :

1 000 – 176 + 253 – 1 000 =

Le résultat ne doit pas changer puisque nous ajoutons 1 000 puis les soustrayons. Nous savons par ailleurs que 1 000 équivaut à 999 + 1. Au lieu de commencer par 1 000, nous commençons par 999 pour ajouter 1 plus tard. Notre formule devient encore plus longue, mais reste inchangée au niveau du résultat :

999 – 176 + 253 + 1 – 1 000 =

Le résultat semble devenu inutilement complexe. Commençons à travailler sur cette équation de gauche à droite. La première opération est une soustraction de 999 – 176. Nous sommes ravis de découvrir qu’il n’y a aucune retenue à prévoir. On peut facilement trouver que 999 – 176 donne 823 :

823 + 253 + 1 – 1 000 =

En décimal, le fait de soustraire une valeur à une chaîne de chiffres 9 produit un nombre qui est le complément à neuf du nombre de départ. Le complément à neuf de 176 vaut 823. L’opération fonctionne en sens inverse : le complément à neuf de 823 est 176. L’avantage est que lorsque l’on cherche le complément à neuf d’un nombre, on n’a jamais besoin de prévoir une retenue inverse !

Les deux étapes suivantes sont des additions. Nous ajoutons d’abord 253 à 823 pour obtenir 1076 :

1 076 + 1 – 1 000 =

Il ne reste ensuite plus qu’à ajouter 1 et soustraire 1 000 :

1 077 – 1 000 = 77

Nous sommes bien parvenus au même résultat, mais sans avoir une seule retenue inverse à réaliser.

Un point important : lorsque l’on utilise la technique du complément à neuf pour simplifier une soustraction, il est essentiel de savoir sur combien de chiffres on travaille. Si au moins l’un des deux nombres d’entrée possède quatre chiffres, par exemple, il faut calculer les compléments à neuf en utilisant la valeur 9 999 puis en soustrayant 10 000 à la fin.

Mais comment faire lorsque vous cherchez à soustraire un petit nombre à un nombre plus grand ? Imaginons la soustraction suivante :

Si vous réfléchissez au problème, pour soustraire un nombre plus grand à un nombre plus petit, vous vous dites qu’il faudrait intervertir les deux nombres, réaliser la soustraction et ne pas oublier que le résultat est en réalité une valeur négative. Vous pouvez intervertir les deux valeurs mentalement et obtenir la réponse ainsi :

La technique pour réaliser ce calcul sans retenue est un peu différente de celle que nous venons de voir. Nous commençons par intervertir les deux valeurs, ajouter 999 au début et soustraire la même valeur à la fin :

999 – 253 + 176 – 999 =

Comme précédemment, nous commençons par soustraire 253 à 999 pour obtenir le complément à neuf :

746 + 176 – 999 =

Il ne reste qu’à ajouter le complément à neuf de 176 :

922 – 999 =

Dans le premier cas, il n’y avait plus qu’à ajouter 1 et à soustraire 1 000 pour obtenir le résultat final. Cette technique ne fonctionne pas ici. Nous aboutissons à une valeur positive 922 et une valeur négative 999. Si les signes étaient inversés, avec 922 négatif et 999 positif, il suffirait de prendre les compléments à neuf de 922 et le complément à neuf de 922, soit 77. Mais nous avons permuté les signes pour réaliser le complément à neuf et la réponse est – 77. Autrement dit, l’opération n’est pas aussi simple que dans le premier cas, mais nous n’avons toujours pas eu besoin de poser une retenue.

La technique est bien sûr possible avec des valeurs binaires, et c’est d’ailleurs plus simple qu’avec des valeurs décimales, ce que nous allons voir.

Repartons de la soustraction initiale :

Reposons l’opération après conversion en binaire des deux valeurs :

Je commence par permuter les deux nombres pour que le problème se présente comme l’ajout d’une valeur positive à une valeur négative. Je rappelle les équivalents en décimal sous les valeurs binaires :

– 1011 0000 + 1111 1101 =

– 176 + 253 =

Nous pouvons maintenant ajouter la valeur maximale 1111 1111 (255 en décimal) au départ, puis ajouter 0000 0001 (1 en décimal) et, enfin, soustraire 1 0000 0000, soit 256d :

1111 1111 – 1011 0000 + 1111 1101 + 0000 0001 – 1 0000 0000 =

255 – 176 + 253 + 1 – 256 =

La première soustraction ne va pas générer de retenue parce que la valeur est soustraite à la valeur maximale :

Lorsque l’on ôte une valeur décimale à une série de neuf, le résultat est le complément à neuf de cette valeur. Avec des valeurs binaires, quand on effectue les soustractions par rapport à une série de un, on obtient le complément à un. Si vous regardez bien, vous comprenez que nous n’avons même pas besoin de faire une soustraction pour aboutir au complément à un : le complément à un de 1011 0000 vaut 0100 1111 et celui de 0100 1111 vaut 1011 0000 :

10110000

01001111

En fait, les bits sont simplement inversés : chaque bit à 0 devient un bit à 1 dans le complément et inversement. C’est pourquoi on parle souvent d’inversion d’état. (Rappelez-vous, dans le Chapitre 8, nous avions découvert une porte logique très simple appelée inverseur qui se contentait de changer un 0 en 1 et un 1 en 0.)

Voici comment se présente dorénavant le problème :

0100 1111 + 1111 1101 + 0000 0001 – 1 0000 0000 =

79 + 253 + 1 – 256 =

Nous pouvons faire l’addition des deux premiers nombres :

Nous obtenons un nombre sur 9 bits, ce qui ne pose pas de souci. Notre équation peut être simplifiée ainsi :

1010 01100 + 0000 0001 – 1 0000 0000 =

332 + 1 – 256 =

Aucun problème pour ajouter 1 :

1 0100 1101 – 1 0000 0000 =

333 – 256 =

Nous n’avons plus qu’à soustraire l’équivalent binaire de 256, ce qui permet d’éliminer le bit le plus à gauche :

0100 1101

77

Ce résultat, vous l’aurez remarqué, est bien le même que lorsque nous avions traité le problème en décimal.

Essayons maintenant l’approche avec les deux valeurs inversées. Rappelons comment se présente la soustraction en décimal :

Voici sa reformulation en binaire :

Comme dans la base décimale, nous permutons les deux nombres, puis ajoutons 1111 1111 au début et soustrayons la même valeur à la fin :

1111 1111 – 1111 1101 + 1011 0000 – 1111 1111 =

255 – 253 + 176 – 255 =

Nous devons d’abord chercher le complément à un de 1111 1101 :

Nous ajoutons cette valeur au nombre suivant :

1011 0010 – 1111 1111 =

178 – 255 =

Il ne reste plus qu’à soustraire 1111 1111 (la valeur maximale) à ce résultat. Normalement, vous devriez ajouter 1 puis soustraire 1 million. Mais cela n’est pas possible sans faire apparaître une retenue. Nous allons plutôt soustraire ce résultat à la valeur 1111 1111 :

1111 1111 – 1011 0010 = 0100 1101

255 – 178 = 77

Nous ne faisons qu’inverser l’état de tous les bits. Le résultat est encore l’équivalent de 77d, mais dans ce cas particulier, la réponse est – 77.

Ce petit souci auquel nous faisons face lorsque nous demandons à soustraire un grand nombre d’un plus petit nous prévient que la situation n’est pas totalement stabilisée. Il nous reste quelques éléments à régler.

Nous avons cependant engrangé suffisamment de connaissances pour pouvoir modifier notre additionneur du Chapitre 14 afin qu’il sache également réaliser des soustractions. Pour que l’exposé ne devienne pas brutalement trop complexe, notre machine à additionner et soustraire ne va faire de soustraction que lorsque le résultat est à coup sûr positif.

Rappelons le circuit synthétique d’un additionneur 8 bits créé à partir de portes logiques :

Figure 16.1

Vous vous souvenez que les entrées A0 à A7 et B0 à B7 étaient connectées à des interrupteurs qui incarnaient les deux valeurs sur 8 bits à additionner.

La première entrée de retenue CI était reliée à la masse, simulant un bit à 0. Les huit sorties S0 à S7 étaient reliées à des lampes présentant le résultat. Comme une addition pouvait faire apparaître une valeur sur 9 bits, nous avions prévu une neuvième lampe reliée à la sortie de retenue CO du dernier étage de l’additionneur.

Rappelons l’aspect du panneau de commande :

Dans cette figure, les interrupteurs sont positionnés pour réaliser en guise d’exemple l’addition de 183 (1011 0111) et de 22 (0001 0110). On obtient le résultat 205, soit 1100 1101, ce que confirment les lampes allumées en bas.

Nous modifions ce panneau de commande pour gérer l’addition et la soustraction de deux valeurs sur 8 bits. À la place du signe + à gauche, nous installons un interrupteur permettant de choisir entre le mode addition et le mode soustraction.

Dans la figure, Sub signifie « soustraction » et Add, « addition ».

La seule autre différence est que seules les huit lampes de droite vont servir au résultat, la neuvième portant dorénavant le nom Overflow (« débordement »). Ce terme se rencontre souvent en informatique dans différents contextes ; en général, ce n’est pas bon signe. Notre panneau de contrôle est celui d’un additionneur 8 bits, et la plupart des résultats tiendront sur ces 8 bits. C’est lorsque le résultat réclamera un neuvième bit que nous parlerons de débordement. Une soustraction peut aussi déclencher un débordement lorsque nous tentons de soustraire une valeur plus grande à une plus petite.

La lampe de débordement Overflow va servir à prévenir que le résultat est négatif, mais nous n’avons pour l’instant pas encore géré l’affichage d’un résultat négatif.

Au niveau des circuits de traitement derrière le panneau, la principale modification est l’ajout d’un module pour calculer les compléments à un d’une valeur sur 8 bits. Rappelons que cette opération se résume à inverser l’état de chaque bit. Un circuit qui calcule le complément à un d’une valeur sur 8 bits se résume donc à une batterie de huit inverseurs :

Le problème de ce circuit inverseur est qu’il inverse en permanence les bits qui lui sont présentés en entrée alors que nous voulons créer une machine capable de faire des additions et des soustractions. Il faut que ce circuit soit en action seulement dans le cas d’une soustraction. Voici donc une version améliorée de cette batterie d’inverseurs :

Un signal unique d’inversion Invert est injecté dans une des deux entrées de chacune des huit portes OR, dont revoici la table de vérité :

XOR

0

1

0

0

1

1

1

0

Si le signal Invert vaut 0, les huit sorties des portes OR sont identiques aux huit entrées. Pour une valeur d’entrée 0110 0001, la sortie reste à la même valeur. Si le signal Invert vaut 1, les huit signaux de sortie sont dans l’état inverse de celui des huit signaux d’entrée ; pour 0110 0001, nous obtenons 1001 1110.

Regroupons ce sous-ensemble de portes OR inverseuses dans une boîte fonctionnelle que nous intitulerons Complément à un :

Figure 16.2

Nous pouvons maintenant interconnecter ce boîtier de complément au boîtier de l’additionneur complet 8 bits et à une porte XOR finale :

Figure 16.3

Notez dans ce schéma le fil en haut à droite intitulé mode soustraction. Ce fil vient de l’interrupteur Add/Sub. Le signal vaut 0 dans le cas d’une addition et 1 dans le cas d’une soustraction. Dans le premier cas, l’entrée Invert du circuit des compléments à un est dans l’état 0, et ce circuit n’a aucun effet ; il reste transparent. L’entrée CI est également à 0 ; l’ensemble est un circuit d’addition normale.

Dans le cas d’une soustraction, toutes les entrées de la valeur B (la deuxième rangée d’interrupteurs du panneau de commande) voient leur signal inversé par le circuit de complément à un avant d’être présentés en entrée de l’additionneur. De plus, dans le cas d’une soustraction, on ajoute 1 au résultat en forçant à 1 l’entrée de retenue CI de l’additionneur.

Le signal du mode soustraction ainsi que la sortie de retenue CO de l’additionneur sont injectés dans une porte XOR dont le rôle est de contrôler l’allumage de la lampe de débordement Overflow. Si le signal du mode soustraction est à 0 (cas d’une addition), la lampe ne va s’allumer que si la sortie de retenue CO de l’additionneur est égale à 1. Autrement dit, cela indique que le résultat de l’addition a débordé de la valeur 255.

Dans le cas d’une soustraction, si le nombre B est inférieur au nombre A, il est normal que la sortie CO de l’additionneur vaille 1. Le résultat intègre en effet la valeur binaire 1 0000 0000 qu’il faut soustraire à la fin. Dans le cas des soustractions, la lampe de débordement Overflow ne va s’allumer que si la sortie CO de l’additionneur vaut 0. En effet, cela confirme que nous tentons de soustraire un nombre plus grand d’un plus petit. Notez que l’appareil utilisé ici n’a pas été conçu pour afficher des valeurs négatives.

Une fois ces préparatifs en place, vous vous demandez sans doute comment on réalise réellement des soustractions.

J’ai déjà abordé les nombres négatifs dans ce chapitre, sans expliquer comment ils sont représentés. Vous pourriez croire qu’il suffit en binaire d’ajouter un signe moins, comme en décimal. Ainsi, – 77d s’écrit – 100 1101 en binaire. Ce n’est pas impossible, mais un des buts de l’utilisation de valeurs binaires et la représentation de toutes valeurs uniquement avec des 0 et des 1, même le signe moins, symbole de valeurs négatives.

La première idée serait d’utiliser un bit pour indiquer la présence ou l’absence du signe moins. Il suffit de réserver ce bit qui vaudra 1 en cas de valeur négative et 0 sinon. Cela fonctionnerait, mais sans être idéal. Il existe une autre solution qui est devenue l’approche standard pour représenter les valeurs négatives dans les machines. Cette standardisation résulte de la méthode simple proposée pour additionner des valeurs négatives et positives. Son gros inconvénient, si l’on peut dire, est qu’il faut savoir à l’avance combien de chiffres, combien de positions, vont être requis pour tous les calculs que vous envisagez de faire dans le futur.

Faisant un retour sur nos pratiques habituelles : la notation classique des valeurs négatives offre l’avantage d’être illimitée. Nous pouvons considérer la valeur 0 comme un point milieu d’un flux infini de valeurs positives dans un sens et négatives dans l’autre :

... – 1 000 000 – 999 999... – 3 – 2 – 1 0 1 2 3... 999 999 1 000 000...

Imaginez maintenant que nous n’ayons pas besoin d’un nombre infini de chiffres et que nous sachions dès le départ que tous les nombres que nous allons traiter vont rester dans une plage de valeurs limitée.

Prenons comme exemple un compte bancaire. C’est le genre de lieu de survenance sporadique de valeurs négatives. Supposons qu’en accord avec votre banquier, vous lui avez fait promettre de ne refuser aucun chèque ou prélèvement tant que votre découvert reste dans les limites de – 500 €. Par ailleurs, vous savez que vous n’allez jamais avoir plus de 500 € au crédit du compte. Le solde du compte sera donc toujours entre +499 et – 500. Supposons par ailleurs, pour simplifier, que vous n’allez jamais déposer plus de 500 € à la fois, que vous n’allez jamais faire un chèque de plus de 500 € et que vous ne gérerez pas les centimes.

Avec ce jeu de conditions drastique, nous savons que les nombres que nous allons devoir manipuler pour gérer le compte bancaire se situent tous entre – 500 et +499. Il y a donc mille valeurs différentes. Grâce à cette limitation, nous sommes certains de pouvoir nous contenter de trois chiffres décimaux, sans avoir besoin de signe. En effet, puisque nous n’avons pas besoin des nombres positifs qui vont de 500 à 999 (en raison du plafond de +499), nous pouvons réutiliser cette plage de 500 à 999 pour symboliser des valeurs négatives sans signe. Voici comment cela fonctionne :

Pour symboliser – 500, nous écrivons 500.

Pour symboliser – 499, nous écrivons 501.

Pour symboliser – 498, nous écrivons 502.

…

Pour symboliser – 2, nous écrivons 998.

Pour symboliser – 1, nous écrivons 999.

Pour symboliser 0, nous écrivons 000.

Pour symboliser 1, nous écrivons 001.

Pour symboliser 2, nous écrivons 002.

…

Pour symboliser 497, nous écrivons 497.

Pour symboliser 498, nous écrivons 498.

Pour symboliser 499, nous écrivons 499.

Pour symboliser – 500, nous écrivons 500.

Autrement dit, tout nombre sur trois chiffres qui commence par un 5, un 6, un 7, un 8 ou un 9 est en réalité un nombre négatif. Nous avons remplacé la progression habituelle des valeurs négatives aux valeurs positives en passant par 0 en position centrale comme ceci :

– 500 – 499 – 498... – 4 – 3 – 2 – 1 0 1 2 3 4... 497 498 499

par la plage suivante qui va elle aussi de la moins grande à la plus grande valeur :

500 501 502... 996 997 998 999 000 001 002 003 004... 497 498 499

Vous remarquez que les deux valeurs extrêmes se rejoignent. La plus grande valeur négative 500 semble reprendre la suite de la plus grande valeur positive 499. Par ailleurs, la valeur 999 vaut en réalité – 1, c’est-à-dire 0-1. En ajoutant 1 à 999, on obtient normalement 1 000, mais comme nous ne traitons que trois chiffres, nous retombons sur 000.

Cette convention porte le nom de complément à dix. Pour convertir un nombre négatif sur trois chiffres en complément à 10, on le soustrait de 999 puis on ajoute 1. Autrement dit, le complément à dix est le complément à neuf plus 1. Pour écrire la valeur – 255 en complément à dix, on la soustraie de 999, ce qui donne 744, puis on ajoute 1 pour obtenir 745.

Vous n’avez de ce fait plus jamais besoin d’effectuer de soustractions lorsque vous travaillez avec le complément à dix. Il n’y a plus que des additions.

Prenons comme exemple un solde bancaire initial de 143 €. Vous rédigez un chèque d’un montant de 78 €. Habituellement, vous calculeriez votre nouveau solde ainsi :

C’est une soustraction avec deux retenues. Si nous reformulons la valeur en complément à 10, – 78 s’écrit 99 – 078 + 1, c’est-à-dire 922. Nous pouvons donc écrire ceci :

Nous pouvons ignorer le débordement et retrouver le même solde de 65 €. Si vous émettez ensuite un chèque de 150, il faudra ajouter – 150, c’est-à-dire 850 en complément à dix :

Le résultat commence par un 9, c’est donc une valeur négative, – 85.

En base binaire, on trouve un système équivalent qui se nomme le complément à deux. Il est utilisé de façon standard pour représenter les valeurs positives et négatives dans les ordinateurs.

Supposons que nous traitions des octets, donc des valeurs sur 8 bits. La plage des valeurs possibles va de 0000 0000 à 1111 1111. Pour le moment, nous avons toujours considéré ces valeurs comme les équivalents binaires des valeurs décimales 0 à 255. Si nous voulons faire de la place pour coder des valeurs négatives, il faut considérer que chacune des valeurs sur 8 bits dont le premier bit (de poids fort) vaut 1 doit être considéré comme une valeur négative, ce que montre le tableau suivant :

Binaire
Décimal

1000 0000

−128

1000 0001

−127

1000 0010

−126

1000 0011

−125

⁞

⁞

1111 1101

−3

1111 1110

−2

1111 1111

−1

0000 0000

0

0000 0001

1

0000 0010

2

⁞

⁞

0111 1100

124

0111 1101

125

0111 1110

126

0111 1111

127

Ce faisant, nous avons délimité la plage des valeurs possibles entre – 128 et +127. Le bit le plus significatif à gauche devient le bit de signe. Il vaut 1 pour les valeurs négatives et 0 pour les valeurs positives.

Voyons comment calculer un complément à deux. Il suffit de commencer par calculer le complément à un puis d’ajouter 1. Cela revient à inverser tous les bits avant d’ajouter 1. La valeur décimale 125 s’écrit 0111 1101. Pour convertir la valeur négative – 125 en complément à deux, nous commençons par inverser l’état des bits, ce qui donne 1000 0010, puis nous ajoutons 1 et obtenons 1000 0011. Vous pouvez vérifier le résultat en vous servant de la table précédente. Pour la conversion en sens inverse, le procédé est le même : inversion des bits puis ajout de 1.

L’énorme avantage de cette approche est d’éviter toute utilisation du signe moins pour les valeurs négatives. Un autre avantage est bien sûr de pouvoir traiter les valeurs positives et négatives sans jamais avoir besoin de faire autre chose que des additions. Voici par exemple comment additionner les équivalents binaires de – 127 et +124. Nous nous servons de la table précédente pour retrouver les valeurs :

Le résultat équivaut à – 3 en décimal.

Ce qu’il faut, c’est détecter les éventuels débordements qui peuvent dorénavant se produire lorsque le résultat de l’addition dépasse 127. Essayons, par exemple, d’ajouter 125 à lui-même :

Le bit de poids fort du résultat vaut 1, on doit donc en conclure qu’il s’agit d’une valeur négative, plus précisément la valeur – 6. Mais il est évident que l’addition de deux nombres positifs ne peut pas produire un nombre négatif. Il y a donc clairement une erreur.

Il se passe la même chose lorsque l’on essaie d’ajouter la valeur – 125 à elle-même :

Nous avons donc ici aussi un problème : nous avons choisi au départ de nous limiter à des nombres sur 8 bits, et le bit de poids fort du résultat, tout à fait à gauche, doit être ignoré. Les 8 bits restants valent 6, qui est un nombre positif.

Nous pouvons en déduire la règle suivante : le résultat d’une addition mettant en jeu des nombres positifs et négatifs en complément à deux doit être considéré comme invalide si les bits de signe des deux opérandes sont dans le même état, mais que le bit de signe du résultat est différent. En revanche, le résultat sera toujours correct lorsque l’on ajoute un nombre positif à un nombre négatif parce que, par nature, le résultat sera toujours limité à la plage entre – 128 et +127.

Voici une version modifiée de notre additionneur de deux nombres sur 8 bits en complément à deux :

Figure 16.4

Cet additionneur 8 bits nous est devenu familier. Nous lui ajoutons quelques portes logiques pour gérer le débordement. Rappelons que le bit de poids fort est celui de signe et qu’il vaut 1 si le nombre est négatif. Les bits de signe des entrées sont en A7 et B7 et celui de la sortie en S7. Prenez bien note du fait que l’état de S7 est inversé avant d’être injecté dans les portes AND et NOR du détecteur de débordement.

La porte AND détecte un cas de débordement pour les valeurs négatives. Si les bits de signe des deux entrées A et B sont à 1 (ce sont donc deux nombres négatifs), alors que le bit de signe du résultat S vaut 0 (résultat positif), c’est clairement un problème. L’addition de nombres négatifs ne peut vraiment pas être positive, mais le résultat peut être tellement négatif qu’il n’est plus représentable avec les bits disponibles.

La porte NOR (NON-OU) gère les débordements en cas de valeurs positives. Si les signes de A et de B sont à 0 et celui du résultat S à 1, c’est que l’addition des deux valeurs positives a produit un résultat tellement grand qu’il a empiété sur la demi-plage des valeurs négatives. Les trois entrées de la porte NOR seront donc à l’état 0 (rappelons que S7 a été inversé) ; la sortie de la porte sera à 1, ce qui confirmera qu’il y a eu débordement.

Lorsque nous avons commencé ce chapitre, les nombres binaires semblaient être une notion assez simple, avec une correspondance directe avec les nombres décimaux. Une valeur binaire sur 8 bits pouvait varier entre 0 et 255. Il s’agissait dans ce cas de valeurs binaires non signées, toujours positives.

En utilisant la technique du complément à deux, nous pouvons dorénavant travailler avec des valeurs binaires signées, positives ou négatives. Dans le cas d’une valeur sur 8 bits, la plage de valeurs va de – 128 à +127. Le nombre de valeurs différentes est le même, mais la plage ne l’est plus.

Le terme mathématique qui caractérise ce type de valeurs numériques est entier (integer). Il s’agit de valeurs pouvant être positives ou négatives, sans décimales (sans chiffres derrière une éventuelle virgule). Dans la vie courante, les valeurs entières sur 8 bits sont rarement de taille suffisante pour les besoins des programmeurs qui leur préfèrent des valeurs sur 16 bits (2 octets), 32 bits (4 octets) ou même 64 bits (8 octets).

Quelle que soit la largeur de la valeur, elle peut être signée ou non signée. Le tableau suivant présente, exprimées en décimal, les plages autorisées pour les différentes largeurs de numériques entiers, signés ou pas :

Largeur entier
Plage si non signé
Plage si signé

8 bits

0 à 255

– 128 à 127

16 bits

0 à 65 535

– 32 768 à 32 767

32 bits

0 à 4 294 967 295

– 2 147 483 648 à 2 147 483 647

64 bits

0 à 18 446 744 073 709 551 615

– 9 223 372 036 854 775 808 à 9 223 372 036 854 775 807

Ces plages découlent des puissances de 2 : une valeur sur 16 bits permet de symboliser un nombre de valeurs équivalent à 2 à la puissance 16, soit 65 536 valeurs différentes. Si les valeurs ne sont que positives, la plage va de 0 à +65 535. Dans le cas de valeurs avec signe, la plage s’étend de – 32 768 à +32 767.

La lecture des valeurs ne permet pas de savoir si elles sont de type signé ou non signé. Supposez que quelqu’un vous dise : « J’ai une valeur binaire sur 8 bits qui vaut 1011 0110. Quelle est sa valeur décimale ? » Vous devrez lui demander de préciser s’il s’agit d’une valeur signée ou non signée. Le résultat est en effet égal soit à – 74, soit à +182.

Les bits ne sont que des 0 et des 1. Ils ne donnent aucune information sur leur type ou leur nature exact. Cette information n’est disponible que dans le contexte d’utilisation des valeurs.

Chapitre 17  Temps et mémoire

L’énergie électrique permet d’agir sur les objets du monde réel. Il suffit de faire le tour de votre domicile pour y trouver des moteurs électriques : dans l’horloge murale, le ventilateur, la hotte de cuisine, le robot ménager, la machine à laver, et tout ce qui fait tourner un disque. C’est également l’électricité qui anime les haut-parleurs, les casques et les oreillettes pour écouter de la musique et les voix de nos semblables. Et même si vous ne disposez pas d’une voiture électrique, votre vieille guimbarde à combustible fossile ne saurait se mettre en marche sans son démarreur électrique.

Le domaine d’application peut-être le plus simple et le plus élégant de l’électricité est celui de ces équipements d’accès aux domiciles (qui tendent de plus en plus à être remplacés par leur équivalent électronique). Je veux parler des sonnettes et carillons de porte d’entrée électromécaniques.

Intéressons-nous à un relais connecté à un interrupteur et à une pile électrique, mais d’une manière nouvelle :

Figure 17.1

Si la vue de ce montage vous interloque, c’est normal. Nous n’avons encore jamais tenté d’interconnecter un relais de cette façon. Normalement, quand nous utilisons un relais, nous distinguons bien le circuit d’entrée du circuit de sortie. Ici, tout est réuni en un seul !

Si nous fermons l’interrupteur, nous permettons la circulation du courant :

Figure 17.2

Puisque le circuit est fermé, l’électro-aimant attire le contact :

Figure 17.3

Une fois le contact attiré, le circuit est interrompu et l’électro-aimant n’attire plus le contact qui revient en position de repos :

Figure 17.4

Le circuit est ainsi à nouveau fermé. Autrement dit, tant que l’interrupteur manuel est fermé, le contact va sans cesse passer entre les deux positions, risquant fort au passage de produire un ronronnement sans doute désagréable. Si la vibration est rapide, on entend le bruit d’un vibreur ou d’un buzzer. Si on connecte un petit marteau qui va frapper sur une cloche, on aura ainsi fabriqué un carillon de porte d’entrée. Il existe plusieurs techniques de câblage d’un relais dans cette fonction sonore. En voici une autre qui utilise les symboles conventionnels pour la tension et la masse :

Figure 17.5

Vous vous souvenez sans doute du montage inverseur vu au milieu du Chapitre 8. Il s’agit bien du même circuit. Nous pouvons le simplifier ainsi :

Figure 17.6

Vous savez que la sortie d’un inverseur vaut 1 si l’entrée vaut 0 et inversement. En fermant l’interrupteur de notre circuit d’exemple, le relais ou le transistor qui constitue l’inverseur laisse passer puis interrompt tour à tour le passage du courant. Nous pouvons même aller jusqu’à présenter l’inverseur sans interrupteur, pour qu’il change d’état continuellement :

Figure 17.7

Ce montage pourrait sembler étrange parce que la sortie d’un inverseur est normalement toujours l’inverse de son entrée. Ici, cette sortie est aussi l’entrée ! Ce qui donne sa pertinence à ce montage, c’est l’existence d’un temps de propagation entre l’entrée et la sortie (qu’il s’agisse d’un relais, d’un tube à vide ou d’un transistor). Même si l’entrée est dans le même état que la sortie, cette sortie va changer d’état sous peu pour devenir l’inverse de l’entrée, ce qui bien sûr va changer l’état d’entrée, et ainsi de suite.

Mais alors, quel est l’état de la sortie de ce circuit ? En fait, il change sans cesse entre présence et absence de tension. Autrement dit, la sortie alterne rapidement entre 0 et 1.

On appelle justement ce circuit un oscillateur. Il est très différent des circuits de type porte logique vus jusqu’ici. Les circuits précédents changeaient d’état seulement par une intervention humaine ou un changement d’état d’une entrée indépendant de la sortie. Avec un oscillateur, il n’y a plus besoin de présence extérieure. Il fonctionne en autonomie.

Il est évident que faire fonctionner un oscillateur en vase clos n’a pas d’intérêt. Nous verrons plus loin dans ce chapitre et dans le chapitre suivant qu’un tel circuit, utilisé avec d’autres, va constituer un module fondamental de l’automatisation des traitements. Tous les ordinateurs sont animés par un oscillateur qui fait changer l’état de tous les autres éléments de la machine en synchronicité. (Dans les ordinateurs réels, les oscillateurs sont assez sophistiqués, car ils se basent sur un cristal de quartz connecté de manière qu’il vibre de façon très précise et très rapide.)

La sortie de l’oscillateur va donc passer alternativement de 0 à 1. On peut représenter ce comportement de façon schématique avec un diagramme :

Figure 17.8

Dans ce graphique, l’axe horizontal représente l’écoulement du temps de gauche à droite et l’axe vertical, les états 0 et 1 :

Figure 17.9

Ce graphique nous informe que la sortie passe de façon très régulière de 0 à 1. C’est pour cette raison que l’on parle également d’horloge, car le circuit permet de mesurer le temps qui passe, en comptant le nombre d’oscillations (nous simplifions quelque peu).

À quelle vitesse le circuit doit-il osciller ? Combien de fois par seconde la sortie doit-elle basculer entre 0 et1 ? Tout dépend des détails de fabrication. Vous imaginez aisément qu’un bon vieux relais va changer d’état lentement alors qu’un petit relais véloce basculera plus vite. Un oscillateur basé sur des transistors peut changer d’état des millions ou des milliards de fois par seconde.

Parmi les caractéristiques d’un oscillateur, le cycle est la durée qui sépare un changement d’état du même changement d’état suivant :

Figure 17.10

Le temps qu’il faut pour faire un cycle complet correspond à la période d’oscillation. Prenons comme exemple une période de 0,02 seconde. Nous pouvons ajouter une légende pour l’axe horizontal en commençant par un temps initial égal à 0 :

Figure 17.11

La fréquence est l’inverse de la période. Dans l’exemple, la période étant de 0,02, la fréquence est de 1/0,02 soit 50 cycles par seconde. La sortie repasse 50 fois par seconde dans l’état 1 puis dans l’état 0.

L’unité de mesure « cycles par seconde » se comprend facilement, car elle est similaire à nos kilomètres par heure ou grammes par litre. Cependant, on n’utilise plus tellement cette unité. En hommage à Heinrich Rudolph Hertz (1857-1894), première personne à avoir émis et reçu des ondes radioélectriques, il a été décidé de choisir le mot hertz comme unité pour les fréquences. Cette convention a d’abord été adoptée en Allemagne dans les années 1920, puis s’est répandue dans le monde entier au cours des décennies suivantes.

Ainsi, notre oscillateur fonctionne à une fréquence de 50 Hz.

L’objectif de ce chapitre est d’apprendre à construire un équipement permettant de mesurer la vitesse d’un oscillateur.

Pour y parvenir, commençons par étudier une paire de portes NOR (NON-OU) en les interconnectant d’une façon spéciale. Vous savez que la sortie d’une porte NOR n’est dans l’état 1 que lorsque ses deux entrées sont dans l’état 0 :

NOR

0

1

0

1

0

1

0

0

Voici notre circuit initial, constitué de deux portes NOR, de deux interrupteurs et d’une lampe :

Figure 17.12

Vous remarquez des interconnexions étranges : la sortie de la porte NOR de gauche est injectée en entrée de celle de droite, alors que la sortie de cette seconde porte est envoyée en entrée de la première. Cela correspond à une boucle de rétroaction (feed-back). Comme dans l’oscillateur précédent, l’état d’une sortie est renvoyé dans une entrée. Ce principe est typique de la plupart des circuits que nous verrons dans ce chapitre.

Pour exploiter correctement ce montage, il y a une règle à observer : vous pouvez fermer l’interrupteur du haut ou celui du bas, mais jamais les deux en même temps. Toute la discussion qui suit dépend de l’observation de cette règle.

Avant toute intervention, le seul endroit où il y a présence de tension dans ce circuit est la sortie de la porte NOR de gauche, puisque ses deux entrées sont à 0. Nous fermons maintenant l’interrupteur du haut ; la sortie de la porte NOR de gauche passe à 0, ce qui, en conséquence, force la sortie de l’autre porte NOR à 1 et allume la lampe :

Figure 17.13

C’est maintenant que la magie opère : lorsque vous ouvrez l’interrupteur du haut, puisque la sortie de la porte NOR de gauche reste à 0 à partir du moment où une de ses entrées est à 1, la sortie de cette porte de gauche demeure dans le même état : la lampe reste allumée.

Figure 17.14

Nous nous retrouvons dans une situation étonnante, n’est-ce pas ? Les deux interrupteurs sont ouverts, comme dans la situation initiale, et pourtant la lampe reste allumée, situation inédite. Normalement, la sortie d’un circuit ne dépend que de l’état de ses entrées. Ici, on dirait que ce n’est plus le cas. D’ailleurs, vous pouvez même vous évertuer à ouvrir et fermer l’interrupteur du haut sans changer quoi que ce soit à l’état de la lampe. L’interrupteur n’a plus aucun effet sur le circuit parce que la sortie de la porte NOR de gauche reste toujours à 0.

Essayons maintenant de fermer l’interrupteur du bas. De ce fait, l’une des entrées de la porte NOR de droite passe à 1, ce qui force sa sortie à 0 et provoque l’extinction de la lampe. La sortie de la porte NOR de gauche passe à 1 :

Figure 17.15

Vous pouvez maintenant ouvrir l’interrupteur du bas ; la lampe va rester éteinte :

Figure 17.16

Nous sommes revenus dans l’état initial. Ouvrir et fermer l’interrupteur du bas n’aura plus aucun effet sur la lampe. Récapitulons :

En fermant l’interrupteur du haut, on provoque l’allumage de la lampe qui reste allumée même une fois que l’interrupteur est à nouveau ouvert.

En fermant l’interrupteur du bas, on provoque l’extinction de la lampe qui reste éteinte même si l’on ouvre ensuite le même interrupteur du bas.

Le comportement de ce circuit est étrange : la lampe est tantôt allumée quand les deux interrupteurs sont ouverts, et tantôt éteinte dans la même situation. C’est un circuit à deux états stables, lorsque les deux interrupteurs sont ouverts. On parle également de circuit bascule (en anglais flip-flop, désignant aussi les tongs de plage et les trajectoires de certains politiciens). Le circuit bascule a été découvert en 1918 grâce aux efforts des deux radiophysiciens anglais William Henry Eccles (1875-1966) et F.W. Jordan (1881-1941). Ce circuit est intéressant parce qu’il est capable de conserver une information, de la mémoriser. Même si ce qu’il mémorise se résume au numéro du dernier interrupteur qui a été fermé, ce n’est pas rien. Si vous êtes devant un tel circuit et constatez que la lampe est allumée, vous pouvez sans erreur en déduire que le dernier interrupteur à avoir été fermé est celui du haut. De même, si la lampe est éteinte, c’est que l’interrupteur du bas a été fermé en dernier.

Un circuit bascule se comporte un peu comme une balançoire tape-cul. Seuls deux états stables sont possibles, et la balançoire ne reste que très peu de temps dans l’état intermédiaire. En regardant une telle balançoire arrêtée, vous savez par définition quel est le côté qui a été poussé vers le haut en dernier.

Même si cela semble étonnant, les bascules sont indispensables, car c’est grâce à elles que l’on peut doter un circuit de la mémoire d’un état passé. Sauriez-vous compter si vous ne pouviez pas vous souvenir du sous-total précédent ? Quel était le dernier nombre prononcé et lequel serait donc le suivant ? Un montage qui va compter (ce que nous allons voir un plus loin dans ce chapitre) a besoin de circuits bascules.

Il existe plusieurs variantes de bascule. Nous venons de voir la variante la plus simple : la bascule R-S (Reset-Set). Pour donner un aspect plus esthétique à ce composant, les deux portes NOR sont représentées comme dans le schéma suivant, dans un esprit de symétrie. Les sorties reçoivent des noms dédiés :

Figure 17.17

La sortie à laquelle nous connectons normalement une lampe correspond dorénavant à Q. Nous prévoyons une seconde sortie, , qui montre l’état opposé à Q. Lorsque Q vaut 0, vaut 1. Les deux entrées portent dorénavant les noms S pour Set (armer) et R pour Reset (remettre à zéro). L’entrée S sert à forcer la sortie Q à l’état 1 alors que l’entrée R force Q à 0. Lorsque S vaut 1 (ce qui équivaut à fermer l’interrupteur du haut), Q passe à 1 et à 0. Lorsque R vaut 1 (fermeture de l’interrupteur du bas), Q repasse à 0 et passe à 1. Lorsque les deux entrées sont à 0, l’état de la sortie permet de savoir si la dernière action sur Q a été l’état 1 par S ou l’état 0 par R. Les résultats sont indiqués dans le tableau suivant :

Entrées
Sorties

S

R

Q

1

0

1

0

0

1

0

1

0

0

Q

Q

1

1

Interdit

Il s’agit d’une table, ou d’un tableau, de fonctions ou de vérité. Elle montre l’état des sorties en fonction de chaque combinaison des entrées. Puisqu’il n’y a que deux entrées dans une bascule R-S, le nombre de combinaisons d’entrées est égal à quatre. Nous trouvons donc quatre lignes sous les deux d’en-tête.

Intéressons-nous au troisième cas, lorsque S et R sont à 0 : dans ce cas, les sorties sont Q et . C’est-à-dire que l’état des sorties reste celui qu’elles avaient juste avant que S et R passent à 0. La dernière ligne de la table rappelle que le passage simultané de S et de R à 1 est interdit et illégal. Cela ne veut pas dire qu’il est impossible d’amener le circuit dans cet état, mais, si les deux entrées sont à 1, les deux sorties sont à 0 en même temps, ce qui contrevient au principe selon lequel est l’inverse de Q en permanence. Ainsi, lorsque vous réalisez un circuit utilisant une bascule R-S, prenez soin d’éviter toute situation dans laquelle S et R valent 1 simultanément.

On simplifie en général le schéma de la bascule R-S par une petite boîte avec deux entrées et deux sorties :

Figure 17.18

La bascule R-S est intéressante comme premier exemple de circuit qui semble capable de se souvenir laquelle de deux entrées s’est trouvée en dernier dans un certain état. Il serait encore plus intéressant de pouvoir exploiter un circuit se souvenant de l’état 0 ou 1 d’un signal à un moment particulier dans le temps.

Essayons d’imaginer comment devrait se comporter un tel circuit avant de tenter de le construire. Il lui faudrait deux entrées, dont l’une serait nommée Data. Son état pourrait basculer entre 0 et 1. Choisissons de nommer l’autre entrée Mémoriser, soit l’équivalent digital d’une personne « qui demande de noter quelque chose ». Ce signal Mémoriser reste normalement à l’état 0 ; dans ce cas, le signal de l’entrée Data n’a aucun effet sur le circuit. Dès que l’entrée Mémoriser passe à 1, le circuit accepte la valeur qu’il voit sur l’entrée Data. L’entrée Mémoriser peut ensuite repasser à 0, le circuit continuant à maintenir l’état lu sur l’entrée Data. Tout changement ultérieur de l’entrée Data n’a aucun effet.

Ce que nous voulons est donc un composant dont le tableau de vérité est le suivant :

Entrées
Sortie

Data
Mémoriser
Q

0

1

0

1

1

1

0

0

Q

1

0

Q

Dans les deux premiers cas, le signal Mémoriser est à 1 et la sortie Q est dans le même état que l’entrée Data. Dans les deux autres cas, le signal Mémoriser est repassé à 0, la sortie Q conserve l’état dans lequel elle était, même si l’entrée Data change. Nous pouvons donc simplifier la table de fonctions ainsi :

Entrées
Sortie

Data
Mémoriser
Q

0

1

0

1

1

1

X

0

Q

La lettre X signifie « ignorer ». L’état de l’entrée de données n’a aucune importance tant que l’entrée Mémoriser reste à 0. La sortie Q restera dans le même état.

Pour disposer d’un signal Mémoriser en utilisant une bascule R-S, nous devons prévoir deux nouvelles portes AND sur les entrées, comme ceci :

Figure 17.19

Ce schéma ne comporte pas d’entrée Data, mais cela sera réparé sous peu.

Souvenons-nous que la sortie d’une porte AND n’est à 1 que si ses deux entrées le sont. Les entrées Reset et Set n’ont donc aucun effet tant que l’entrée Mémoriser reste à 0.

Au départ, Q est dans un état et dans l’état opposé. Dans l’exemple, Q vaut 0 et vaut 1. L’état de l’entrée Set n’a aucun effet sur celui des sorties puisque Mémoriser reste à 0 :

Figure 17.20

De même, l’entrée Reset n’a aucun effet non plus :

Figure 17.21

Le circuit va fonctionner comme une bascule R-S normale, c’est-à-dire voir la sortie changer d’état à partir du moment où l’entrée Mémoriser passe à1 :

Figure 17.22

Le comportement redevient celui d’une bascule R-S normale parce que la sortie de la porte AND du haut restitue fidèlement l’état du signal Reset, et de même pour la sortie de l’autre porte AND qui retransmet fidèlement le signal Set.

Notre travail de conception n’est cependant pas encore achevé, car nous voulons aboutir à deux entrées et non trois. Comment faire ?

Repartons de la table de fonctions de la bascule R-S initiale. Nous savons que la situation dans laquelle Set et Reset sont à 1 en même temps est à bannir. D’ailleurs, cela n’a aucun intérêt de laisser ces deux entrées simultanément à 0, puisque rien ne change en sortie dans ce cas. Nous obtenons le même effet en forçant l’entrée Mémoriser à 0. Ainsi, les deux entrées Set et Reset n’ont d’intérêt que lorsqu’elles sont dans des états opposés : si Set vaut 0, Reset vaut 1, et inversement.

Nous pouvons donc appliquer deux améliorations au circuit. Nous remplaçons les deux entrées Set et Reset par une seule entrée nommée Data. Son état sera l’équivalent de l’entrée Set ; nous ajoutons un inverseur pour récupérer l’état Reset.

L’autre modification consiste à donner à l’entrée Mémoriser le nom qui a été standardisé : Clock (car c’est l’entrée d’horloge). Bien sûr, ce n’est pas une vraie horloge, mais vous allez sous peu voir que ce signal peut revêtir des caractéristiques typiques d’une horloge, en alternant de façon périodique entre les états 0 et 1. Pour l’instant, contentons-nous de nous souvenir que l’entrée Clock détermine quand l’état de l’entrée Data va être mémorisé.

Voici le circuit amélioré. Une entrée Data remplace l’entrée Set sur la porte AND du bas et un inverseur configure le signal opposé pour remplacer Reset en entrée de la porte AND du haut :

Figure 17.23

Au départ, les deux entrées sont dans l’état 0. La sortie Q aussi, et donc est à 1. Tant que l’entrée Clock reste à 0, l’entrée Data n’a aucun effet sur le circuit :

Figure 17.24

Dès que Clock passe à 1, le circuit lit la valeur qu’il voit sur l’entrée Data :

Figure 17.25

La sortie Q prend dorénavant le même état que l’entrée Data. L’entrée Clock peut redescendre à l’état 0 :

Figure 17.26

Dorénavant, le circuit conserve la sortie dans l’état lu sur l’entrée Data au moment où Clock était dans l’état 1, même si l’entrée Data change par la suite. Elle peut, par exemple, repasser à 0 sans changer quoi que ce soit à la sortie :

Figure 17.27

On appelle ce circuit une bascule de type D déclenchée par niveau. Le D signifie « Data » ou « Donnée ». Le déclenchement par niveau signifie que le circuit mémorise l’état de l’entrée Data pendant que l’entrée Clock est à un niveau spécifique, par exemple 1. (Nous verrons une autre approche en remplacement du déclenchement par niveau un peu plus loin.).

Dans la table de fonctions, nous abrégeons Data en D et Clock en Clk1 :

Entrées
Sorties

D
Clk
Q

0

1

0

1

1

1

1

0

X

0

Q

On parle également de verrou (latch) de type D. Cela signifie tout simplement que le circuit se verrouille sur l’état d’un bit de données et maintient son état. On peut également parler de circuit mémoire 1 bit. Nous verrons dans le Chapitre 19 comment réunir un grand nombre de bascules de ce type pour constituer un véritable espace mémoire de bits et d’octets.

Pour le moment, contentons-nous de mémoriser 1 octet. Nous pouvons en effet réunir huit bascules de type D en réunissant toutes les entrées Clock. Voici le circuit résultant :

Figure 17.28

Ce circuit verrou est capable de mémoriser la valeur d’un octet entier en une étape. Les huit entrées correspondent aux broches D0 à D7 et les huit sorties en bas, aux broches Q0 à Q7. Sur la droite, nous trouvons l’entrée d’horloge Clk qui est normalement à l’état 0. Lorsque ce signal passe à 1, la valeur sur 8 bits présente sur les entrées D est recopiée sur les sorties. Lorsque le signal de l’horloge redescend à 0, la valeur lue reste en place jusqu’au prochain passage à 1 du signal d’horloge. Les sorties inversées ne sont pas utilisées.

Nous pouvons redessiner ce verrou 8 bits en simplifiant les huit entrées de données et les huit sorties sous forme de chemins de données :

Figure 17.29

On peut encore simplifier légèrement en figurant un D et un Q collectifs :

Figure 17.30

Rappelez-vous : dans le Chapitre 14, nous avions simplifié en deux étapes le schéma d’un additionneur constitué de huit éléments sur 1 bit :

Figure 17.31

Dans ce schéma, les huit entrées A et les huit entrées B étaient connectées à des interrupteurs. L’entrée de retenue CI était reliée à la masse pour le premier étage. Les huit sorties S ainsi que la dernière sortie de retenue CO étaient reliées à des lampes.

Nous pouvons combiner le circuit verrou et le circuit additionneur pour former un sous-ensemble pour un circuit plus complexe, par exemple pour mémoriser la sortie de l’additionneur 8 bits dans le verrou 8 bits. On peut aussi envisager de remplacer une des deux lignes d’interrupteurs d’entrée par le verrou 8 bits pour que la sortie du verrou devienne l’une des deux entrées à additionner. Le projet qui suit combine deux concepts pour aboutir à un additionneur à accumulateur qui conserve les sous-totaux multiples :

Figure 17.32

Notez que c’est l’interrupteur à droite intitulé Add qui contrôle le passage à 1 du signal d’horloge du verrou.

Ce montage divise par deux le nombre d’interrupteurs tout en permettant d’additionner plus de deux nombres sans avoir à réentrer les résultats successifs. Au départ, la sortie du verrou ne montre que des 0, tout comme les entrées A de l’additionneur. Vous configurez alors la première valeur sur les interrupteurs puis activez le bouton Add en fermant l’interrupteur et en l’ouvrant juste après. La valeur que vous avez configurée est ainsi mémorisée dans le verrou, ce que confirment les lampes. Vous pouvez alors « saisir » (si on peut dire) la valeur du second nombre sur les interrupteurs. Il ne reste alors qu’à valider avec le bouton Add. Le nombre qui était configuré sur les interrupteurs est ajouté à l’ancien total et le résultat apparaît sur les lampes. Vous pouvez continuer ainsi indéfiniment à saisir de nouvelles valeurs sur les interrupteurs et à les faire additionner au moyen de Add.

Mais cela ne répond pas exactement à vos attentes. Cela fonctionnerait si vous aviez utilisé des relais lents et si vous étiez en même temps capable d’inverser l’état de l’interrupteur Add très rapidement afin de capturer le résultat de l’addition dans le verrou. Le problème est que lorsque l’interrupteur Add est fermé, tout changement au niveau des entrées de données du verrou va être propagé immédiatement sur les sorties, puis revenir dans l’additionneur ; la valeur va être additionnée à celle des interrupteurs et le résultat va revenir dans le verrou, etc. Bref, un rebouclage interminable.

C’est justement ce que l’on appelle une boucle infinie ou infernale. La cause de ce problème est la nature de déclenchement par niveau de la bascule D. Pour que la valeur des entrées de données soit mémorisée dans le verrou, il faut que l’entrée d’horloge Clock voit son niveau passer de 0 à 1. Or, pendant toute la durée du signal d’horloge dans l’état 1, les entrées de données risquent de changer, et ces changements vont être propagés aux valeurs mémorisées en sortie.

La technique de déclenchement par niveau pour l’entrée d’horloge peut suffire dans certaines applications, mais elle ne convient pas à notre projet d’additionneur à accumulation. Nous ne voulons pas que le verrou laisse les données fluctuer pendant le temps où le signal d’horloge est à 1. Il nous faut un verrou qui mémorise les données au moment exact où le signal d’horloge passe de 0 à 1 (ou de 1 à 0). On parle dans ce cas de déclenchement par front montant ou descendant (edge). En voici une représentation graphique :

Figure 17.33

Le passage de 0 à 1 est généralement désigné en tant que front montant (transition positive). Le basculement inverse de 1 vers 0 est appelé front descendant (transition négative).

Une bascule à déclenchement par niveau que nous venons d’utiliser capture les valeurs d’entrée tant que le signe d’horloge est à 1 alors qu’une bascule déclenchée par front montant va prendre en compte les données seulement au moment exact où le signal d’horloge passe de 0 à 1. Dans les deux modes de déclenchement, les changements d’état en entrée n’ont aucun effet sur la sortie tant que le signe d’horloge reste à 0. La différence est que les changements des données d’entrée n’ont plus aucun effet sur les sorties alors même que le signal d’horloge continue à être à 1. Les données d’entrées ne sont prises en compte pour les sorties qu’au moment du passage de 0 à 1, lors du front montant de la valeur 0 à la valeur 1.

Ce comportement est différent de tous ceux rencontrés jusqu’ici. Sa concrétisation pourrait sembler ardue, mais il y a une astuce. On construit aisément une bascule D à déclenchement par front à partir de deux bascules D à déclenchement par niveau, en les interconnectant comme ceci :

Figure 17.34

Dans ce montage, le signal d’horloge Clock contrôle les deux étages, mais vous remarquez que le signal est inversé pour entrer dans le premier étage. Le premier étage va donc fonctionner comme une bascule de type D sauf que l’entrée Data va être lue et mémorisée seulement lorsque le signal de l’horloge est à 0. La sortie du premier étage alimente l’entrée du second, qui est mémorisée normalement, lorsque l’horloge est dans l’état 1. Le résultat final est que les données d’entrée ne sont mémorisées que lorsque l’horloge passe de 0 à 1.

Revoyons le fonctionnement de façon détaillée. Voici d’abord la bascule au repos, les entrées de données et d’horloge étant à 0, tout comme la sortie Q :

Figure 17.35

Puis on fait passer l’entrée de données Data à 1 :

Figure 17.36

Le premier étage de bascule change alors puisque l’entrée d’horloge inversée est à 1. En revanche, le second étage ne change pas puisque son horloge (non inversée) reste dans l’état 0. Nous pouvons maintenant faire basculer le signal horloge vers 1 :

Figure 17.37

Ce changement provoque le basculement du second étage, et le passage à 1 de la sortie Q. Ce qui change, c’est que l’entrée de données peut tout à fait évoluer dorénavant (par exemple, retomber à 0) sans que cela modifie la sortie Q :

Figure 17.38

Ainsi, les deux sorties Q et ne peuvent changer d’état qu’au moment exact où l’entrée d’horloge Clock passe de 0 à 1.

La table de fonctions de cette bascule déclenchée par front montant réclame un symbole spécifique : ce sera une flèche orientée vers le haut. Ce symbole indique un signal qui passe de l’état 0 à 1 :

Entrées
Sorties

D
Clk
Q

0

↑

0

1

1

↑

1

0

X

0

Q

La flèche pointant vers le haut symbolise un front montant : dans ces deux cas, la sortie Q devient la copie de l’entrée de données Data au moment de la transition de 0 vers 1, soit un front montant de l’horloge. Voici le schéma simplifié de cette bascule :

Figure 17.39

Vous remarquez un petit symbole de chevron sur l’entrée de l’horloge Clock. Il indique qu’il s’agit d’une bascule déclenchée par front. On peut symboliser un groupe de huit bascules de ce type en ajoutant le même petit chevron entrant sur l’entrée d’horloge Clock :

Figure 17.40

Ce verrou déclenché par front s’avère idéal pour notre projet d’additionneur à accumulation :

Figure 17.41

En revanche, notre additionneur à accumulation ne gère pas très bien le signal de retenue en sortie Carry Out. Lorsque l’addition de deux nombres dépasse la valeur 255, nous ignorons CO. La lecture des lampes allumées fait croire à une somme inférieure à la vraie valeur. Une solution consisterait à doubler la taille de l’additionneur et du verrou en la portant à 16 bits, ou au moins à une taille supérieure à celle de la plus grande valeur binaire pouvant être obtenue par addition. Mais laissons cela de côté pour l’instant.

Nous devons en effet gérer un autre problème : il n’y a aucun moyen de remettre l’additionneur à zéro pour repartir sur une nouvelle série d’opérations. Il existe une solution pour y parvenir de façon indirecte. Nous avions vu dans le précédent chapitre que l’on pouvait créer des compléments à un et des compléments à deux. Utilisons ce concept : si, par exemple, le sous-total actuel est égal à 1011 0001, nous ajoutons le complément à un sur les interrupteurs (0100 1110) et lançons l’addition. Le total sera égal à 1111 1111. Nous pouvons maintenant rajouter la valeur 0000 0001 avec les interrupteurs (sans l’espace intérieur) et lancer le calcul. À partir de ce moment, toutes les lampes s’éteignent : l’additionneur a été remis à zéro.

Découvrons maintenant un autre modèle de circuit qui exploite une bascule D déclenchée par front. Vous vous souvenez de l’oscillateur qui avait été construit en tout début de chapitre. Sa sortie alterne sans cesse entre 0 et 1 :

Figure 17.42

Connectons la sortie de l’oscillateur à l’entrée d’horloge Clock d’une bascule D déclenchée par front. Connectons par ailleurs la sortiede la bascule à son entrée de données D :

Figure 17.43

La sortie de la bascule est réinjectée sur son entrée. C’est une boucle de rétroaction sur rétroaction ! (Il est vrai que cela pourrait causer un problème dans les faits. Ce genre de circuit peut être construit à partir d’un relais ou d’un autre composant capable de commuter, et ce composant va changer d’état le plus vite possible. La sortie de l’oscillateur étant connectée aux composants qui constituent la bascule, il est possible que ces composants qui sont en aval ne parviennent pas à suivre la cadence de l’oscillateur. Dans cette présentation théorique, nous allons supposer que l’oscillateur travaille moins vite que les bascules qui l’exploitent dans le projet.)

Pour voir ce qui va se produire dans ce circuit, analysons la table de fonctions des différents changements d’état. L’étude va commencer à devenir délicate, aussi allons-nous progresser pas à pas.

Au départ, l’entrée d’horloge Clock est à 0 et la sortie Q aussi. Évidemment, la sortie est à 1 et sa valeur est injectée dans l’entrée D :

Entrées
Sorties

D
Clk
Q

1

0

0

1

Lorsque l’entrée d’horloge passe à 1, la sortie Q recopie l’état de l’entrée D :

Entrées
Sorties

D
Clk
Q

1

0

0

1

1

↑

1

0

L’entrée d’horloge est toujours à 1. En revanche, la sortie bascule à 0 et l’entrée D passe aussi à 0 :

Entrées
Sorties

D
Clk
Q

1

0

0

1

1

↑

1

0

0

1

1

0

L’entrée d’horloge peut ensuite repasser à 0 sans avoir d’effet sur l’état des sorties :

Entrées
Sorties

D
Clk
Q

1

0

0

1

1

↑

1

0

0

1

1

0

0

0

1

0

Le signal d’horloge bascule à nouveau à 1. Du fait que l’entrée D est à 0, la sortie Q passe à 0 et la sortie passe à 1 :

Entrées
Sorties

D
Clk
Q

1

0

0

1

1

↑

1

0

0

1

1

0

0

0

1

0

0

↑

0

1

En conséquence, l’entrée D rebascule à 1 :

Entrées
Sorties

D
Clk
Q

1

0

0

1

1

↑

1

0

0

1

1

0

0

0

1

0

0

↑

0

1

1

1

0

1

Tentons une synthèse du fonctionnement, au demeurant fort simple : lorsque l’horloge passe de 0 à 1, la sortie Q change, soit de 0 à 1, soit l’inverse. La situation se comprend plus aisément avec un diagramme temporel :

Figure 17.44

Lorsque l’entrée d’horloge Clock passe de 0 à 1 (front montant), la valeur de l’entrée D (la même que à ce moment) est copiée dans la sortie Q, ce qui modifie l’état de et de D pour la prochaine transition de l’entrée Clock de 0 à 1.

Nous avons vu en début de chapitre qu’un oscillateur basculait entre 0 et 1 à une certaine fréquence qui se mesure en hertz (abrégé en Hz), c’est-à-dire en cycles par seconde. Si, par exemple, cette fréquence est de 20 Hz, la fréquence de changement d’état de la sortie Q va être divisée par deux, donc de 10 Hz. C’est pourquoi un tel montage, dans lequel la sortie est réinjectée dans l’entrée Data, porte le nom de diviseur de fréquence.

On voit immédiatement un intérêt à mettre en cascade plusieurs diviseurs de fréquence. La sortie d’un premier diviseur peut servir d’entrée d’horloge au diviseur suivant, la fréquence étant divisée par deux à chaque étage. Voici par exemple trois bascules en cascade (nous aurions pu en envisager plus) :

Figure 17.45

Analysons l’évolution des quatre signaux mentionnés dans la partie supérieure du schéma :

Figure 17.46

J’ai fait commencer le diagramme temporel de l’horloge au moment le plus pertinent, mais cette préméditation est justifiée : le circuit va répéter sans cesse ce motif exact. Est-ce que vous ne repérez pas des relations entre signaux que vous connaissez déjà ?

Je vous donne un indice. Présentons les mêmes signaux en ajoutant des états 0 et 1 :

Figure 17.47

Est-ce que vous voyez où je veux en venir ? Tournez cette page d’un quart de tour dans le sens horaire et lisez les quatre séquences de chiffres binaires dans le sens vertical. Vous constatez que chaque ligne correspond à un nombre de 0 à 15 :

Binaire
Décimal

0000

0

0001

1

0010

2

0011

3

0100

4

0101

5

0110

6

0111

7

1000

8

1001

9

1010

10

1011

11

1100

12

1101

13

1110

14

1111

15

Autrement dit, le circuit énumère des valeurs binaires croissantes. En ajoutant d’autres bascules, nous augmentons le nombre de valeurs décomptées. Comme je l’avais indiqué dans le Chapitre 10, lorsqu’une séquence de nombres binaires est croissante, chaque colonne de chiffres, chaque position, alterne entre 0 et 1 deux fois moins souvent que la colonne située à sa droite. C’est exactement ce que simule ce compteur sans le savoir. Lors de chaque front montant du signal d’horloge, les sorties du compteur augmentent de 1, c’est-à-dire s’incrémentent.

Réunissons huit bascules dans un boîtier :

Figure 17.48

On parle ici de compteur asynchrone (ripple), tout simplement parce que la sortie d’une bascule sert d’entrée d’horloge à la suivante. Les changements d’état se propagent par décalage d’un étage au suivant. Évidemment, la dernière bascule change d’état avec un peu de retard. Il existe aussi des compteurs synchrones pour lesquels les états de tous les étages de sortie changent en même temps2.

Les sorties ont été nommées Q₀ à Q₇ et arrangées pour que la première bascule, Q₀, soit tout à fait à droite. Vous pouvez ainsi directement lire une valeur sur 8 bits en branchant des lampes sur les sorties.

J’avais indiqué dans ce même chapitre que nous allions voir comment connaître la fréquence d’un oscillateur. Nous y sommes. Si vous connectez un oscillateur à l’entrée d’horloge du compteur 8 bits, ce dernier affichera le nombre de cycles réalisés. Lorsque le compteur aura atteint la valeur 1111 1111 (soit 255d), il retombera à 0000 0000. On parle dans ce cas de rebouclage (rollover). Une solution basique pour connaître la fréquence consiste à brancher huit lampes sur les sorties du compteur, puis à attendre que toutes les sorties soient à 0 (aucune lampe allumée). On lance alors un chronomètre pour l’arrêter lorsque toutes les lampes s’éteignent à nouveau. On mesure ainsi le temps nécessaire pour 256 cycles d’oscillation. En supposant que la valeur soit de 10 secondes, on peut en déduire que la fréquence de l’oscillateur est égale à 256 / 10, soit 25,6 Hz.

En réalité, un oscillateur est construit sur la base d’un cristal vibrant ; il va fonctionner bien plus rapidement, au minimum à 32 000 Hz (soit 32 kHz), et jusqu’à plusieurs millions de cycles par seconde ou plusieurs MHz. Certains cristaux permettent même d’aller jusqu’au milliard de cycles par seconde, soit le GHz.

Un oscillateur à cristal très répandu propose la fréquence de 32 768 Hz. Cette valeur n’a pas été choisie au hasard. En se servant de ce genre de composant pour animer l’horloge du premier étage d’un diviseur de fréquence, on passe successivement à 16 384 Hz, puis 8 192, 4 096, 2 048, 1 024, 512, 256, 128, 64, 32, 16, 8, 4, 2 et 1 Hz. Cette dernière valeur permet de compter des secondes dans une horloge.

Les compteurs asynchrones souffrent d’un souci : ils ne démarrent pas nécessairement à 0. Lors de la mise sous tension, la sortie Q de chaque étage de la bascule peut se trouver dans l’état 1 ou l’état 0. Une solution habituelle pour résoudre ce problème consiste à prévoir un signal de préparation Clear qui va forcer la sortie Q à 0, quel que soit le niveau de l’entrée d’horloge et de l’entrée Data.

L’ajout d’une entrée de remise à zéro Clear dans une bascule de type D déclenchée par niveau est facile : il ne manque qu’une porte OU. Normalement, l’entrée Clear est à 0. Lorsqu’elle passe à 1, la sortie Q est forcée à 0, comme ceci :

Figure 17.49

La sortie Q est forcée à 0 sous l’effet du nouveau signal, quel que soit l’état des autres signaux d’entrée, ce qui remet la bascule à 0.

L’ajout d’un signal Clear est moins simple dans le cas d’une bascule déclenchée par front. Quitte à ajouter des composants pour le signal Clear, nous pouvons en profiter pour aménager de quoi gérer un autre signal Preset. Alors que Clear force la sortie Q à 0 quel que soit l’état des deux autres entrées, la nouvelle entrée Preset force Q à 1. Si vous êtes en train de concevoir une horloge digitale, les deux signaux Clear et Preset vont vous permettre de régler l’heure.

Voici le circuit d’une bascule de type D déclenchée par front et dotée des deux signaux Preset et Clear. Il est construit à partir de six portes NOR à trois entrées et d’un inverseur. Le résultat perd en simplicité, mais gagne en symétrie :

Figure 17.50

Les deux nouvelles entrées ont priorité sur les entrées Clock et Data. En temps normal, elles sont toutes deux à 0. Lorsque l’entrée Preset passe à 1, Q est forcée à 1 (et Q à 0). Lorsque l’entrée Clear passe à 1, Q est forcée à 0 et Q à 1. (Comme avec les entrées Set et Reset d’une bascule R-S, les deux entrées Preset et Clear ne doivent jamais être en même temps à 1.) Le reste du comportement ne change pas par rapport à celui d’une bascule déclenchée par front :

Entrées
Sorties

Preset
Clear
Data
Clock
Q

1

0

X

X

1

0

0

1

X

X

0

1

0

0

0

↑

0

1

0

0

1

↑

1

0

0

0

X

0

Q

Voici le schéma synthétique d’une bascule de type D déclenchée par front avec les deux entrées Preset et Clear :

Figure 17.51

J’avais cité au cours du Chapitre 15 quelques circuits intégrés de la famille TTL. Si vous comptez utiliser cette technologie TTL, ne perdez pas de temps à construire ces circuits à partir des portes élémentaires. Profitez du circuit nommé 7474 qui est une double bascule D à front montant avec Preset et Clear. Voici son schéma tel que trouvé dans le Data Book TTL pour ingénieurs :

Figure 17.52

Nous avons prouvé que des relais ou des transistors sont capables d’ajouter, de soustraire et de compter en binaire. Nous avons vu également comment une bascule permettait de mémoriser un bit ou un octet. Nous disposons ainsi des deux composants fondamentaux qui permettent de construire un ingrédient essentiel de tout ordinateur : sa mémoire.

Mais prenons d’abord le temps de nous amuser un peu.

1 N.d.T. : dans la suite de ce livre, nous ne traduirons pas le terme Clock en « horloge », car vous le verrez dans toutes les notes et spécifications de composants.

2 N.d.T. : le terme anglais ripple évoque une idée de propagation progressive par petites vagues (ripples), sans alignement chronologique avec un autre événement, donc de façon asynchrone.

Chapitre 18  Construisons une horloge

Construire votre propre horloge digitale ? Ne serait-ce pas un projet passionnant ? Imaginez une de ces horloges comtoises dans leur grand bâti en bois finement sculpté, avec son cadran protégé derrière une petite porte vitrée, et le lent balancement du mécanisme. Et derrière le cadran, une forêt d’engrenages qui conservent le sens du temps grâce à un mécanisme appelé échappement. Un tic-tac se diffuse dans toute la maison et chaque heure est célébrée par des coups de carillon.

En fait, non. Ce n’est pas ce genre d’horloge que nous allons construire. Nous allons évidemment envisager une horloge numérique ou « digitale » qui va afficher les heures, les minutes et les secondes sous forme de chiffres et non avec des aiguilles. La première version de notre horloge ne va même pas afficher les chiffres décimaux attendus, mais des lumières clignotantes qui vont représenter l’heure sous forme binaire.

Oui, je sais : afficher l’heure en binaire est fort peu sympathique ! C’est pourtant une première étape obligée pour pouvoir ensuite la représenter avec les valeurs décimales habituelles. D’autant que, pour être exact, les valeurs binaires que nous allons afficher vont en fait être à mi-chemin entre le pur binaire et le décimal1.

Faisons d’abord l’inventaire des besoins au niveau du nombre de chiffres à afficher. Si nous montrons les secondes et les minutes, il nous faut six chiffres, par exemple :

12 : 30 : 47

Cela correspond à 12h30 et 47 s, c’est-à-dire midi et demi ou minuit et demi dans le système sur deux fois 12 heures. Il faudrait un témoin permettant de distinguer entre les deux, du style AM ou PM.

Une fois traduite en valeurs binaires, la même heure serait présentée ainsi pour les trois nombres 12, 30 et 47 :

1100 : 11110 : 101111

Je ne sais pas ce qu’il en est pour vous, mais je préférerais ne pas devoir lire l’heure sous cette forme. Il me faut tellement de temps pour faire les conversions de tête que l’heure ne sera vraiment plus la bonne lorsque j’aurai fini.

Choisissons une approche plus aisée en affichant chaque chiffre indépendamment. Nous présentons les valeurs binaires des six chiffres 1, 2, 3, 0, 4 et 7 :

0001 0010 : 0011 0000 : 0100 0111

Dans ce format, il nous faut convertir les six chiffres binaires en décimal, mais puisque ce ne sont que des valeurs entre 0 et 9, les opérations mentales sont bien plus faciles. De plus, vous allez voir passer les secondes en regardant évoluer le chiffre de droite, ce qui vous aidera à vous entraîner à lire ces valeurs binaires.

Ce type de représentation hybride porte un nom : il s’agit du décimal codé binaire ou BCD (binary code decimal). Dans ce mode, chaque chiffre est codé indépendamment sur quatre chiffres binaires, selon le tableau suivant :

BCD
Décimal

0000

0

0001

1

0010

2

0011

3

0100

4

0101

5

0110

6

0111

7

1000

8

1001

9

Nous avons déjà rencontré ce genre de liste de valeurs dans d’autres chapitres, mais en général, elle ne s’arrêtait pas au chiffre décimal 9. On voyait notamment les valeurs allant de 10 à 15, soit 1010, 1011, 1100, 1101, 1110 et 1111. Dans le mode BCD, ces valeurs au-delà de 9 sont devenues invalides car BCD ne va que jusqu’à 1001b.

Cet exemple montre bien qu’un ensemble de bits n’indique pas comment il doit être interprété. Lorsque vous faites face à la valeur binaire 1001 1001, vous ne pouvez pas connaître avec certitude la valeur correspondante. Si c’est un entier non signé, c’est l’équivalent de 153d. Si c’est un entier signé en complément à deux (comme vu dans le Chapitre 16), la valeur est – 103d. Et si c’est du BCD, la valeur est 99d (9 et 9).

La notation BCD est très rarement utilisée dans les ordinateurs parce qu’elle rend les opérations arithmétiques telles que l’addition et la soustraction inutilement complexes. En revanche, lorsqu’il s’agit d’afficher des valeurs décimales pour les humains, BCD constitue une solution intermédiaire bienvenue.

La première horloge que nous allons mettre au point dans ce chapitre ne va pas afficher l’heure « humanisée », mais montrera comment les secondes progressent de 00 à 59, puis les minutes de 00 à 59 et, enfin, les heures. En choisissant le format BCD pour l’affichage, nous avons besoin de faire calculer chacun des six chiffres de façon individuelle, en commençant par les secondes. Voici les plages de valeurs dont nous aurons besoin pour chaque position, de droite à gauche :

Secondes, chiffre des unités, de 0 à 9.

Secondes, chiffre des dizaines, de 0 à 5.

Minutes, chiffre des unités, de 0 à 9.

Minutes, chiffre des dizaines, de 0 à 5.

Heures, chiffre des unités, de 0 à 9.

Heures, chiffre des dizaines, soit 0, soit 1.

Le chiffre des secondes croît de 0 à 9 puis reboucle à 0 pendant que le chiffre des dizaines de secondes augmente de 1 par incrémentation. Il passe de 0 à 1 puis à 2, et jusqu’à 5, puis les deux chiffres des secondes repassent à 0 et celui des minutes augmente de 1.

Nous devons prévoir un circuit indépendant pour chacun des six chiffres, et chaque circuit impacte son voisin immédiat.

Commençons par le chiffre des unités des secondes. Interconnectons quatre bascules déclenchées par front un peu de la même façon que pour le compteur asynchrone du Chapitre 17. Chacune des sorties Q des bascules est branchée à une lampe :

Figure 18.1

Notez que dans le Chapitre 17, les bascules étaient interconnectées en partant de gauche à droite alors qu’ici nous travaillons de droite à gauche, car cela permet de présenter les lampes d’affichage dans le bon sens pour lire les valeurs binaires.

Le point d’entrée du premier étage de bascule à droite est le signal de sortie d’un oscillateur fonctionnant à 1 Hz. Avec cette fréquence, la période, soit le temps d’un cycle d’oscillation, correspond à l’inverse de la fréquence (1/1), donc à 1 seconde. Chaque seconde, l’oscillateur fait passer son signal de 0 à 1 et de 1 à 0.

Chaque bascule possède deux sorties Q et dont les états sont opposés par construction. La sortie est connectée à l’entrée de données D ou Data. Lorsque le signal d’horloge passe de 0 à 1, l’état de l’entrée D est propagé à la sortie Q. Lorsque la sortie Q passe de 0 à 1, elle change l’état de la bascule suivante à sa gauche.

Pour la première bascule de droite, la sortie Q sera à 0 pendant une seconde puis à 1 pendant la seconde suivante, allumant la lampe pendant une seconde sur deux. La période est donc égale à 2 secondes et la fréquence de l’oscillateur a été divisée par deux. Avec le second étage, la fréquence est à nouveau divisée par deux, et sa lampe reste allumée 2 secondes puis éteinte 2 secondes. Nous poursuivons ainsi dans les deux autres étages. Nous obtenons au final quatre lampes pour décompter des secondes en binaire :

0000

0001

0010

0011

0100

…

1110

1111

0000

…

Dans cette configuration, les lampes permettent de voir le comptage de 0000 jusqu’à 1111, avant de revenir à 0000. Le cycle dure donc 16 secondes.

Mais ce n’est pas ce que nous voulons ! Nous désirons que les lampes montrent le décompte sur 10 secondes, donc de 0000 à 1001. Arrivés à 1001, nous voulons que le décompte reprenne à 0000.

Fort heureusement, nous avions choisi d’utiliser des bascules dotées d’une entrée de remise à zéro Clear (voyez la légende Clr sur le schéma). Lorsque cette entrée est forcée à 1, la sortie Q est forcée à 0 quel que soit l’état des autres entrées. D’ailleurs, il est possible de forcer à 1 toutes les entrées Clear en même temps, ce qui ramène effectivement tout l’affichage à 0.

À quel moment doit-on effectuer cet effacement ? La valeur 1001, soit 9d, est autorisée, mais la valeur 1010, qui vaut 10d, ne l’est pas. Nous devons donc détecter l’arrivée à 1010 sur les quatre bascules pour forcer leur remise à 0.

Nous pouvons détecter cette situation en mettant en place une porte ET connectée à deux des quatre sorties Q :

Figure 18.2

En réalité, les transistors des bascules vont fonctionner si vite que vous n’aurez jamais le temps de voir la transition de 1010 à 0000. Dès que les sorties Q des quatre bascules parviennent à 1010, la sortie de la porte ET passe à 1, ce qui remet à 0 les bascules. Visuellement, vous ferez face à une transition directe de 1001 à 0000 :

0000

0001

0010

0011

0100

0101

0110

0111

1000

1001

0000

…

Si l’idée d’utiliser les sorties des bascules pour les remettre à 0 vous laisse perplexe, vos craintes sont en partie légitimes. D’autres techniques existent pour parvenir au même but, mais elles sont moins faciles à réaliser. Si vous deviez concevoir une horloge digitale, vous auriez intérêt à profiter des circuits intégrés compteurs BCD (decade counter) qui savent compter de 0000 à 1001 pour revenir automatiquement à 0000.

Après la position des unités des secondes, nous nous intéressons au circuit des dizaines de secondes, qui doit compter de 0 à 5. Lorsque les quatre lampes du chiffre des unités passent de 1001 à 0000, il faut augmenter de 1 le chiffre des dizaines de secondes. Il faut donc qu’un signal passe de 0 à 1 à ce moment précis.

Nous pourrions utiliser la sortie de la porte ET, mais choisissons une autre approche en ajoutant une porte NAND (NON ET) :

Figure 18.3

Je rappelle que l’état de la sortie d’une porte NAND est strictement l’inverse de celui d’une porte ET. La sortie est à 1 sauf lorsque les deux entrées le sont. La porte NAND a ici été câblée pour que les deux entrées passent à 1 lorsque le nombre décompté est 1001, soit 9 décimal. La sortie de la porte NAND passe à 0 à ce moment, puis revient à 1. Cet événement se produit toutes les 10 secondes et le front montant (transition de 0 à 1) va servir d’entrée à une autre bascule à front montant.

Le chronogramme suivant montre le signal ayant une période de 1 seconde, les sorties Q des quatre bascules de droite à gauche et le signal de périodicité 10 secondes :

Figure 18.4

Si vous tournez la page de 90o vers la droite, vous pouvez lire les chiffres et constater qu’ils épousent l’accroissement naturel d’un comptage binaire de 0000 à 1001 avant de reprendre à 0000.

La sortie de la porte NAND sert d’entrée au prochain étage de l’horloge qui compte les dizaines de secondes avec les six chiffres de 0 à 5. Nous n’avons de ce fait besoin que de trois bascules qu’il faut remettre à 0 en atteignant la valeur 110 (6d) :

Figure 18.5

La sortie de la porte NAND passe à 0 lorsque la valeur affichée est 101, soit 5 décimal. Si nous tenons compte du sous-ensemble des unités de secondes, l’horloge sait maintenant compter de 000 0000 à 101 1001, soit de 00d à 59d. À ce moment, les sept bascules repassent à 0. Voici le chronogramme des trois bascules des dizaines de secondes :

Figure 18.6

Tournez la page de 90o pour bien voir la progression des valeurs binaires entre 000 et 101.

Nous disposons maintenant d’un signal ayant une période de 1 minute. Nous allons pouvoir commencer à compter les minutes. Il nous faut quatre autres bascules interconnectées exactement comme celle des secondes, pour les unités des minutes :

Figure 18.7

Cette période de 10 minutes va servir d’entrée à un autre ensemble de trois bascules pour les dizaines de minutes. Le montage est strictement identique à celui des dizaines de secondes :

Figure 18.8

C’est à ce moment précis, le moment où vous pensiez avoir quasiment fini de concevoir votre horloge, que vous pressentez l’ombre d’un désagrément menaçant votre projet.

Dans le cas d’une horloge sur 24 heures, le chiffre des heures va de 0 à 23. Dans les pays anglo-saxons, on a conservé la convention basée sur deux fois 12 heures, ce qui nous oblige à gérer un petit problème. L’heure est bien affichée sur deux chiffres, comme les secondes et les minutes, mais elle ne commence pas à 0. Par convention, on indique 12 pour minuit ou midi, et de 12 on passe à 12.

Mais laissons ce problème en suspens et considérons pour l’instant que les heures s’affichent de 0 à 11 puis reviennent à 0. L’heure 00 : 00 : 00 va correspondre à midi ou à minuit.

Mais gérer les heures réclame un autre aménagement : pour les secondes et les minutes, nous pouvions remettre à 0 le chiffre des unités et celui des dizaines de la même façon. Pour les unités, nous repartions à 0 en atteignant 1010 (10d) et pour les dizaines en arrivant à 110 (6d).

Dans le cas des heures, le chiffre des unités repart à 0 pour la même valeur 1010, ce qui correspond au passage de 9 : 59 : 59 à 10 : 00 : 00. Mais il faut repasser à 0 pour les deux positions lorsque le chiffre des dizaines est à 1 et que le chiffre des unités atteint 0010, soit 2d. Cela correspond au passage de 11 : 59 : 59 à midi ou à minuit que, pour le moment, nous allons représenter sous le format 00 : 00 : 00.

Il faut donc considérer les deux chiffres des heures avec un seul circuit. Dans le schéma suivant, nous utilisons cinq bascules qui permettent de remettre à 0 les deux chiffres dans deux conditions :

Figure 18.9

Les quatre bascules de droite sont implantées presque comme les bascules des unités de secondes et de minutes. Une porte ET les remet à 0 en arrivant à 1010. Une porte NAND fait basculer sa sortie à 1 au même moment.

Nous trouvons également une porte ET à trois entrées tout à fait en haut à gauche. Elle permet de détecter la situation dans laquelle le chiffre des dizaines d’heure est à 1 alors que le chiffre des unités est à 0010 (2d), ce qui donne la valeur BCD 12. Vous pourriez dire qu’il n’y a qu’à laisser l’affichage à 12 puisque c’est ce que l’on veut à terme. Mais n’allons pas trop vite. Vous serez gêné pour afficher 1 comme prochaine valeur. Grâce à cette porte ET à trois entrées, nous pouvons remettre les cinq bascules à 0 et afficher 0 pour l’heure.

Ce circuit affiche correctement la progression du temps avec les heures de 0 à 11. Il ne nous reste qu’un problème à régler : lorsque la sortie Q de toutes les bascules est à 0, nous voulons afficher 12, c’est-à-dire 1 0010.

Pour ce faire, nous ajoutons une porte NOR à cinq entrées, installée ici tout à fait à droite :

Figure 18.10

Vous savez que la sortie d’une porte NOR est l’inverse de celle d’une porte OU (OR). La sortie sera à 1 ici seulement lorsque les cinq entrées seront à 0. Cette sortie sert ensuite d’entrée à deux portes OU pour deux des cinq chiffres. De ce fait, lorsque les cinq bascules ont leur sortie à 0, les lampes affichent la valeur binaire 1 0010, c’est-à-dire 12 en décimal.

Je n’ai pas encore parlé de la porte NAND tout à fait à gauche. Sa sortie est normalement à 1 sauf lorsque l’heure vaut 1 0001, c’est-à-dire 11d. Dans ce cas, la sortie passe à 0. Lorsque la onzième heure est passée, la sortie rebascule à 1. Nous nous servons de cette sortie comme entrée d’une bascule qui pilote l’indicateur AM/PM :

Figure 18.11

Vous pouvez admirer et essayer cette horloge telle qu’elle est construite ici sur le site Web des exemples du livre.

Vous avez remarqué que chacun des six chiffres exploite une porte NAND pour produire un signal d’horloge destiné au chiffre suivant. La sortie de cette porte reste à l’état 1 sauf lorsque les deux entrées passent à 1. Il en résulte une situation étrange au premier démarrage de l’horloge. À ce moment précis, les portes NAND ont toutes leur sortie à 1, ce qui déclenche l’entrée d’horloge de la première bascule de l’étage suivant. Autrement dit, au démarrage de l’horloge, l’heure affichée va être celle-ci :

1:11:10

Pour que votre horloge soit à l’heure, il faudrait donc la mettre sous tension exactement à cette heure-là. Ce serait bien de pouvoir régler l’heure.

Certaines horloges numériques se synchronisent en consultant un site spécial sur Internet ou en se connectant à un satellite GPS, ou encore en recevant un signal radio de synchronisation mondiale. Pour régler manuellement la valeur, on utilise les fameux boutons de réglage en suivant une procédure peu normalisée, et cela réclame en général de se plonger dans des instructions détaillées.

En effet, créer des points de dialogue (des interfaces) entre une machine et un humain est toujours un défi. Essayons de trouver une solution simple.

Les secondes, heures et minutes de notre horloge numérique sont présentées pour être lues de droite à gauche :

Figure 18.12

Nous allons doter notre projet de deux interrupteurs. Le premier va permettre d’augmenter manuellement le nombre de minutes et le deuxième de faire de même pour les heures. Ce n’est pas une solution parfaite : si l’heure affichée est 2 : 55 et qu’il faille la régler à 1 : 50, vous devrez appuyer sur le bouton des minutes 55 fois, puis sur le bouton des heures 11 fois. Mais notre approche est simple à mettre en place et vous n’aurez pas besoin de consulter une documentation avant de savoir régler l’heure.

L’affichage des minutes change lorsque les secondes passent de 59 à 0. Le signal « Période de 1 minute » vaut normalement 1, sauf lorsque le chiffre des dizaines de secondes est 5, auquel cas il vaut 0. De même, le signal « Période de 1 heure » vaut 1 sauf lorsque le chiffre des dizaines de minutes vaut 5.

Notre but est de provoquer un changement d’état de ces deux signaux par utilisation de l’un ou de l’autre interrupteur. Si, par exemple, le signal de période 1 minute est à 1 (ce qu’il est presque toujours), on doit pouvoir le forcer à 0 par appui sur l’interrupteur correspondant. En relâchant l’interrupteur, le signal doit revenir à 1. De même, si le signal vaut 0 (ce qui est le cas dans les secondes de 50 à 59), l’appui sur l’interrupteur doit forcer le signal à 1, et le relâchement de l’interrupteur le laisser revenir à 0.

Autrement dit, nos interrupteurs de réglage doivent faire en sorte que les deux signaux de période une minute et une heure soient forcés dans l’état opposé à leur état normal.

Voici un excellent cas d’utilisation de la porte OU exclusif XOR. Nous l’avions utilisée dans le Chapitre 14 pour additionner deux nombres. La sortie d’une porte XOR est identique à celle d’une porte OU sauf lorsque les deux entrées sont à 1 :

XOR
0
1

0

0

1

1

1

0

Nous avons vu que ce type de porte XOR était indispensable pour les additions, mais nous pouvons également nous en servir plus simplement comme inverseur de signal. Lorsqu’une entrée est à 0, la sortie de la porte XOR est dans le même état que l’autre entrée. Lorsqu’une des entrées est à 1, la sortie est dans l’état opposé à celui de l’autre entrée.

Avec deux portes XOR et deux interrupteurs, nous créons facilement le sous-circuit de réglage manuel de l’horloge :

Figure 18.13

Afficheurs à tubes Nixie

Regarder s’écouler les secondes sur l’afficheur d’une horloge numérique peut s’avérer hypnotisant. Dès le début des années 1970, on a commencé à fabriquer et vendre des horloges numériques utilisant des lampes clignotantes, en les vendant comme objets de décoration à des prix sans aucun rapport avec la simplicité du matériel utilisé. Reconnaissons que ces objets avaient une certaine valeur pédagogique pour ceux qui avaient le désir d’apprendre à lire en binaire, ou du moins en décimal codé binaire BCD.

Mais comme la majorité des gens préférait lire des chiffres décimaux, on a cherché des solutions adéquates. Dans un esprit de rétrotechnologie, l’une des plus esthétiques est l’afficheur à cathodes froides ou Nixie. Il s’agit d’un tube de verre empli de gaz néon. Le tube contient dix filaments ayant la forme des dix chiffres décimaux. Chaque filament est relié à une broche de connexion sur le socle du tube, le chiffre 0 correspondant à la broche de gauche :

Figure 18.14

La figure ne montre pas la broche de masse (elle est supposée). On ne voit pas non plus la grille reliée à la masse et entourant les filaments.

Lorsqu’une tension suffisante est appliquée à l’une des broches, le filament correspondant s’illumine :

Figure 18.15

Ces tubes d’affichage ont été lancés en 1955 par la société Burroughs Corporation. Le nom qui lui a été donné, Nixie, désigne aussi de petits esprits aquatiques, sortes de lutins mythologiques.

Il nous faut six tubes Nixie pour afficher l’heure. Au niveau de l’ingénierie, l’utilisation d’un tel tube est assez simple : il faut prévoir un circuit pour alimenter une par une les broches des chiffres. En réalité, le travail est un peu plus complexe du fait que la puissance nécessaire pour ce genre de composant est largement supérieure à celle que l’on rencontre en général dans les montages à transistors, et plus encore dans les circuits intégrés. Pour fournir une quantité de courant suffisant à ces tubes, il existe des circuits spéciaux de pilotage appelés drivers.

La partie numérique de pilotage d’un tube Nixie doit traduire une valeur décimale codée binaire BCD qui provient des bascules en un signal alimentant le chiffre à allumer. Lorsque la valeur est 0000, nous voulons faire produire du courant sur la broche du chiffre 0. Pour la valeur 0001, c’est la broche du chiffre 1, et pour 1001, celle du 9.

Nous avons rencontré un circuit similaire à la fin du Chapitre 10, lorsque nous avons eu besoin de convertir un nombre en octal sous forme de signaux pour allumer une lampe parmi huit. Il s’agit d’un décodeur. Nous allons repartir de ce principe en l’enrichissant pour gérer des nombres codés en BCD. C’est pour cette raison que l’on parle de décodeur BCD :

Figure 18.16

Au premier contact, ce schéma peut sembler incompréhensible, mais il est tout à fait méthodique. La valeur BCD qui provient des quatre bascules est collectée tout en bas. Les traits rouges témoignent d’une présence de tension. La figure nous informe que le nombre présenté est 0101, soit 5d. Chacun des quatre signaux est inversé, puis différentes combinaisons des signaux directs et inversés alimentent les dix portes ET à quatre entrées. La porte ET de la valeur 5 (la cinquième en partant de la droite) reçoit les signaux suivants sur ses quatre entrées :

le bit de poids faible BCD (celui de droite) ;

le bit BCD suivant après inversion ;

le bit BCD suivant tel quel ;

le bit de poids fort BCD après inversion (celui de gauche).

Dans cette configuration, les quatre entrées sont toutes dans l’état 1 uniquement pour la valeur BCD 0101.

Afficheurs 7-segments

Un autre type de composant d’affichage de valeurs numériques décimales est l’afficheur 7-segments. Il s’agit d’un boîtier rectangulaire qui réunit sur sa face avant sept LED de forme allongée, disposées selon un motif permettant de suggérer tous les chiffres :

Figure 18.17

Ce composant est doté de sept broches de données à l’arrière, une par segment, et d’une huitième broche pour le contact de masse. En appliquant une tension à une combinaison choisie des broches, on allume les segments correspondants, ce qui permet de dessiner les différents chiffres décimaux :

Figure 18.18

Pour éviter toute erreur de connexion de ce composant, chacun des sept segments reçoit une lettre d’identification :

Figure 18.19

La table suivante montre quels segments il faut allumer pour chacun des chiffres décimaux de 0 à 9 :

Digit
a
b
c
d
e
f
g

0

1

1

1

1

1

1

0

1

0

1

1

0

0

0

0

2

1

1

0

1

1

0

1

3

1

1

1

1

0

0

1

4

0

1

1

0

0

1

1

5

1

0

1

1

0

1

1

6

1

0

1

1

1

1

1

7

1

1

1

0

0

0

0

8

1

1

1

1

1

1

1

9

1

1

1

1

0

1

1

Nous disposons déjà, grâce au circuit précédent, des signaux correspondant à chacun des dix chiffres décimaux. Ce sont en effet les sorties des portes ET du décodeur BCD qui vient de servir à piloter les tubes d’affichage Nixie.

Intéressons-nous au segment a du composant d’affichage. Il doit être allumé pour les chiffres 0,2, 3, 5, 6, 7, 8 et 9. Autrement dit, les sorties de huit des dix portes ET doivent servir d’entrées à une porte OU à huit entrées :

Figure 18.20

La même approche peut s’appliquer aux autres segments b à g. Mais il y a peut-être une solution plus astucieuse.

En effet, si le segment a doit être allumé pour huit des dix chiffres décimaux, c’est qu’il ne doit pas l’être pour deux chiffres : le 1 et le 4. On peut donc utiliser les deux seules portes ET de ces chiffres en entrée d’une porte NOR à deux entrées :

Figure 18.21

La sortie de cette porte NOR reste à 1 sauf pour les chiffres 1 et 4. Dans ces deux cas, le segment d’afficheur horizontal du haut ne doit pas être allumé.

Les deux approches que nous venons de voir ne sont pas interchangeables. Dans certains cas, vous aurez besoin d’éteindre totalement l’afficheur, sans présenter aucun chiffre. Pour y parvenir, aucun des dix signaux des portes ET du décodeur BCD ne doit être à 1. La première approche, avec la porte OU à huit entrées, assure cette condition, mais l’astuce simplificatrice avec la porte NOR à deux entrées va laisser briller le segment du haut.

Si vous acceptez que l’afficheur sept segments présente toujours un chiffre ou un autre, vous pouvez construire un décodeur pour les segments d’affichage en aval d’un décodeur BCD comme ceci :

Figure 18.22

Vous remarquez que nous utilisons des portes NOR pour cinq segments, mais une porte OU et un inverseur pour les deux autres.

Vous voyez aussi que la porte ET tout à fait à droite est une pseudo-porte : elle n’est connectée à rien. En effet, pour le chiffre 9, toutes les portes NOR et l’inverseur lui-même sont sollicités, mais pas la porte OR.

Ce genre de composant afficheur 7-segments permet d’afficher également les six lettres de la numération en base hexadécimale. Il faut cependant se mettre d’accord sur une convention pour le dessin de certaines lettres. En effet, la lettre B doit pouvoir se distinguer du chiffre 8 et la lettre D, du 0. La solution consiste à utiliser des lettres minuscules pour ces deux lettres. Nous obtenons ainsi les six lettres A, b, C, d, E et F :

Figure 18.23

Pour réaliser un afficheur alphanumérique, donc avec les vingt-six lettres de l’alphabet, il faudrait un composant avec un peu plus de segments, notamment des diagonales. Les solutions disponibles dans le commerce sont des afficheurs à 14 ou à 16 segments.

Vous vous demandez peut-être s’il faut prévoir des circuits pour décoder chaque chiffre à afficher. En théorie oui, mais vous pouvez simplifier le circuit avec la technique de multiplexage. Elle permet d’utiliser les mêmes connexions du circuit de décodage pour plusieurs chiffres, tour à tour bien sûr. Le principe est de permuter les entrées du décodeur parmi plusieurs sources, alors que les sorties du décodeur vont vers tous les afficheurs en même temps. En maintenant une synchronisation avec les décalages des traitements du décodeur, il n’y a à chaque instant qu’un seul afficheur relié à la masse, et qui va donc s’allumer. À chaque instant, un seul chiffre est alimenté, mais la rotation d’un chiffre à l’autre est si rapide que l’œil ne le perçoit pas : cela est dû à la persistance rétinienne.

Afficheurs à matrice de points

Une troisième technique pour afficher des nombres et des lettres se fonde sur un composant qui se présente comme une matrice de points. Chaque point est une diode LED ronde positionnée dans une grille. La matrice la plus petite qui permette malgré tout d’afficher tous les chiffres, les signes de ponctuation et les lettres de l’alphabet latin comporte cinq points en largeur sur sept en hauteur. On l’appelle une matrice 5 × 7. En voici une qui affiche le chiffre 3 à gauche :

Figure 18.24

Vous pourriez penser que l’on va allumer indépendamment chacune des 35 LED en réutilisant éventuellement une technique de simplification comme celle vue pour l’afficheur précédent. En fait, cela ne fonctionnera pas très bien. Rappelons qu’il y a 35 petites lampes à contrôler. Pour y parvenir, il faudrait un circuit assez complexe. Une autre approche serait préférable.

Chacun des 35 points lumineux est une diode électroluminescente (une LED). Rappelons qu’une diode est un semi-conducteur symbolisé ainsi :

Figure 18.25

Le courant électrique ne peut traverser une diode que dans un sens ; dans l’exemple, c’est de gauche à droite. Le trait vertical du côté droit du symbole rappelle que la diode bloque le courant qui voudrait venir par la droite.

Une LED est une diode particulière dans le sens où le passage du courant lui fait émettre de la lumière, donc des photons. Ces photons parviennent sur notre rétine comme ceux du soleil. Le symbole de la diode LED est celui d’une diode avec deux petites flèches suggérant l’émission de lumière :

Figure 18.26

En quelques dizaines d’années, les diodes LED sont devenues de plus en plus puissantes et de moins en moins onéreuses. On en trouve dorénavant dans les éclairages de nos domiciles, car elles consomment moins et chauffent bien moins que les anciennes ampoules à incandescence.

Voici comment sont connectées les 35 LED à l’intérieur du composant afficheur 5 × 7 :

Figure 18.27

Chaque diode se trouve à l’intersection d’une ligne et d’une colonne. Les lignes horizontales correspondent aux entrées des diodes et les colonnes, aux sorties. (Il existe le modèle inverse dans lequel les entrées sont dans les colonnes et les sorties dans les lignes, mais cela ne change rien au principe.)

Avec cette approche, le nombre de connexions passe de 35 à 12 pour 7 lignes sur 5 colonnes.

Mais le prix à payer pour cette simplification est qu’une seule ligne ou qu’une seule colonne peut être contrôlée, donc mise sous tension, à chaque instant. En première approche, vous pourriez croire que cela rend le montage inutile, mais il y a une astuce : si les lignes et les colonnes de la matrice sont alimentées tour à tour par un balayage rapide, l’œil humain va croire que tout est affiché en permanence.

Revenons au chiffre 3 que nous avons vu sur la première figure de la matrice. Dans la colonne la plus à gauche, deux LED sont allumées : celle du haut et celle de la deuxième ligne en partant du bas. Pour y parvenir, il faut mettre sous tension les deux lignes et connecter à la masse à la première colonne, comme ceci :

Figure 18.28

Si vous essayez les différents chemins qui mènent à la masse, vous constaterez que tous les chemins sont bloqués, sauf ceux des deux LED qui peuvent ainsi s’allumer.

Pour la deuxième colonne de points, nous pouvons afficher la suite du chiffre 3 en mettant sous tension la LED du haut et celle tout en bas. Il suffit de mettre sous tension les deux lignes et de relier à la masse seulement la deuxième colonne :

Figure 18.29

Le principe reste applicable aux colonnes suivantes. Pour celle tout à fait à droite, nous devons allumer trois LED (les 1, 5 et 6). Nous mettons sous tension les trois lignes correspondantes et connectons à la masse cette seule dernière colonne :

Figure 18.30

Le principe est ainsi défini. Il nous reste à trouver un moyen technique pour alimenter précisément les lignes désirées en tension et relier à la masse une seule colonne à la fois.

Pour nous aider, nous avons fait appel à d’autres diodes, mais cette fois-ci des diodes normales, non lumineuses. Voici comment connecter ces diodes pour former la silhouette du chiffre 3 :

Figure 18.31

En vous éloignant un peu de la page, vous devriez parvenir à voir la silhouette du chiffre 3. Ces diodes correspondent exactement à celles qui doivent briller pour afficher le même chiffre. Ces diodes simples sont utilisées pour leur capacité à bloquer le courant dans un sens et pas dans l’autre. Le dessin du chiffre 3 est ainsi codé dans les interconnexions de ces diodes.

On parle dans ce cas d’une matrice de diodes. Cette matrice incarne une information, ici la position des lumières qui permettent de dessiner dans l’espace le chiffre 3. On peut donc dire que ce genre de matrice de diodes est une sorte de mémoire. Le contenu de la matrice ne peut pas changer (à moins de dessouder et ressouder les diodes). On parle plus précisément d’une mémoire en lecture seule ou ROM (Read-Only Memory).

Cette mémoire morte à diodes sait faire afficher la silhouette d’un chiffre sur une matrice de diodes LED. Vous voyez les cinq lignes d’alimentation des colonnes de la matrice. Le schéma suivant illustre une tension appliquée à la première colonne de gauche :

Figure 18.32

La façon dont les diodes sont interconnectées permet de fournir une tension sur deux lignes à droite. Ce sont ces lignes qui vont alimenter les points de matrice qui doivent être allumés dans la première colonne pour construire le début du dessin du chiffre 3.

En balayant rapidement les différentes colonnes tout en allumant les points désirés, on fait apparaître le chiffre désiré.

Dans le schéma suivant, la matrice de diodes de mémoire et la matrice d’affichage sont interconnectées, et quelques circuits de support ont été ajoutés. Notez que la matrice de mémoire a été légèrement retouchée, car les diodes pointent vers le haut à droite, mais le fonctionnement reste le même :

Figure 18.33

Analysons ce circuit en commençant dans le coin inférieur gauche. Nous devons en effet utiliser un oscillateur très rapide, en tout cas assez pour que l’on puisse allumer et éteindre les lumières à un tel rythme que l’œil humain ne se rende pas compte du subterfuge. L’oscillateur sert d’entrée à un compteur construit avec trois bascules. Nous avons déjà rencontré ce genre de circuit :

Figure 18.34

C’est un circuit comme celui-ci qui a servi à créer un compteur pour construire notre horloge, sauf que nous n’allons compter ici que de 0 à 4, soit en binaire 000, 001, 010, 011 et 100. Dès que l’on atteint la valeur binaire 101, la porte ET ramène les trois bascules à 0.

Ces valeurs binaires servent d’entrées au décodeur 3-vers-5 de gauche. C’est une version simplifiée du décodeur 3-vers-8 vu dans le Chapitre 10 et du décodeur BCD vu plus haut dans ce chapitre. Nous parlons de version allégée, parce que nous n’avons besoin de décoder que des valeurs sur trois chiffres binaires, de 000 à 100, pour obtenir un signal parmi cinq :

Figure 18.35

Le compteur binaire sur trois chiffres progresse de 000 jusqu’à 100 ; les sorties du décodeur deviennent successivement 0, 1, 2, 3 et 4, puis retombent à 0 pour le début du cycle suivant. La sortie « 4 » n’est pas dotée d’une porte ET parce que la sortie Q₂ n’est à 1 que lorsque la valeur binaire est 100, c’est-à-dire 4d.

Les cinq sorties de 0 à 4 correspondent aux cinq colonnes de la matrice de diodes mémoire à gauche du circuit principal. Celle-ci fournit ensuite la tension aux différentes lignes de l’afficheur matriciel à droite. Dans un projet réel, il faudrait songer à ajouter des résistances entre les deux matrices pour limiter le courant, afin d’éviter de détruire les diodes LED (les « claquer »).

Les tensions se propagent ensuite vers une boîte au nom mystérieux : « Drain (sink) 3-vers-5 »3. C’est le seul composant nouveau dans ce circuit.

En effet, nous avons besoin, en synchronisation avec les états des cinq signaux dans la matrice de diodes, d’un moyen de relier une et une seule des cinq colonnes de la matrice d’affichage à la masse pour établir le circuit. Tout au long du livre, nous avons utilisé des circuits et des portes logiques présentant une tension sur leur sortie. On parle dans ce cas de source de courant. Il nous faut dorénavant un composant qui assure l’effet inverse : rediriger une tension vers la masse, en la drainant, comme une sorte d’évier (sink en anglais).

Réutilisons les transistors que nous avons découverts dans le Chapitre 15. Rappelons la symbolique de ce composant :

Figure 18.36

Les trois lettres correspondent à la Base, au Collecteur et à l’Émetteur. Lorsqu’un courant est appliqué à la base, cela autorise le passage d’un courant (plus fort) entre collecteur et émetteur ; autrement dit, nous pouvons envisager de relier l’émetteur à la masse.

Ce drain 3-vers-5 (situé tout en bas du circuit principal) ressemble beaucoup à un décodeur 3-vers-5. D’ailleurs, on peut utiliser le même circuit pour les deux. Il n’y a qu’une seule différence : les sorties des portes ET ainsi que l’entrée Q2 sont injectées dans la base de cinq transistors :

Figure 18.37

Le courant circule en descendant dans les cinq colonnes de la matrice d’affichage, successivement et en fonction de celle des cinq colonnes qui est à chaque instant reliée à la masse grâce à un des transistors.

Nous disposons maintenant d’un circuit complet et capable d’afficher le chiffre 3 sur l’afficheur à matrice de points. Ce n’est pas la fin de notre projet. Comme avec les tubes Nixie et avec les afficheurs 7-segments, nous voulons pouvoir afficher tous les chiffres de 0 à 9. Il faut donc multiplier la matrice de diodes mémoire pour qu’elle exprime les silhouettes des neuf autres chiffres. Il faut également prévoir un autre niveau de sélection qui utilise le décodeur BCD.

Vous trouverez une version animée complète du circuit capable d’afficher tous les chiffres sur le site Web compagnon mentionné en introduction.

Nous avons certainement pris du plaisir à afficher le temps qui passe sous forme de valeurs binaires, mais l’objectif ultime de ce livre n’est pas de construire une horloge. Revenons à nos boutons.

1 N.d.T. : on ne confondra pas le terme horloge utilisé dans ce chapitre pour désigner l’appareil qui compte et affiche l’heure avec les nombreux signaux d’horloge Clock qui servent à contrôler le fonctionnement de tout équipement fondé sur la logique numérique.

2 N.d.T. : dans la version française du livre, nous aurions pu adapter le texte pour construire une horloge sur 24 heures, mais cela aurait trop simplifié les choses. Nous conservons la convention anglo-saxonne avec un format sur 12 heures avant midi (AM) et après midi (PM).

3 N.d.T. : sink peut se traduire par drain, soit un réceptacle de courant, le contraire d’une source.

Chapitre 19  Construction d’une mémoire

Au matin, lorsque nous émergeons de notre nuit de sommeil, notre mémoire remplit des blancs. Nous nous souvenons illico d’où nous sommes, de ce que nous avons fait la veille et de ce que nous avons prévu de faire dans la journée. Ces bribes de mémoire reviennent en un instant, mais quelques lacunes survivent parfois quelques minutes (« Je ne me rappelle pas avoir dormi avec mes chaussettes ! »). En règle générale, nous parvenons à reconstituer notre existence et atteindre un niveau de continuité suffisant pour être paré pour la journée qui démarre.

La mémoire humaine n’est pas trop organisée. Est-ce que vous vous souvenez d’un détail au hasard, par exemple de vos cours de géométrie au lycée ? Vous allez peut-être plutôt vous souvenir du marteau-piqueur qui avait démarré alors que le professeur allait vous expliquer ce que signifie le sigle CQFD.

Et la mémoire humaine n’est pas tout à fait fiable. D’ailleurs, on a sans doute inventé l’écriture pour compenser les trous de mémoire.

Nous écrivons pour relire plus tard. Nous préservons pour réutiliser plus tard. Nous stockons pour accéder plus tard. La fonction de la mémoire est de maintenir inaltérée une information entre deux événements. Dès que nous stockons une information, nous utilisons un type de mémoire ou un autre. Dans le siècle passé, on utilisait comme support pour stocker l’information, du papier, des disques en plastique et des bandes magnétiques, et bien sûr différents types de mémoire électronique d’ordinateur.

Il était même possible de mémoriser des informations au moyen de relais électromagnétiques, à condition de les interconnecter sous forme de portes logiques et de bascules. Nous avons vu qu’une bascule permettait de stocker 1 bit. Ce n’est pas beaucoup, mais c’est un excellent début. À partir du moment où on sait stocker 1 bit, on sait en stocker des milliards.

Dans le Chapitre 17, nous avions présenté la bascule de type D déclenchée par niveau. Nous l’avions construite à partir d’un inverseur, de deux portes ET et de deux portes NOR. Revoici son schéma :

Figure 19.1

Lorsque l’entrée Clock vaut 1, la sortie Q est dans le même état que l’entrée Data. Lorsque Clock tombe à 0, la sortie Q maintient son état, qui était le dernier état de l’entrée Data avant le changement. Toute évolution ultérieure de l’entrée Data n’a plus aucun effet sur la sortie, tant que Clock ne revient pas à l’état 1.

Nous avons utilisé cette bascule dans plusieurs circuits au Chapitre 17. Ici, nous n’allons l’utiliser que dans sa fonction de stockage de 1 bit. Je vais en profiter pour renommer les entrées et sorties afin qu’elles coïncident mieux avec leur emploi1 :

Figure 19.2

La bascule est la même, mais la sortie de donnée Q porte dorénavant le nom Data Out (DO). L’entrée de l’horloge Clock (qui s’appelait d’ailleurs au départ Mémoriser dans le Chapitre 17) porte dorénavant le nom Write ; elle sert d’autorisation d’écriture. L’état de ce signal détermine si l’état de la broche d’entrée Data In (DI) doit être stocké dans la bascule. En temps normal, l’entrée Write est à 0 et l’entrée Data In peut fluctuer sans effet sur la sortie. Pour stocker 1 bit dans la bascule, nous faisons passer Write à 1 puis à 0, comme le montre la table de fonctions suivante. Les entrées et sorties sont abrégées de façon conventionnelle DI, W et DO :

Entrées
Sortie

DI
W
DO

0

1

0

1

1

1

X

0

DO

Dans le Chapitre 17, nous avions précisé que ce circuit porte également le nom de verrou (latch). Dorénavant, vous nous parlons plutôt de circuits mémoire. Voici comment présenter 1 bit de mémoire, sans mentionner tous les détails :

Figure 19.3

On peut également le présenter dans le sens vertical :

Figure 19.4

La position relative des entrées et des sorties n’a aucune importance.

Bien sûr, 1 bit n’est pas un grand volume d’information, mais nous pouvons facilement répéter ce circuit pour stocker 8 bits. Il suffit de relier ensemble les huit signaux de commande Write :

Figure 19.5

Ce circuit 8 bits dispose de huit entrées et huit sorties ainsi que d’une entrée unique Write qui est normalement à 0. Pour écrire 1 octet en mémoire, nous faisons passer l’entrée Write à 1 puis à 0. Voici la vue simplifiée du circuit :

Figure 19.6

Comme auparavant, nous distinguons les 8 bits par un indice. Le bit de poids faible est à l’indice 0 et celui de poids, à l’indice 7.

Pour rester cohérent avec l’approche utilisée pour un seul bit, nous pouvons représenter le circuit 8 bits avec un chemin de données de largeur 8 pour l’entrée et la sortie :

Figure 19.7

Nous pouvons exploiter huit bascules d’une autre façon, bien que cela soit moins évident. Supposez que vous vouliez mémoriser l’état d’un seul signal d’entrée Data In à huit moments différents d’une même journée ou durant huit minutes ou huit secondes. Nous voulons pouvoir récupérer successivement les valeurs sur une seule broche de sortie Data Out.

Autrement dit, nous ne voulons pas enregistrer une valeur sur 8 bits mais huit valeurs successives sur 1 bit.

Le montage est un peu plus complexe pour répondre à ce besoin, mais le circuit mémoire en est simplifié sous un autre aspect : si vous comptez le nombre de connexions requises pour créer le circuit mémoire 8 bits, vous voyez qu’il en faut dix-sept. En revanche, pour stocker huit valeurs différentes sur 1 bit, il n’y a plus que six connexions.

Voyons cela de plus près.

Il nous faut toujours huit bascules pour stocker huit valeurs sur 1 bit. La différence est que toutes les entrées de données Data In sont reliées ensemble, alors que dorénavant les signaux Write sont séparés :

Figure 19.8

Tous les signaux Data In sont interconnectés, mais n’en déduisez pas que nous allons bêtement stocker la même valeur huit fois. Les signaux d’ordre d’écriture Write sont séparés. Chacune des bascules ne va stocker l’état de l’entrée Data In que lorsque son signal d’écriture Write passera à 1. C’est à ce moment que la bascule va prendre en compte la valeur lue sur l’entrée dans la cellule désignée.

Nous pouvons faire encore mieux : au lieu de gérer huit entrées Write distinctes, nous pouvons gérer un seul signal Write et ajouter un décodeur 3-vers-8 pour adresser chacune des bascules individuellement :

Figure 19.9

Nous avions vu un circuit similaire à la fin du Chapitre 10. Il servait à convertir un nombre spécifié en octal au moyen de trois interrupteurs vers une valeur binaire en utilisant huit portes ET connectées chacune à une lampe. Chaque valeur octale faisait s’allumer une seule lampe. Le même genre de circuit a été réutilisé dans le Chapitre 18 pour l’affichage des chiffres de l’horloge.

Les trois signaux S₀, S₁ et S₂ sont ceux de Sélection de bascule. Chacune des huit portes ET reçoit en entrée trois signaux, soit en direct, soit après inversion. Le décodeur 3-vers-8 ainsi constitué est un peu plus polyvalent que celui du Chapitre 10 parce que nous combinons un signal Write aux trois entrées S₀, S₁ et S₂ (les portes ET sont à quatre entrées). Lorsque le signal Write est à 0, toutes les portes ET ont leur sortie à 0. Lorsque Write passe à 1, une seule des huit portes aura sa sortie à 1, tout dépendant de la valeur des trois signaux S₀, S₁ et S₂.

Les sorties DO des huit bascules peuvent être injectées en entrées d’un circuit de type sélecteur 8-vers-1 qui va sélectionner un seul des huit signaux de sortie de données DO des bascules :

Figure 19.10

Dans ce second sous-ensemble aussi, nous utilisons les trois signaux Select ou leur inversion pour alimenter les huit portes ET. Une seule des huit portes peut avoir sa sortie à 1 en fonction des trois signaux de sélection Sx. Mais les signaux de sortie DO des bascules sont également injectés dans les huit portes ET. De ce fait, la sortie de la porte ET sélectionnée montrera le signal de sortie DO correspondant des bascules. Il ne reste plus qu’à centraliser l’état par une porte OU à huit entrées dont la sortie DO délivre l’état du signal sélectionné parmi les huit.

Nous pouvons combiner le décodeur 3-vers-8 et le sélecteur 8-vers-1 avec les huit bascules comme ceci :

Figure 19.11

Vous remarquez que les trois signaux Select (Adresse) vont en même temps au décodeur et au sélecteur. Vous remarquez également l’apparition de ce terme « adresse » pour les noms des signaux de sélection. Ces trois bits représentent une valeur numérique qui désigne la position d’un bit en particulier dans le circuit mémoire. C’est un peu comme une adresse postale, sauf qu’il n’y a que huit adresses possibles sur trois bits : 000, 001, 010, 011, 100, 101, 110 et 111.

Du côté des entrées, les bits d’adresse décident quelle bascule va être autorisée à recopier l’état de l’entrée de donnée Data In sur sa sortie. Du côté sortie, dans la partie inférieure de la figure, les trois mêmes bits d’adresse contrôlent le sélecteur 8-vers-1 pour qu’il ne sélectionne qu’une des huit sorties DO des bascules.

En guise d’exemple, choisissons la valeur 010 pour les trois lignes d’adresse. L’entrée Data In peut valoir 0 ou 1. Nous faisons passer l’entrée Write à 1 puis à 0. Cela correspond à l’action d’écriture mémoire. La valeur trouvée sur la broche DI est stockée en mémoire à l’adresse 010.

Donnons ensuite une autre valeur aux trois bits d’adresse et revenons plus tard. Si le circuit est resté sous tension, nous pouvons restaurer les trois bits d’adresse à la valeur 010 : vous allez constater que l’état de la sortie DO reflète celui qu’avait DI lorsque vous l’aviez stocké en mémoire. C’est l’opération de lecture mémoire ou d’accès mémoire. Vous pouvez bien sûr écrire une autre valeur à cette adresse en faisant passer le signal Write à 1 puis à 0.

Vous pouvez à tout moment choisir l’une des huit valeurs possibles pour les signaux d’adresse et donc stocker huit valeurs différentes sur 1 bit. Ce montage constitué de bascules, d’un décodeur et d’un sélecteur est un circuit mémoire en lecture/écriture parce qu’il permet de stocker des valeurs (de les écrire) puis de les récupérer (de les relire). Du fait que vous pouvez choisir librement une des huit adresses à tout moment, on parle également de mémoire à accès aléatoire, en anglais Random Access Memory, qui a donné l’acronyme universel RAM2.

La technologie RAM n’est pas la seule possible pour les circuits mémoire ! À la fin des années 1940, alors qu’il n’était pas encore facile de construire des circuits mémoire avec des tubes à vide, et que le transistor n’existait pas, on utilisait d’autres types de composants pour les circuits mémoire, notamment des tubes de mercure. On envoyait une pulsation depuis un bout du tube, et celle-ci se propageait jusqu’à l’autre bout comme une vague. Il fallait lire les pulsations l’une après l’autre. Il existait par ailleurs des lignes à délai utilisées jusque dans les années 19603.

Le circuit mémoire RAM que nous venons de construire est capable de mémoriser huit valeurs distinctes sur 1 bit et peut être schématisé ainsi :

Figure 19.12

Les cellules mémoire élémentaires peuvent être configurées de diverses manières. Dans notre exemple, la structure RAM (RAM array) est de type 8 × 1 (on prononce « 8 fois 1 »). Chacune des huit valeurs correspond à un seul bit. Pour connaître le nombre de bits pouvant être stocké au total dans une structure mémoire, il suffit de faire la multiplication. Ici, 8 × 1 donne 8 (bits).

Pour créer des structures plus spacieuses, il suffit de relier ensemble des structures élémentaires. Si vous disposez de huit modules 8 × 1, vous pouvez interconnecter tous les signaux d’adresse et tous les signaux d’écriture Write pour aboutir à un bloc RAM 8 × 8 :

Figure 19.13

Vous constatez que les entrées Data In et les sorties Data Out ont dorénavant 8 bits de large. Chaque des 8 octets pouvant être stocké est récupérable au moyen de son adresse unique sur 3 bits.

Si l’on y réfléchit bien, construire une structure à partir de huit modules 8 × 1 va entraîner une duplication inutile de toute la logique de décodage et de sélection. Vous avez par ailleurs remarqué qu’un décodeur 3-vers-8 et un sélecteur 8-vers-1 sont en fait très similaires. Dans les deux cas, il y a huit portes ET à quatre entrées, la sélection se faisant grâce à des signaux Select ou Adresse. Dans un circuit réel, les portes ET seraient partagées entre décodeur et sélecteur.

Voyons s’il est possible d’utiliser une approche plus efficace pour construire une structure RAM. Au lieu de stocker 8 octets dans une structure 8 × 8, passons directement à la quantité double pour stocker 16 octets dans une structure 16 × 8. Nous voulons aboutir à quelque chose qui puisse se représenter ainsi :

Figure 19.14

Dorénavant, il nous faut 4 bits d’adresse, puisque nous devons identifier un par un 16 octets différents. Le nombre de bits pouvant être stocké dans cette structure mémoire est égal à 128, soit 16 × 8 ; le circuit devra donc réunir 128 bascules. Vous vous doutez qu’il devient difficile de montrer les détails d’une structure 16 × 8 dans une page de ce livre. Je vais donc vous la présenter en plusieurs parties.

En début de chapitre, nous avions vu que l’on pouvait symboliser une bascule stockant 1 bit avec une boîte avec une entrée de données Data In, une entrée d’écriture Write et une sortie de données Data Out :

Figure 19.15

On appelle fréquemment cellule mémoire le circuit élémentaire permettant de mémoriser un seul bit. Implantons 128 cellules sous la forme d’une matrice de huit colonnes sur seize lignes. Chaque ligne de huit cellules contiendra 1 octet. Les seize lignes ont donc une capacité de 16 octets. Nous n’en montrons que trois :

Figure 19.16

Pour l’instant, nous ne nous intéressons pas à la sortie DO. Vous remarquez qu’il y a un signal d’écriture Write commun à tous les bits de chaque octet, puisque l’octet doit être écrit en une seule opération. Les signaux Write sont identifiés par un indice de W₀ à W₁₅, ce qui correspond donc à seize adresses.

Les signaux DI des données d’entrée sont connectés autrement. Tous les bits de poids fort (les plus significatifs) de tous les octets sont interconnectés, de même pour les autres positions binaires de chaque octet. Le partage de cette liaison n’a aucun effet négatif puisque seul l’octet pour lequel le signal d’écriture est actif sera écrit.

Pour stocker la valeur d’un octet parmi les seize, nous devons fournir une adresse sur 4 bits, ce nombre permettant effectivement seize valeurs différentes, qui sont les seize octets de notre circuit mémoire. Nous avons vu que la ligne d’adresse de notre matrice 16 × 8 est sur 4 bits. Il reste cependant à convertir l’adresse pour faire basculer à l’état actif un seul des seize signaux d’écriture Write. C’est le but du décodeur 4-vers-16 :

Figure 19.17

Rassurez-vous, vous ne verrez pas de schéma de décodeur plus complexe que celui-ci dans ce livre ! Chaque porte ET possède quatre entrées pour recevoir les quatre signaux d’adresse ou leur état après inversion. En sortie des portes ET, j’ai indiqué les numéros correspondant à la valeur qui résulte du décodage des 4 bits d’adresse.

Le décodeur participe à la production de l’autorisation d’écriture pour les 16 octets du bloc mémoire : chacun des signaux de sortie du décodage vient en entrée d’une autre porte ET qui combine ce signal avec le signal commun d’écriture Write :

Figure 19.18

On obtient les 16 signaux permettant de stocker un octet en mémoire par l’entrée Data In (ces signaux Wx sont présentés à gauche dans la Figure 19.16).

Nous en avons fini avec les entrées. Tournons-nous vers les sorties de données des 128 cellules mémoire. Cette partie est plus délicate à gérer parce que chacune des huit colonnes de bits doit être gérée individuellement. Voici, par exemple, un circuit simplifié qui se charge de gérer la colonne la plus à gauche de la matrice 16 × 8 vue dans la Figure 19.16. On voit que les signaux de sortie DO des seize cellules mémoire sont combinés avec les seize sorties du décodeur 4-vers-16 en sorte de ne sélectionner qu’une seule cellule mémoire :

Figure 19.19

Nous voyons à gauche les seize sorties du décodeur 4-vers-16. Chacune est injectée en entrée d’une porte ET, l’autre entrée de porte étant une ligne de sortie DO issue de l’une des seize cellules mémoire de la première colonne de la Figure 19.16. Les sorties de ces portes ET sont envoyées dans une gigantesque porte OU à seize entrées pour obtenir en sortie le bit DO₇ qui est le bit de poids fort de l’octet de données de sortie DO.

Le pire dans ce circuit, c’est qu’il faut le dupliquer pour chacun des 8 bits d’un octet !

Fort heureusement, une autre approche est possible.

À chaque instant, une seule des seize sorties du décodeur 4-vers-16 va être dans l’état 1, c’est-à-dire présentant une tension. Les autres sorties seront toutes à 0, donc à la masse. De ce fait, une seule des portes ET verra sa sortie à 1, et seulement si par ailleurs la sortie DO de cette cellule mémoire est également à 1, toutes les autres restant à 0. Autrement dit, la seule raison d’être de la porte OU géante est de détecter qu’au moins une de ses entrées est passée à l’état 1.

Eh bien, nous pourrions nous passer de cette porte OU géante si nous pouvions interconnecter toutes les sorties des portes ET les unes aux autres. Il faut en effet préciser qu’il est généralement proscrit de connecter directement des sorties de portes logiques parce qu’il suffit qu’une des entrées soit à 1 et une autre à 0 pour créer un joli court-circuit (entre tension et masse). Mais on a trouvé une technique pour éviter ce problème ; elle consiste à employer un transistor, comme ceci :

Figure 19.20

Si le signal provenant du décodeur 4-vers-16 est à 1, le signal DO sur la broche de l’émetteur du transistor sera dans le même état que le signal DO de la cellule mémoire : soit une tension, soit pas. En revanche, si le signal du décodeur 4-vers-16 est à 0, le transistor est bloqué et sa sortie, l’émetteur du transistor, sera ni à la tension (1), ni à la masse (0). On peut ainsi relier entre eux tous les signaux DO d’une rangée de transistors sans craindre de court-circuit.

Voici la même structure mémoire simplifiée pour montrer la connexion des sorties Data Out. Les sorties du décodeur 4-vers-16 sont à gauche et les signaux DO, en bas. Nous n’avons pas figuré les petites résistances de rappel des lignes DO qui garantissent un état 1 ou 0 sans flottement.

Figure 19.21

Vous trouverez une configuration mémoire 16 × 8 complète sur le site compagnon du livre (voir l’introduction pour l’adresse).

La façon dont nous venons d’utiliser des transistors forme le principe d’un circuit que l’on appelle le tampon trois états ou triple état (tri-state buffer)4. En effet, la sortie de ce composant de stockage de donnée transitoire peut être dans un état parmi trois : à la masse (niveau logique 0), sous tension (niveau logique 1) ou ni l’un ni l’autre, comme si la sortie n’était connectée à rien (« en l’air »).

Voici le symbole schématique d’un tampon triple état :

Figure 19.22

L’aspect fait songer au circuit tampon que nous connaissons déjà, mais enrichi d’un signal d’autorisation Enable. Si ce signal vaut 1, la sortie recopie fidèlement l’état de l’entrée. Sinon, la sortie est flottante, comme connectée à rien, ni à un voltage ni à la masse.

C’est ce tampon triple état qui permet de s’affranchir de l’interdiction de connecter entre elles plusieurs sorties de portes logiques. On peut relier entre eux des tampons triple état sans craindre un court-circuit à la condition impérieuse qu’un seul des tampons soit activé par Enable à chaque instant.

En général, les tampons triple état sont fabriqués sous la forme d’une batterie de huit pour pouvoir gérer les 8 bits d’un octet avec un seul signal Enable :

Figure 19.23

Cette batterie de tampons triple état est symbolisée de façon simplifiée comme ceci :

Figure 19.24

Dans les diagrammes ultérieurs du livre, lorsque je suis obligé de représenter ces tampons alors qu’il n’y a pas assez de place dans la figure, j’emploie l’abréviation TRI5.

Nous venons de voir que des tampons triple état permettaient de sélectionner 1 octet parmi 16 dans une structure mémoire 16 × 8. J’aimerais que cette structure dispose aussi d’une entrée d’activation Enable :

Figure 19.25

Lorsque le signal Enable vaut 1, les signaux de sortie DO incarnent l’octet stocké à l’adresse fournie. Si Enable vaut 0, Data Out n’a aucune signification.

Puisque nous savons construire un circuit pour mémoriser 16 octets, essayons d’en stocker le double, ou plutôt le quadruple. Non, multiplions par 8. Et pourquoi pas seize fois plus ?

Pour y parvenir, il suffit de réunir seize matrices mémoire 16 × 8 et de les interconnecter comme ceci :

Figure 19.26

Nous ne faisons figurer que trois des seize circuits mémoire de 16 octets. Tous partagent les entrées Data In. Les sorties Data Out des seize structures sont interconnectées sans danger puisque que nous y avons prévu des tampons triple état. Remarquez qu’il y a deux blocs d’adresses sur 4 bits : les 4 bits d’adresse A₀ à A₃ identifient chacune des seize structures mémoire alors que les bits d’adresse A₄ à A₇ forment l’entrée de sélection du décodeur 4-vers-16 qui permet de contrôler laquelle des seize structures mémoire reçoit un signal d’écriture Write et laquelle reçoit un signal d’activation Enable.

Nous avons ainsi multiplié par 16 la capacité mémoire totale, puisque nous pouvons dorénavant stocker 256 octets. Simplifions la schématique de ce joli circuit :

Figure 19.27

Remarquez que les lignes d’adresse sont dorénavant représentées sur 8 bits. Un composant mémoire stockant 256 octets ressemble à un mur de casiers de poste restante de 256 cases, chaque case contenant une valeur unique sur 1 octet.

Et si nous allions encore plus loin ? Réunissons seize de ces structures de 256 octets et ajoutons un autre décodeur 4-vers-16 pour pouvoir les sélectionner avec 4 autres bits d’adresse. La capacité mémoire est ainsi multipliée par 16 et nous disposons alors d’un immense espace de 4 096 octets. Voici le résultat :

Figure 19.28

La différence est le bus d’adresse, dorénavant de 12 bits de large.

Montons une dernière fois d’un niveau. Regroupons 16 structures de 4 096 × 8 en ajoutant encore un décodeur 4-vers-16. Les adresses sont maintenant sur 16 bits, et la capacité mémoire atteint 65 536 octets ou 64 ko :

Figure 19.29

Nous pourrions continuer ainsi, mais vous avez compris le principe.

Vous avez sans doute remarqué que le nombre d’octets pouvant être stockés dans un circuit mémoire dépend directement du nombre de bits d’adresse. Sans aucun bit d’adresse, on ne peut stocker qu’une seule valeur. Avec 4 bits d’adresse, on stocke 16 valeurs différentes et, avec 16 bits, on en stocke 65 536. La relation correspond à l’équation suivante :

Nombre d’éléments en mémoire = 2nombre de bits d’adresse

La capacité d’un circuit mémoire RAM de 65 536 octets est annoncée comme égale à 64 kilo-octets, ou ko. Quand on y songe, cela peut sembler bizarre. Comment est-il possible que 65 536 se réduisent à 64 000 ?

Souvenez-vous que la valeur 2¹⁰ est égale à 1 024 en décimal, et c’est la valeur que l’on a choisi d’abréger en 1 ko. On utilise souvent le terme « kilo » dans le système métrique européen (le mot vient du grec khilioi qui signifie « mille »). 1 kg vaut 1 000 mg et 1 km vaut 1 000 m ; 1 ko devrait donc valoir 1 000 octets, mais c’est faux : il en vaut 1 024.

La raison de cette divergence est liée au système métrique qui est évidemment basé sur les puissances de 10, puisqu’il s’agit d’un système décimal. Les valeurs binaires sont, comme vous le savez, basées sur des puissances de 2 ; il y avait peu de chances que les deux bases coïncident au niveau attendu. Les puissances de 10 sont 100, 1 000, 10 000, etc. Les puissances de 2 sont 4, 8, 16, 32,128, 256, 512 et 1 024. Aucune puissance entière de 10 n’équivaut à une puissance entière de 2.

À certains moments dans la progression, les deux échelles de puissance se rapprochent. On peut dire que 1 000 étant assez proche de 1 024, on peut poser une égalité approximative avec le symbole mathématique correspondant :

2¹⁰ ≅ 10³

Il n’y a rien de magique dans cette relation : elle signifie simplement qu’une certaine puissance de 2 vaut à peine plus qu’une certaine puissance de 10. C’est grâce à cela que l’on peut parler d’un kilo-octet de mémoire même si, en réalité, ce n’est pas exactement 1 000 octets, mais 1 024.

Vous vous refuserez de dire qu’un circuit mémoire de 64 ko contient 64 000 cellules de 1 octet. Il en contient 1 536 de plus. Pour montrer que vous maîtrisez le sujet, vous parlez de « 64 k » plutôt que de « soixante-cinq mille cinq cent trente-six octets ».

Chaque bit supplémentaire pour l’adressage double la quantité de mémoire pouvant être adressée. Chaque ligne de la liste suivante illustre ce doublement progressif :

1 kilo-octet = 1 024 octets = 2¹⁰ octets (soit environ 10³ octets)

2 kilo-octets = 2 048 octets = 2¹¹ octets

4 kilo-octets = 4 096 octets = 2¹² octets

8 kilo-octets = 8 192 octets = 2¹³ octets

16 kilo-octets = 16 384 octets = 2¹⁴ octets

32 kilo-octets = 32 768 octets = 2¹⁵ octets

64 kilo-octets = 65 536 octets = 2¹⁶ octets

128 kilo-octets = 131 072 octets = 2¹⁷ octets

256 kilo-octets = 262 144 octets = 2¹⁸ octets

512 kilo-octets = 524 288 octets = 2¹⁹ octets

1 024 kilo-octets = 1 048 576 octets = 2²⁰ octets (soit environ 10⁶ octets)

Vous remarquez que les kilo-octets de la première colonne sont évidemment tous des puissances de 2.

Puisque nous acceptons que 1 kilo-octet corresponde à 1 024 octets, nous pouvons continuer en considérant que 1 024 ko représentent 1 mégaoctet abrégé en 1 Mo. (En grec, méga signifie « grand ».) L’espace mémoire continue à doubler par étapes :

1 mégaoctet = 1 048 576 octets = 2²⁰ octets (soit environ 106 octets)

2 mégaoctets = 2 097 152 octets = 2²¹ octets

4 mégaoctets = 4 194 304 octets = 2²² octets

8 mégaoctets = 8 388 608 octets = 2²³ octets

16 mégaoctets = 16 777 216 octets = 2²⁴ octets

32 mégaoctets = 33 554 432 octets = 2²⁵ octets

64 mégaoctets = 67 108 864 octets = 2²⁶ octets

128 mégaoctets = 134 217 728 octets = 2²⁷ octets

256 mégaoctets = 268 435 456 octets = 2²⁸ octets

512 mégaoctets = 536 870 912 octets = 2²⁹ octets

1 024 mégaoctets = 1 073 741 824 octets = 2³⁰ octets (soit environ 10⁹ octets)

Nous devons adopter ensuite le préfixe grec giga qui signifie « géant ». 1 024 Mo forment 1 gigaoctet, abrégé en Go.

Mille fois plus que 1 Go nous amène au téraoctet (téra signifie « monstre »), soit 2⁴⁰ (l’équivalent décimal est d’environ 10¹²). À l’octet près, cela représente 1 099 511 627 776. L’abréviation du téraoctet est To.

Alors qu’un gigaoctet vaut environ un million d’octets, un téraoctet en vaut environ un millier de milliards.

Si nous allons encore plus loin, nous arrivons au pétaoctet (Po), valant 2⁵⁰ ou 1 125 899 906 842 624 octets, soit environ 10¹⁵ décimal. Mieux encore, l’exaoctet vaut 2⁶⁰ octets ou 1 152 921 504 606 846 976 octets, environ 10¹⁸ ou un milliard de milliards.

Pour une idée concrète des progrès au cours des âges, je rappelle que les ordinateurs personnels que l’on pouvait acquérir à l’époque de la première édition américaine de ce livre en 1999 étaient dotés de 32 ou 64 Mo, rarement 128 Mo, de mémoire vive RAM. Lors de la rédaction de cette nouvelle édition en 2021, les machines grand public disposent de 4, 8 ou 16 Go de mémoire vive RAM. Nous ne parlons pas ici des mémoires de masse de type disque dur ou SSD dont le contenu est conservé après mise hors tension de l’ordinateur.

Les gens adorent adopter des abréviations. Dans les années 1980, on avait l’habitude de dire « Ma machine a 64 k ». Une personne dont la machine possède 33 554 432 octets de mémoire dira plutôt « J’ai 32 Mo », et « 8 Go » est plus vite dit que « huit milliards cinq cent quatre-vingt-neuf millions neuf cent trente-quatre mille cinq cent quatre-vingt-douze octets ».

Certains, mais ils sont rares, expriment les quantités de mémoire en multiple du bit et non de l’octet, comme dans kilobits ou mégabits. Lorsque l’on parle d’espace de stockage de données en mémoire, on parle presque toujours en octets. Les quantités en bits servent pour décrire des débits et des taux de transfert dans les échanges réseau filaires ou sans fil. Une liaison d’accès réseau ou d’accès Internet sera par exemple qualifiée avec une vitesse de x kilobits ou mégabits par seconde.

Vous en savez maintenant assez pour construire des modules mémoire de quelque taille que ce soit, au moins au niveau conceptuel. Dans mes exemples, je m’étais arrêté à 65 536.

Pourquoi m’arrêter ainsi à 64 ko ? Pourquoi pas 32 ou 128 ko ? Tout simplement parce que 65 536 est un nombre joliment arrondi, parce qu’il correspond exactement à 2¹⁶. Un tel circuit mémoire requiert des adresses sur 16 bits, soit exactement 2 octets. En base hexadécimale, la plage d’adresses va de 0000h à FFFFh.

Comme nous l’avons vu, dans les années 1980, les ordinateurs personnels étaient généralement dotés de 64 ko d’espace mémoire. Mais il n’y a pas eu qu’une seule technologie de mémoire vive. Celle basée sur des bascules vues dans les pages précédentes est la mémoire statique. Au cours des années 1980 sont apparus une technologie de circuit mémoire RAM dite dynamique et l’acronyme DRAM. Une mémoire dynamique n’a besoin pour chaque cellule que d’un transistor et d’un condensateur (un composant très utilisé en électronique pour stocker de l’énergie électrique entre deux plaques conductrices séparées par un isolant). Le condensateur maintient une charge électrique pendant un certain temps seulement. Pour que la mémoire DRAM n’oublie pas son contenu, il est nécessaire de récrire fréquemment les données pour les rafraîchir et recharger le condensateur (plusieurs milliers de fois par seconde !).

Les deux types de mémoire RAM statique et dynamique sont dites volatiles, c’est-à-dire qu’il faut maintenir une tension d’alimentation pour que le contenu survive. Dès que l’on coupe le courant, le contenu d’une mémoire volatile disparaît, s’envole.

Il serait intéressant d’un point de vue didactique de disposer d’un panneau de contrôle pour gérer les 64 ko de mémoire, c’est-à-dire y écrire des valeurs ou les en relire.

Un tel panneau serait doté de seize interrupteurs pour définir une adresse, de huit interrupteurs pour définir la valeur sur 8 bits à stocker, d’un interrupteur pour le signal d’écriture Write et de huit lampes pour montrer une valeur :

Figure 19.30

Dans la figure, tous les interrupteurs sont en position 0. Un nouvel interrupteur a été ajouté : Takeover (Local). Son but est de choisir de laisser ou non un équipement extérieur prendre le contrôle des circuits mémoire cachés derrière le panneau. Lorsque cet interrupteur est en position 0, aucun des autres interrupteurs du panneau n’a d’effet sur les circuits. Lorsque l’interrupteur est en position 1, c’est le panneau qui décide de l’exploitation des circuits mémoire qui lui sont connectés. La position 1 peut donc se comprendre comme « prise de contrôle locale ».

La logique associée au nouvel interrupteur est constituée d’une série de sélecteurs 2-vers-1, donc bien plus simples que les décodeurs et sélecteurs évoqués précédemment dans ce chapitre :

Figure 19.31

Lorsque le signal Select vaut 0, la sortie de la porte OU est dans le même état que l’entrée A. Lorsque Select vaut 1, c’est l’entrée B qui est sélectionnée.

Nous avons besoin de vingt-six sélecteurs 2-vers-1 : seize pour les signaux d’adresse, huit pour les interrupteurs des entrées de données Data et encore deux pour l’interrupteur Write et le signal Enable. Voici le circuit résultant :

Figure 19.32

Lorsque l’interrupteur de prise de contrôle local Takeover est en position inactive (0), les signaux d’adresse, les entrées de données, et les signaux Write et Enable qui contrôlent l’ensemble proviennent de l’extérieur, comme montré dans la partie gauche du boîtier des sélecteurs 2-vers-1. Lorsque l’interrupteur est basculé dans l’état local (1), tous ces signaux et valeurs sont définis par les interrupteurs du panneau de contrôle et le signal Enable est forcé à 1. Dans les deux cas, les sorties de données DO de la batterie de mémoires sont envoyées vers les huit lampes et éventuellement ailleurs aussi.

Quand l’interrupteur de prise de contrôle locale est en position 1, les seize interrupteurs de l’adresse sont disponibles pour sélectionner une des 65 536 cellules mémoire. Les lampes vont montrer la valeur sur 8 bits qui a été lue à cette adresse. Les huit interrupteurs Data (D₀ à D₇) permettent de préparer une nouvelle valeur binaire à stocker dans la mémoire sous l’effet de l’interrupteur Write.

Cette structure mémoire 64 ko × 8 ainsi que le panneau de contrôle permettent bien sûr de mémoriser 65 536 valeurs sur 8 bits. Mais ce projet nous ouvre une autre opportunité : un autre circuit va permettre d’exploiter les valeurs qui sont ainsi stockées en mémoire et d’en créer de nouvelles.

Si vous pensez ce scénario improbable, revoyez par exemple la page de couverture du numéro devenu célèbre de janvier 1975 du magazine américain Popular Electronics. Elle présentait le premier ordinateur personnel en kit nommé Altair 8800 :

La face avant de cette machine était un panneau de contrôle avec des interrupteurs et des lampes. Si vous tentez de compter le nombre d’interrupteurs dans le bas du panneau, vous constatez qu’il y en a exactement seize.

Ce nombre serait-il une coïncidence ? Je ne pense vraiment pas.

1 N.d.T. : comme dans le chapitre précédent, nous avons décidé de ne pas traduire les noms des points d’entrée et de sortie des circuits car c’est sous leur nom anglais que vous les rencontrerez dans toutes les documentations techniques des circuits intégrés. Ici, Write signifie bien sûr « écrire ».

2 N.d.T. : nous conservons en français l’acronyme RAM, mais ce type de mémoire que l’on peut effacer et récrire sans limite est aussi désigné par mémoire vive, en contraste avec les circuits de mémoire morte (ROM) qui ne peuvent être écrits qu’une fois.

3 N.d.T. : une autre technologie assez répandue à l’époque exploitait un effet mémoire de petits anneaux de ferrite magnétique appelés tores et fixés à cheval sur une grille de fils rigides.

4 N.d.T. : nous choisissons de parler de tampon « triple état » et non « trois états » pour pouvoir conserver l’abréviation TRI dans les nombreux schémas qui suivent.

5 N.d.T. : cette abréviation rappelle le terme anglais Tri-state buffer.

Chapitre 20  Arithmétique automatique

L’espèce humaine est en général d’une inventivité et d’une ingéniosité époustouflantes, mais aussi puissamment paresseuses. Il est clair que les humains n’aiment pas le travail et cherchent à s’épargner des efforts. Cette aversion est telle (mais notre ingéniosité la surpasse) que nous sommes prêts à passer des heures pour concevoir puis réaliser de superbes appareils qui nous feront économiser quelques minutes de notre charge quotidienne. Peu de perspectives ravissent autant les centres de plaisir d’un cerveau humain que celle de se relaxer dans un hamac tout en surveillant du coin de l’œil le nouveau robot que nous venons de construire pour tondre la pelouse à la perfection.

N’en déduisez pas que je me prépare à vous dévoiler les plans d’une tondeuse à gazon automatique dans les pages qui suivent. Au cours de ce chapitre, nous allons commencer à parcourir le chemin qui mène à des machines bien plus sophistiquées, permettant d’automatiser les processus d’addition et de soustraction de nombres. Je sais, cela ne semble pas si ambitieux, mais ces machines vont acquérir une telle polyvalence qu’elles seront capables à terme de résoudre n’importe quel problème qui se fonde sur les additions, les soustractions, ainsi que toute opération qui réclame de la logique booléenne, ce qui en fin de compte englobe un nombre énorme de problèmes concrets.

Bien sûr, pas de sophistication sans complexité et certaines sections du chapitre vont s’avérer un peu plus ardues. Personne ne vous en tiendra rigueur si vous décidez de passer rapidement sur certains détails. Vous pourriez être de ceux qui se rebellent contre l’automatisation et se promettent de ne jamais réclamer l’assistance d’un appareil électronique pour résoudre ses problèmes mathématiques. Persistez néanmoins dans la lecture, car nous allons aboutir à une machine que l’on peut en toute légitimité considérer comme un ordinateur.

À la fin du Chapitre 18, nous avions vu un additionneur fondé sur un verrou 8 bits piloté par front montant et capable d’accumuler des sous-totaux à partir de valeurs définies sur un jeu de huit interrupteurs :

Figure 20.1

Vous vous souvenez qu’un verrou 8 bits permet de mémoriser une valeur sur 8 bits au moyen de bascules. Au départ, le verrou ne contient que des zéros, et ses sorties sont dans le même état. Avec les interrupteurs, vous programmez la première valeur et l’additionneur calcule la somme de cette valeur et des valeurs nulles du verrou. Le premier résultat est donc identique au premier nombre configuré. Lorsque l’on actionne l’interrupteur Add, la valeur est stockée dans le verrou, ce que confirme l’allumage des lampes correspondantes. Il s’agit d’un verrou déclenché par front : la prochaine valeur ne sera stockée que lors du prochain front montant, donc de la prochaine sollicitation de l’interrupteur Add.

Vous pouvez à ce moment définir le deuxième nombre sur le jeu d’interrupteurs. Cette nouvelle valeur est additionnée à celle mémorisée dans le verrou. Une nouvelle action sur le bouton Add stocke le nouveau sous-total dans le verrou et l’affiche sur les lampes. Vous pouvez ainsi additionner progressivement toute une série de nombres en affichant le sous-total courant. Votre seule contrainte est que les huit lampes ne permettent pas d’afficher une valeur supérieure à 255 décimal.

Lorsqu’un verrou mémoire sert ainsi à mémoriser un sous-total courant, on le désigne en général comme un accumulateur. Vous verrez plus tard qu’un accumulateur peut avoir d’autres usages. C’est en effet un verrou contenant d’abord une valeur, puis le résultat de la combinaison de cette valeur arithmétique logique avec une autre valeur.

La machine à additionner que nous venons de voir souffre d’un souci évident : si vous avez une liste de 100 octets à additionner, vous devez vous asseoir devant la machine et configurer chaque série de 8 bits pour les accumuler dans la machine. Une fois la série terminée, si vous détectez la moindre erreur de saisie d’une valeur, vous n’avez qu’à recommencer depuis le début. Et ce sans évoquer les erreurs passées sous silence.

Il y a peut-être une solution pour éviter cela. Dans le chapitre précédent, nous avons utilisé des bascules pour construire une matrice mémoire RAM de 64 ko de capacité. Nous avons également utilisé un panneau de contrôle réunissant des interrupteurs et des lampes :

Figure 20.2

L’interrupteur intitulé Takeover permet de réaliser localement toutes les écritures et lectures de cette structure mémoire, comme le montre le schéma suivant :

Figure 20.3

Si nous avions mémorisé les 100 octets dans ce circuit mémoire au lieu de les configurer l’un après l’autre sur les interrupteurs, nous aurions beaucoup moins de mal à vérifier les valeurs et même à les corriger avant de relancer les calculs.

Pour alléger les prochains schémas, la matrice mémoire de 64 ko × 8 sera représentée sous forme simplifiée, en sous-entendant le panneau de contrôle et les vingt-six sélecteurs de gestion des lectures et écritures :

Figure 20.4

Cette vue simplifiée sous-entend l’existence d’un équipement permettant de définir, d’écrire et de relire des octets dans le circuit mémoire, que ce soit un panneau de contrôle ou un autre moyen. Je vais parfois ne même pas montrer le signal Enable, et vous pouvez supposer que les tampons triple état des sorties Data Out sont activés si nécessaire, même si le signal n’est pas mentionné.

En guise d’exemple, supposons que nous voulions additionner 8 octets, correspondant aux valeurs hexadécimales suivantes : 35h, 1Bh, 09h, 31h, 1Eh, 12h, 23h et 0Ch. Si vous cherchez à connaître le résultat en vous aidant de la calculette de Windows, ou de macOS en mode programmeur, vous découvrirez que la somme finale est E9h. Essayons cependant de construire une machine qui va réaliser ces additions.

Le panneau de contrôle permet d’indiquer les 8 octets pour les injecter en séquence dans la mémoire RAM à partir de l’adresse mémoire 0000h. Voici à quoi ressemble le début du contenu de la mémoire une fois la saisie des huit valeurs terminée :

Figure 20.5

C’est sous cette forme que je présenterai mes extraits de contenu mémoire dans la suite du livre. Chaque case est une cellule mémoire de 1 octet. L’adresse de la cellule est indiquée ou à deviner sur la gauche. En effet, inutile de numéroter chaque cellule puisque les adresses sont croissantes. Sur la droite, j’ajoute quelques commentaires. Dans cette première figure, le commentaire suggère qu’après l’addition du contenu des huit premières cellules, nous stockons le résultat dans la première cellule libre, qui contient la valeur 00h, et c’est celle à l’adresse 0008h, la neuvième cellule.

Vous n’êtes évidemment pas limité à huit valeurs. Si vous avez cent nombres à additionner, vous les stockez aux adresses de 0000h à 0063h.

Intéressons-nous maintenant aux modalités de connexion de la matrice mémoire RAM à l’additionneur à accumulation défini dans le Chapitre 17 (Figure 17.41). En revoici le schéma général :

Figure 20.6

Sur ce schéma, les interrupteurs en entrée et les lampes en sortie ne sont plus nécessaires puisque nous en avons sur le panneau de contrôle connecté au circuit mémoire. Nous remplaçons les interrupteurs de l’additionneur par les signaux de sorties Data Out du circuit mémoire. Pour remplacer les sorties du verrou qui alimentait les lampes, nous récupérons ces signaux pour les injecter dans les entrées Data In du circuit mémoire :

Figure 20.7

Ce circuit n’est pas encore complet, bien sûr, car il ne montre pas la connexion de l’entrée d’horloge Clock du verrou qui valide le stockage du sous-total. Le schéma ne montre pas non plus ce qui est connecté au signal d’écriture Write du circuit mémoire, ce qui est indispensable pour stocker le résultat final. Je n’ai pas encore fait figurer non plus les 16 bits d’adresse pour choisir la cellule mémoire.

Justement, l’entrée d’adresse dans la mémoire doit être une valeur qui augmente de façon séquentielle en commençant à 0000h, suivi de 0001h, etc. C’est exactement le genre de travail que sait réaliser un compteur bâti avec une série de bascules en cascade, comme celui que nous avions vu dans le Chapitre 17 :

Figure 20.8

Vous remarquez que nous avons élargi le chemin de données de sortie pour montrer qu’il y a cette fois 16 au lieu de 8 bits.

Le compteur fournit donc l’entrée d’adresse du circuit mémoire :

Figure 20.9

J’ai donné à cette machine le nom Additionneur accumulateur automatisé.

En ajoutant un circuit compteur pour produire l’adresse mémoire croissante, nous avons également introduit une nouvelle entrée de signal d’horloge Clock qu’il faut alimenter pour faire progresser le compteur. Nous avons de quoi répondre à ce besoin. Les chemins de données principaux sur 8 et sur 16 bits ont été définis. Il ne reste donc que trois signaux à gérer :

l’entrée d’horloge Clock du compteur ;

l’entrée d’horloge Clock du verrou ;

l’entrée d’écriture Write de la mémoire.

Ces signaux sont englobés dans le groupe des signaux de contrôle. En général, ils correspondent aux phases les plus complexes de la conception d’un circuit tel que celui-ci. Les trois signaux doivent en effet être coordonnés et synchronisés.

L’entrée d’horloge Clock du compteur fait progresser l’adresse par incréments de 0000h à 0001h, etc. Cette adresse permet d’accéder à 1 octet mémoire qui est récupéré et envoyé dans l’additionneur avec la valeur en sortie du verrou. L’entrée d’horloge du verrou déclenche ensuite le stockage du nouveau sous-total. En réalité, l’accès mémoire et l’addition prennent un peu de temps. Autrement dit, l’entrée d’horloge du verrou doit être activée un petit peu de temps après l’horloge du compteur et, de même, le signal d’horloge du compteur doit suivre, avec un petit délai, le signal d’horloge du verrou.

Nous pouvons connecter deux bascules ainsi pour réaliser ce cadencement :

Figure 20.10

L’oscillateur à gauche est un composant qui alterne entre 0 et 1. Il peut s’agir d’un oscillateur à cristal de quartz comme ceux utilisés dans les horloges et les ordinateurs. On peut aussi se contenter d’un interrupteur ou d’un bouton que vous sollicitez pour avancer (lentement) étape par étape.

La première bascule est câblée pour provoquer une division par deux de la fréquence, un peu comme ce que nous avons vu à la fin du Chapitre 17. La sortie Q de la bascule est injectée en entrée d’horloge Clk du compteur, ce qui incrémente la valeur du compteur à chaque front montant de 0 à 1. Voici le chronogramme de la première bascule :

Figure 20.11

La ligne du bas montre le changement de valeur de la sortie du compteur.

L’entrée d’horloge de la seconde bascule fonctionne dans l’état inverse de celle du premier. L’entrée D est la sortie Q de la première bascule. Autrement dit, la sortie Q de la seconde bascule est décalée d’un cycle par rapport à celle de la première. Pour aider à la comparaison, je rappelle dans le chronogramme suivant la sortie du compteur vue dans le chronogramme précédent :

Figure 20.12

La porte ET sur la droite du circuit combine la sortie Q de la première bascule et la sortie Q de la seconde. Le signal produit en sortie de cette porte sera appelé Pulsation.

Ce signal Pulsation va servir d’entrée d’horloge au verrou :

Figure 20.13

Nous devons laisser assez de temps à la valeur provenant du compteur avant qu’elle puisse être exploitée comme adresse mémoire, puis pour que les données lues en mémoire soient additionnées au sous-total courant et stockées à nouveau dans le verrou. L’objectif est que tous les états soient stabilisés avant de déclencher l’enregistrement du nouveau sous-total dans le verrou. Nous voulons éviter tout décalage. La sortie du compteur reste constante quand le signal Pulse est à 1.

L’autre signal dont a besoin mon additionneur à accumulation automatique est celui d’autorisation d’écriture mémoire Write. J’ai déjà indiqué que je voulais stocker le sous-total dans le premier emplacement mémoire contenant la valeur 00h. On peut localiser cette cellule mémoire en surveillant la valeur des signaux de la sortie Data Out du circuit mémoire pour en injecter une copie dans une porte NOR à huit entrées. La sortie de cette porte sera à 1 seulement lorsque toutes les entrées Data Out sont à 0. Nous pouvons ensuite combiner cette sortie avec la sortie Pulsation des bascules :

Figure 20.14

Vous trouverez une version interactive de cet additionneur à accumulation sur le site Web compagnon, comme mentionné en introduction.

Nous n’avons rien prévu pour empêcher notre additionneur d’additionner sans relâche. Tant que l’oscillateur oscille et fournit son signal d’horloge, le compteur égrène des adresses mémoire. Si tous les octets suivants en mémoire contiennent la valeur 00h, le circuit va stocker le sous-total dans chacune des cellules.

Le compteur va finir par atteindre la valeur maximale FFFFh puis reboucler avec la valeur 0000h et recommencer l’addition à accumulation. Dans ce second tour, il va additionner toutes les valeurs qu’il lit en mémoire au sous-total courant.

Pour reprendre le contrôle du processus, vous avez intérêt à prévoir un bouton ou un interrupteur avec une légende du type Clear ou RAZ (Remise à zéro). Le compteur qui génère les adresses mémoire croissantes est bâti sur des bascules déclenchées par front. Il dispose donc sans doute d’une entrée Clear. De même, les verrous sont construits à partir du même type de bascule, et nous avons aussi utilisé des bascules déclenchées par front pour générer les signaux de contrôle. Un bouton Clear permet donc de remettre le compteur et les verrous à 0 et d’arrêter le couple de bascules de pulsation. Vous pouvez alors stocker de nouvelles valeurs puis relancer le processus d’addition.

Le vrai souci de cette version de l’additionneur est qu’il ne sait additionner que des octets. Rappelons que l’octet est limité aux valeurs positives entre 00h et FFh, c’est-à-dire entre 0 et 255 en décimal.

Dans les pages précédentes, j’avais choisi les valeurs suivantes pour les 8 octets servant à tester notre additionneur : 35h, 1Bh, 09h, 31h, 1Eh, 12h, 23h et 0Ch. Le total final était égal à E9h, soit 233d. Supposez qu’il y avait un neuvième octet avec la valeur 20h. Au final, le résultat aurait atteint 109h, une valeur qui ne peut pas être représentée avec seulement 8 bits. La sortie de l’additionneur 8 bits serait tronquée à 09h et c’est cette valeur fausse qui serait stockée en mémoire. La sortie de retenue Carry Out de l’additionneur confirmerait que la somme a dépassé la valeur FFh, mais la version actuelle du circuit ne gère pas l’état de ce signal de contrôle.

Supposons que vous vouliez utiliser cet additionneur pour vérifier les dépôts sur un compte bancaire. Aux USA, les comptes sont gérés en dollars et en cents, par exemple 1,25 $, et c’est le cas dans de nombreux autres pays. Pour stocker cette valeur dans 1 octet, il faut en faire une valeur entière (sans point décimal) en la multipliant par 100, ce qui donne 125 cents, donc 7Dh.

De ce fait, si vous êtes limité à l’octet en largeur pour stocker vos flux financiers, votre plafond financier est de FFh, soit 255 décimal ou 2,55 $.

Il faut donc plus d’octets pour stocker plus d’argent. Essayons avec 2 octets. La plage de valeurs possibles s’étend alors à FFFFh, soit 65 535, ou encore 655,35 $.

C’est beaucoup mieux, mais nous avons également besoin de stocker des valeurs négatives, pour gérer le fait que le compte puisse être débiteur. Cela nous oblige à adopter la convention du complément à deux décrite dans le Chapitre 16. Dans cette convention, la valeur positive maximale sur 16 bits vaut 7FFFh (32767d) et la valeur la plus négative vaut 8000h ( – 32768d). Cela permet de gérer une somme entre – 327,68 $ et +327,67 $.

Ce n’est toujours pas suffisant. Essayons avec 3 octets. En complément à deux, une valeur sur 3 octets va de 800000h à 7FFFFFh, soit entre – 8 388 608 $ et +8 388 607 $. Je pense que cette plage de valeurs devrait suffire à une grande majorité de citoyens. Nous adopterons donc une largeur de 3 octets.

Mais comment pouvons-nous améliorer notre additionneur automatisé pour qu’il fasse ses opérations avec des valeurs sur 3 octets au lieu d’un seul ?

La réponse facile serait d’agrandir le circuit mémoire en largeur unitaire pour qu’il permette de stocker des valeurs sur 24 bits, puis créer des additionneurs et des verrous traitant 24 bits.

Mais ce n’est peut-être pas ce qu’il y a de plus pratique si vous avez déjà investi un certain volume de temps et d’efforts dans la création d’une matrice mémoire 64 ko × 8 et disposez déjà d’un additionneur 8 bits, deux éléments que vous n’avez pas envie de reconcevoir.

Si nous conservons une mémoire de 8 bits de large, des valeurs sur 24 bits peuvent être stockées dans trois cellules mémoire consécutives. Mais se pose alors une question cruciale : dans quel sens stocker ? Et s’ensuit cette autre question : que veut dire la première question ?

Supposons que nous devions stocker la somme de 10 000,00 $, soit 1 million de centimes, ou 0F4240h en hexadécimal. Cela correspond aux 3 octets 0Fh, 42h et 40h. Nous pouvons les distinguer en parlant d’octets Haut, Médian et Bas. Il y a deux façons de les stocker en mémoire. Nous pouvons les stocker dans ce sens :

Figure 20.15

Ou dans l’ordre inverse :

Figure 20.16

Vous vous demandez peut-être quel est le meilleur choix ? Quel sens a été choisi par la profession après des années d’expérimentation pour en constituer un standard ? La réponse est ambiguë parce que certaines machines d’aujourd’hui stockent les données dans un sens et d’autres dans l’autre.

Ces deux approches pour fractionner le stockage de valeurs multi-octets portent les noms de gros-boutisme et de petit-boutisme, termes déjà rencontrés dans le Chapitre 13 à propos du codage des caractères Unicode. J’avais précisé qu’ils s’inspiraient de la nouvelle satirique de Jonathan Swift, Les Voyages de Gulliver (partie 1, chapitres 4 et suivants). Il est question dans ce livre d’un conflit entre deux groupes d’habitants de Lilliput à propos du meilleur bout à partir duquel on devait écaler un œuf dur : le grand ou le petit bout ? Dans le domaine informatique, c’est une différence fondamentale dont chacun a appris à tenir compte.

De prime abord, c’est l’approche gros-boutisme qui paraît la plus logique, puisqu’elle correspond à l’ordre dans lequel nous écrivons les chiffres des nombres. L’approche petit-boutisme donne l’impression de travailler à l’envers, puisqu’elle commence par l’octet de poids faible.

Cependant, lorsque vous lisez en mémoire les octets successifs d’une valeur pour les additionner, vous avez d’abord besoin de l’octet ayant le moins de poids. L’addition de cet octet de poids faible va sans doute générer une retenue dont il faut disposer pour l’addition de l’octet suivant.

C’est pour cette raison que je vais stocker dans ce chapitre mes octets dans le format petit-boutisme, donc avec l’octet de poids faible en premier. Ce choix ne concerne que l’ordre de stockage en mémoire. En ce qui concerne l’affichage des valeurs, je continuerai à les présenter avec l’octet le plus significatif à gauche en premier.

La machine que nous allons concevoir ici va utiliser des termes habituels dans le monde de la gestion bancaire : dépôts et retraits. Ces opérations s’appliquent tout autant aux dépenses et recettes d’une petite entreprise, ou même à un bilan comptable simplifié.

Nous commençons par deux dépôts successifs de 450 $ et de 350 $. Voici comment se présente l’addition au format hexadécimal :

L’addition de chaque paire d’octets en commençant par celle de droite génère une retenue qui impacte la paire suivante.

Effectuons maintenant un retrait de 500 $, soit 00C350h en hexa :

Nous avons vu comment réaliser une soustraction binaire dans le Chapitre 16. Il faut d’abord convertir la valeur à soustraire en son complément à deux puis faire une addition. Pour trouver le complément à deux de 00C350h, nous inversons tous les bits, ce qui nous donne FF3CAFh. En y ajoutant 1, nous arrivons à FF3CB0h. Nous pouvons alors procéder à l’addition :

En décimal, nous obtenons la valeur 30 000, soit 300 $. Effectuons maintenant un autre retrait de 500 $ :

Le résultat est négatif. Notre compte est devenu débiteur ! Pour connaître la valeur exacte, nous inversons tous les bits pour obtenir 004E1Fh puis ajoutons 1, ce qui donne 004E20h, soit 20 000d. Le compte est donc à – 200 $.

Fort heureusement, un montant conséquent est annoncé en encaissement. Nous déposons effectivement 2 000 $, soit 030D40h. Calculons au plus vite le nouveau solde du compte :

Je suis heureux d’annoncer que le résultat est égal à 180 000 en décimal, soit 1 800,00 $.

C’est le genre de calcul que je veux pouvoir faire faire à ma machine. Je veux qu’elle soit capable d’ajouter et de soustraire des valeurs sur 3 octets stockés en mémoire, et je veux qu’elle stocke le résultat en mémoire.

Je veux que la machine soit vraiment capable de soustraire. Par exemple, un retrait de 500 $ doit être stocké dans la machine sous forme de 3 octets valant respectivement 00h, C3h et 50h et pas dans le format en complément à deux. Je veux que la machine se charge des calculs de complément à deux de façon transparente.

Mais si toutes les valeurs sont stockées en mémoire en tant que valeurs positives, les recettes et les dépenses auront le même aspect. Comment les distinguer ?

Il nous faudrait ajouter en mémoire un code permettant d’identifier ce qu’il y a lieu de faire avec chaque valeur. Après avoir mûrement réfléchi, par exemple en y passant la nuit, vous pourriez aboutir à une brillante idée : stocker avant chaque valeur en mémoire un code. Un premier code pourrait signifier « Additionner la valeur sur 3 octets qui suit » alors qu’un autre code servirait à dire « Soustraire la valeur sur 3 octets qui suit ». Voici comment la mémoire serait peuplée pour reproduire l’exemple que je viens d’écrire :

Figure 20.17

J’ai choisi le code 02h pour l’ordre d’additionner la prochaine valeur constituée de 3 octets au sous-total actuel. J’ai ensuite choisi le code 03h pour l’ordre de soustraction. Ces valeurs semblent arbitraires, mais ne le sont pas, comme nous allons le voir bientôt.

De tels codes sont appelés codes d’opérations ou codes d’instructions, souvent abrégés en codop. Ces codes représentent les actions que la machine doit réaliser, par exemple ce qu’elle doit faire des valeurs lues en mémoire. La machine réagit par exemple en réalisant une addition ou une soustraction.

Il devient de ce fait possible de distinguer en mémoire entre les valeurs qui sont des codes d’actions et les valeurs qui sont des données sur lesquelles ces actions s’appliquent. Dans notre exemple particulier, chaque octet de code d’instruction est suivi de 3 octets de données.

Puisque nous avons défini deux codes pour l’addition et la soustraction, choisissons un autre code pour provoquer le stockage du sous-total actuel dans la mémoire ; la valeur doit se trouver juste après le code (dans le sens croissant des adresses). Prévoyons enfin un code pour provoquer l’arrêt de la machine pour que le programme ne continue pas à s’exécuter dans le vide :

Figure 20.18

Baptisons cette merveilleuse machine capable d’actions étonnantes l’Accumulateur triple octet. Comme son prédécesseur, l’Additionneur accumulateur automatisé, elle va accéder au contenu d’un espace mémoire de 64 ko × 8 et accumuler un sous-total au moyen d’un additionneur 8 bits. Mais il faut que le nombre de verrous passe à quatre ; il en faut un pour stocker le code d’instruction et trois pour stocker les trois octets du sous-total courant. Le schéma suivant reprend les différents composants et chemins de données :

Figure 20.19

Ce diagramme a été allégé en ne faisant pas apparaître les nombreux signaux de contrôle requis. En effet, je vais consacrer quasiment toute la fin du chapitre à décrire ces signaux. Le schéma, déjà assez fouillé, ne contient pas non plus les entrées des différents sous-ensembles sur lesquels aboutissent les signaux de contrôle. On ne voit pas notamment les entrées d’horloge Clock du compteur et des verrous, les signaux d’activation Enable des tampons triple état et le signal d’écriture Write de la mémoire RAM.

Comme son prédécesseur, cet accumulateur triple octet exploite un compteur 16 bits qui va générer une adresse mémoire. L’entrée d’horloge du compteur est produite par la même configuration de paire de bascules vue plus haut.

L’accumulateur s’appuie sur quatre verrous : le premier sert à l’instruction machine. Il contiendra tour à tour le code trouvé dans les adresses mémoire 0000h, 0004h, 0008h, etc.

Les trois autres verrous sont distingués par les qualificatifs Haut, Médian et Bas. Ils se partagent les 3 octets du sous-total courant. Ils reçoivent en entrée ce qui a été calculé et proposé sur la sortie de somme Sum de l’additionneur. Les sorties des trois verrous parviennent à des tampons triple état (présentés dans le chapitre précédent). Le signal Enable de ces tampons n’est pas montré. Rappelons que lorsqu’il vaut 1, les sorties sont dans le même état que les entrées, 0 si 0 et 1 si 1. Lorsque le signal Enable passe à 0, les sorties ne sont ni à 0 ni à 1. Il n’y a ni tension positive ni mise à la masse. C’est le fameux troisième état protecteur.

Il y a un signal Enable indépendant pour chacun des trois tampons triple état. À chaque instant, un seul des trois signaux Enable est à 1, ce qui permet d’interconnecter les sorties des trois tampons sans risque de conflit de tension et de masse. Les signaux Enable contrôlent donc laquelle des trois sorties de verrous est disponible sur les entrées Data In de la mémoire RAM et sur les entrées B de l’additionneur.

L’état des trois signaux Enable dépend des 2 bits de poids faible de l’adresse mémoire générée par le compteur. Les octets sont placés en mémoire de façon très méthodique : d’abord l’octet du code d’instruction, puis les 3 octets Bas, Médian et Haut de la valeur, dans cet ordre. Les 4 octets peuvent être distingués en testant les 2 bits les moins significatifs de l’adresse. En effet, ces 2 bits évoluent de 00 à 01, puis à 10, puis à 11 en boucle. Chacune des quatre valeurs correspond donc à un type d’octet parmi les quatre possibles en mémoire :

Si les 2 bits de poids faible de l’adresse valent 00, l’octet à cette adresse est un code d’instruction, un code machine.

Si les 2 bits de poids faible valent 01, l’octet désigné ainsi est l’octet de poids faible, l’octet Bas de la valeur à traiter.

Si ces 2 bits valent 10, c’est l’octet Médian.

Si ces 2 bits valent 11, c’est l’octet Haut.

On peut faire générer les trois signaux Enable des tampons triple état avec un décodeur 2-vers-4 en exploitant les bits de poids faible de l’adresse mémoire que nous identifions ici comme A₀ et A₁ :

Figure 20.20

Le schéma montre également la production du signal d’horloge destiné aux verrous de l’instruction. Ce signal devient actif lorsque les 2 bits de poids faible de l’adresse valent 00 et que, simultanément, le signal Pulsation de la paire de bascules passe à 1. Le code d’instruction reste en place dans le verrou pendant la lecture des 3 octets suivants. Les signaux d’horloge des trois autres verrous sont un peu plus complexes à préparer, mais nous allons voir cela très bientôt.

Parcourons les principales étapes du fonctionnement de cet accumulateur triple octet. Nous supposons qu’au départ tous les verrous ont été remis à 0 ; ils ne contiennent aucune valeur significative. La description s’appuie sur la Figure 20.19.

Le compteur génère l’adresse mémoire 0000h. L’octet trouvé à cette adresse (02h dans l’exemple) est stocké dans le verrou de l’instruction.

Le compteur génère alors l’adresse RAM suivante, 0001h, qui est celle de l’octet de poids faible de la première valeur. Cet octet est stocké dans l’additionneur. (Nous ignorons le composant Compl. 1 pour l’instant en considérant qu’il n’a aucun effet, ce qui est vrai dans le cas d’une addition.) Les 2 bits de poids faible de l’adresse valent 01, ce qui provoque la sélection du tampon triple état de l’octet de données Bas. Mais, puisque ce verrou est dans l’état initial (00h), l’entrée B est aussi à 0. La sortie de somme Sum de l’additionneur est l’octet de poids faible de la première valeur à traiter. Ce résultat est stocké dans le verrou de l’octet de données Bas.

Le compteur propose ensuite l’adresse 0002h qui désigne l’octet Médian du premier nombre. La valeur est stockée dans l’additionneur avec le contenu du verrou Médian, qui vaut 00h au départ. La sortie Sum équivaut à la valeur de l’octet Médian lu en mémoire ; ce résultat est placé dans le verrou de l’octet Médian.

Le compteur arrive à l’adresse RAM 0003h qui désigne l’octet de poids fort qui est alors stocké dans l’additionneur et implanté dans le verrou de poids fort.

Le compteur génère l’adresse 0004h qui contient un code 02h de l’opération d’addition.

Le compteur désigne l’adresse 0005h qui contient l’octet de poids faible du second nombre. Il est placé dans l’entrée A de l’additionneur. Le tampon triple état de l’octet Bas est activé et, comme il contient l’octet de poids faible du premier nombre, cette valeur devient l’entrée B de l’additionneur. Les 2 octets sont additionnés puis le résultat stocké dans le verrou de l’octet Bas.

Le même processus est appliqué aux deux octets Médian et Haut, puis de même pour le nombre suivant.

J’avais défini quatre codes machine pour l’accumulateur triple octet :

02h pour l’addition d’un nombre sur 3 octets qui suit le code ;

03h pour la soustraction du même ;

04h pour écrire le sous-total courant sur 3 octets en mémoire ;

08h pour arrêter l’exécution de la machine.

Nous n’avons besoin de stocker que 4 bits dans le verrou d’instruction. Voici la correspondance entre ces bits et nos quatre instructions :

Figure 20.21

Le code machine reste en place dans le verrou pendant la lecture des 3 octets suivants. Les bits du code d’instruction servent en effet à contrôler d’autres éléments de l’accumulateur triple octet.

Par exemple, le deuxième bit Q₁ est à 1 seulement dans les deux cas correspondant à une opération arithmétique (addition ou soustraction). Ce bit peut donc servir à former les entrées d’horloge des verrous des 3 octets de données. Voici à nouveau le schéma du décodeur 2-vers-4 afin de montrer comment ces sorties sont combinées au bit d’instruction Q₁ et au signal Pulsation des bascules :

Figure 20.22

Comme nous le savons, l’octet du code d’instruction est mémorisé dans son verrou quand les 2 bits de poids faible de l’adresse valent 00. On peut alors se servir de cet octet pendant que les deux mêmes bits d’adresse valent 01, 10 ou 11 pour l’accès aux 3 octets de donnés. Lorsque l’instruction demandée est une addition ou une soustraction, la sortie Q₁ du verrou d’instruction est à 1. En conséquence, les trois portes ET du schéma ci-dessus génèrent les signaux d’horloge pour implanter successivement les 3 octets dans les verrous.

Vous vous êtes peut-être interrogé devant mon choix des valeurs 02h et 03h pour les deux instructions d’addition et de soustraction. Pourquoi pas 01h et 02h ? Ou bien 23h et 7Ch ? J’ai choisi ces deux valeurs pour que les deux actions (addition et soustraction) partagent l’état d’un bit de leur code afin de s’en servir pour contrôler les signaux d’horloge des verrous concernés.

Le bit Q₀ du code n’est à 1 que si la valeur se trouvant dans les 3 octets suivants doit être soustraite. Dans ce cas, il faut traiter le complément à deux de la valeur, ce qui se calcule en cherchant d’abord le complément à un, puis en y ajoutant 1. Le complément à un n’est rien d’autre que l’inversion de valeur de tous les bits. Nous avons vu dans le Chapitre 16 un circuit réalisant cette inversion systématique :

Figure 20.23

Les entrées de ce circuit sont les sorties de données Data Out de la mémoire. Les sorties de cette batterie de portes sont envoyées sur les entrées A de l’additionneur 8 bits. Le signal d’inversion peut provenir directement de la sortie Q₀ du verrou d’instruction, puisque c’est ce bit qui détermine si la valeur est à soustraire plutôt qu’à additionner.

Nous avons dit qu’un complément à deux était un complément à un plus 1. Pour ajouter 1, nous pouvons faire passer à 1 l’entrée de retenue Carry de l’additionneur, mais seulement pour le premier des 3 octets :

Figure 20.24

Lorsque le bit de poids faible du verrou d’instruction est à 1, c’est qu’une soustraction est en cours. Tous les bits des octets de données lus dans la mémoire doivent être inversés. C’est l’objectif du composant légendé Complément à 1. L’entrée de retenue CI de l’additionneur doit être forcée à 1 seulement pour le premier octet. C’est la raison d’être de la porte ET. De façon judicieuse, la sortie 01 du décodeur d’instruction n’est à 1 que pour le premier octet de données.

La présence de la porte OU est liée au fait que l’entrée de retenue de l’additionneur CI doit aussi être armée (à 1) lors de l’addition et de la soustraction du deuxième et du troisième octet de données. J’ai évité jusqu’ici de surcharger les schémas en passant sous silence la gestion de la retenue. Le moment est venu de nous y confronter.

Rappelons que cet accumulateur triple octet porte bien son nom, puisqu’il réunit trois verrous et trois tampons triple état, ce qui permet de stocker et de relire 3 octets d’un sous-total courant :

Figure 20.25

Ce que je viens de dire n’est pas tout à fait exact. En effet, pour prendre en charge le bit de retenue, il faut que deux des verrous puissent mémoriser 9 bits : les 8 bits de la somme et la sortie de retenue Carry Out de l’additionneur. De même, deux des trois tampons doivent pouvoir gérer un bit de retenue : celui de l’addition de l’octet Bas dans l’octet Médian, et celui de l’octet Médian dans l’octet Haut :

Figure 20.26

Vous constatez que le signal de sortie de retenue Carry Out de l’additionneur est injecté dans les deux verrous Bas et Médian, mais que les valeurs sont ensuite envoyées dans les tampons triple état Médian et Haut. Puisque cela pourrait étonner, je vais m’en expliquer.

Lors de l’addition des 2 octets Bas, l’entrée de retenue CI de l’additionneur est à 0 si c’est une addition et à 1 si c’est une soustraction (c’est l’autre entrée de la porte OU). L’addition peut générer une retenue. La sortie de retenue Carry Out de l’additionneur est sauvegardée par le verrou de l’octet de poids faible dans le verrou Bas. Mais il exploite la valeur de cette retenue pour l’addition de l’octet Médian. C’est pour cette raison que l’état du bit de retenue du verrou Bas sert d’entrée au tampon triple état de l’octet Médian. Le même raisonnement s’applique pour la retenue éventuelle de l’addition des octets Médian, retenue qui doit être exploitée pour l’addition des 2 octets Haut.

Nous sommes quasiment au bout de nos peines. Notre machine sait faire des additions et des soustractions. Il nous reste à voir le code opératoire 04h qui provoque l’écriture des 3 octets en mémoire. Le circuit utilise le bit Q2 qui a été stocké dans le verrou d’instruction :

Figure 20.27

Le signal d’écriture Write n’est actif que lorsque les 2 bits de poids faible de l’adresse mémoire valent 01, 10 ou 11, ce qui est le cas pour les 3 octets de données. C’est à cela que sert l’inverseur. Lorsque les bits d’adresse prennent ces trois paires de valeurs (01, 10 ou 11), les trois tampons triple état successifs sont activés. Si le bit Q₂ est à 1, ce qui est le cas pour l’instruction d’écriture Write, et si le signal Pulsation de la paire de bascules est à 1 aussi, l’écriture mémoire des 3 octets est possible.

Il ne reste plus qu’à nous intéresser à l’instruction 08h qui signifie Halte. C’est la plus simple. Lorsque le bit Q₃ du verrou d’instruction est à 1, nous voulons que l’oscillateur qui anime l’ensemble de la machine s’arrête.

Figure 20.28

Une version plus raffinée de ce projet aurait prévu un bouton de remise à zéro Clear afin de vider le contenu des bascules, du compteur et de tous les verrous, et être ainsi paré pour une nouvelle série d’opérations et pour accumuler un nouveau sous-total courant.

Vous pouvez essayer une version interactive de l’accumulateur triple octet sur le site Web compagnon dont l’adresse a été fournie en introduction.

Après cette visite guidée, la raison pour laquelle j’ai choisi des valeurs particulières pour les codes d’instructions devrait se préciser. Dans un but pratique, je voulais pouvoir utiliser directement les bits individuels pour contrôler le fonctionnement du circuit. Un premier bit devait permettre de savoir si l’on faisait une addition ou une soustraction, un autre de confirmer que c’était bien une soustraction seulement, un autre que l’ordre était d’écrire les résultats en mémoire, et un dernier bit devait entraîner l’arrêt du fonctionnement. Si j’avais utilisé des valeurs croissantes 01, 02, 03 et 04 pour les instructions, il m’aurait fallu ajouter des circuits pour parvenir à décoder les valeurs et produire les signaux de commande de contrôle appropriés.

Vous devinez sans doute aussi maintenant pourquoi j’ai choisi de gérer 3 octets de données et non 4. C’est ce qui m’a permis d’exploiter les 2 bits A₀ et A₁ de l’adresse pour contrôler directement plusieurs circuits verrous. Si j’avais utilisé des valeurs sur 4 octets, les codes des instructions auraient été stockés successivement aux adresses 0000h, 0005h, 000Ah, 000Fh, 0012h, etc., ce qui aurait rendu beaucoup plus tortueux le stockage des éléments dans les verrous appropriés.

Le moment est enfin venu de revenir sur les deux termes « matériel » et « logiciel » (hardware et software). L’accumulateur triple octet a clairement montré la frontière entre les deux concepts : le matériel correspond à tous les circuits alors que le logiciel est représenté par les bits des codes des opérations et des données stockés dans la mémoire. On parle ici de logiciel parce que ce sont des informations mentales, qui n’ont rien de matériel. En anglais, on ajoute au terme ware le qualificatif soft pour dire qu’il s’agit d’une chose sans présence physique, « molle », donc abstraite, à la différence de la partie hard, « dure », donc concrète.

Si vous faites une erreur en introduisant une des valeurs numériques ou confondez le code de l’addition avec celui de la soustraction, vous pouvez réparer l’erreur facilement. En revanche, il est beaucoup moins simple de modifier les circuits une fois qu’ils sont câblés ou gravés. Cela reste vrai même dans le cas d’un circuit simulé comme celui du site compagnon de ce livre, qui ne contient pourtant aucune soudure, ni composant réel.

L’accumulateur triple octet illustre la relation intime entre matériel et logiciel. Les codes et les valeurs sont stockés dans des bascules mémoire et les bits qui constituent ces valeurs servent de signaux pour faire interagir ces bascules avec les autres composants matériels. Du point de vue le plus basique qui soit, l’ensemble matériel et logiciel n’est rien d’autre qu’un ensemble de signaux électriques en interaction dans des portes logiques.

Si vous songiez à construire un tel accumulateur triple octet pour vous aider dans la gestion financière d’une auto-entreprise, vous seriez en droit de paniquer si, par bonheur, votre activité commençait vraiment à réussir. Vous risquez en effet d’avoir à manipuler une recette ou une dépense qui dépasse les possibilités des 3 octets que sait gérer cette machine. Le souci est que celle-ci n’est pas facile à faire évoluer : ces 3 octets constituent une limite gravée dans le marbre, pardon, dans le silicium du matériel.

Il nous faut donc considérer cet accumulateur triple octet comme un exercice qui mène à une impasse. En revanche, rien de ce que nous avons appris pour le concevoir n’est perdu. Nous avons au contraire découvert quelque chose de très important.

Nous avons en effet prouvé, et c’est une révélation, qu’il est possible de concevoir une machine pouvant être pilotée par des codes opératoires numériques stockés en mémoire. L’accumulateur triple octet ne reconnaît que quatre codes, mais si on décide de stocker les codes sous forme d’octets, on peut en définir 256 différents, et envisager toutes sortes d’opérations. Chacune de ces tâches reste élémentaire, mais on peut les définir d’une façon suffisamment polyvalente pour qu’en les combinant, elles puissent être plus complexes.

L’équilibre à chercher est celui-ci : les tâches simples sont à faire réaliser par le matériel et les tâches plus complexes sont représentées sous forme de combinaisons de codes d’instructions constituant le logiciel.

Si vous aviez décidé de construire une telle machine polyvalente en 1970, vous auriez vraiment eu du pain sur la planche. Mais dix ans plus tard, vous n’auriez même plus eu besoin de construire la machine ! Vous pouviez acheter dans le commerce un circuit intégré appelé microprocesseur capable de gérer 64 ko de mémoire et d’exécuter presque 256 codes d’instructions différents.

C’est en novembre 1971 qu’a été commercialisé le premier « ordinateur sur une puce ». Fabriqué par la société Intel, il portait la référence 4004, parce que c’était un processeur 4 bits. Il réunissait 2 250 transistors et pouvait accéder à 4 ko de mémoire. En 1972, Intel a lancé son premier microprocesseur 8 bits capable de gérer 16 ko de mémoire : le 8008.

Cette génération de processeurs n’offrait pas une polyvalence et une capacité mémoire suffisantes pour envisager d’en choisir un comme cœur d’un ordinateur personnel doté d’un clavier et d’un écran. Ces premiers processeurs étaient surtout utilisés pour les systèmes embarqués, exploités avec d’autres composants de logique numérique, par exemple pour contrôler des machines ou réaliser des tâches de contrôle industriel dans les usines.

En avril 1974 est apparu le Intel 8080, un processeur 8 bits réunissant environ 4 500 transistors et capable d’accéder à 64 ko de mémoire. Le 8080 se présentait dans un boîtier à 40 broches (à peu près en taille réelle ci-dessous) :

Ce Intel 8080 est devenu célèbre : c’est le processeur du premier ordinateur personnel en kit, le Altair 8800, que l’on aperçoit sur la couverture du magazine Popular Electronics reproduite tout à la fin du Chapitre 19. C’est le grand-père des microprocesseurs 16 bits Intel adoptés dans le premier ordinateur personnel IBM PC apparu en août 1981.

Pendant ce temps-là, la société Motorola créait elle aussi des microprocesseurs, à commencer par le Motorola 6800 apparu en 1974 également. Ce microprocesseur 8 bits pouvait accéder à 64 ko de mémoire. Une version simplifiée en avait été produite par la société MOS Technology en 1975 sous la référence MOS 6502, et c’est ce circuit qui a été utilisé par Steve Wozniak (né en 1950) pour créer le fameux Apple II lancé en juin 77.

Les deux familles Intel 8080 et Motorola 6800 étaient assez proches sur certains aspects : des microprocesseurs 8 bits se présentant dans un boîtier rectangulaire à 40 broches et capables d’accéder à 64 ko de mémoire. En revanche, le jeu d’instructions était tout à fait différent. Il y avait une autre différence fondamentale : j’ai parlé plus haut du grand-boutisme et du petit-boutisme pour l’ordre de stockage des octets des valeurs en comportant plus d’un. Le Motorola 6800 avait opté pour le gros-boutisme et stockait l’octet de poids fort en premier. Le Intel 8080 était petit-boutiste en stockant d’abord l’octet de poids faible.

À compter du prochain chapitre, je vais vous guider dans les entrailles d’un Intel 8080, en tentant de le re-concevoir. Je n’utiliserai que les composants élémentaires que nous connaissons déjà : portes logiques, bascules, additionneur, verrous et tampons triple état.

Je n’irai pas jusqu’à reconstruire entièrement un 8080, un projet trop ambitieux. Ma version n’aura pas la puissance du véritable composant, mais nous mènera suffisamment loin dans l’étude qu’à la fin du parcours, vous aurez acquis une compréhension vraiment approfondie de ce qui se passe à l’intérieur d’un ordinateur.

Chapitre 21  L’unité arithmétique et logique ALU

De nos jours, ce que l’on appelle « ordinateur » est un assemblage complexe d’une foule de composants. Néanmoins, on peut les distribuer en trois catégories principales :

la mémoire ;

l’unité centrale de traitement CPU (Central Processing Unit) ;

les équipements d’entrée et de sortie I/O (Input/Output) que l’on regroupe sous le terme périphériques.

Nous avons vu dans le Chapitre 19 comment est fabriqué et organisé un composant mémoire à accès aléatoire1 appelé RAM, et comment chaque octet est stocké au moyen d’une adresse. Nous avons vu dans le Chapitre 20 que les circuits mémoire permettaient de stocker des valeurs numériques et des codes d’instructions qui déclenchent des actions que les circuits de contrôle font réaliser pour manipuler les valeurs. De façon plus générale, la mémoire peut contenir du texte, des images, de la musique, des films et tout ce qui peut être représenté sous forme numérique, avec des 0 et des 1. Les codes d’instructions stockés dans la mémoire correspondent au terme générique code, toutes les autres données en mémoire étant simplement des données. Autrement dit, un circuit mémoire ne peut contenir que du code ou des données.

Pour ses échanges avec le monde extérieur, un ordinateur est constitué de périphériques d’entrée/sortie. La nature et le nombre de périphériques dépendent du contexte d’utilisation de la machine, selon que c’est un ordinateur de bureau, un portable, un ordinateur de poche ou encore d’un équipement embarqué, par exemple, dans un four à micro-ondes, un robot aspirateur, etc.

Les périphériques d’entrée/sortie les plus visibles dans le cas d’un ordinateur de bureau sont évidemment l’écran d’affichage, le clavier et la souris, avec parfois également une imprimante installée non loin. Dans le cas d’un ordinateur portable, la souris est remplacée par une surface tactile. Dans le cas d’un téléphone, les fonctions sont toutes réalisées de façon tactile. Tous ces ordinateurs disposent d’un périphérique de stockage de données, soit un disque dur traditionnel ou SSD, soit une carte mémoire flash (sur un téléphone), agrémenté parfois d’une clé de stockage USB externe.

Nous trouvons ensuite d’autres périphériques moins visibles, et notamment les circuits de traitement audio pour la lecture et l’enregistrement de sons, les circuits de connexion à Internet via un réseau Ethernet ou Wi-Fi, les circuits de capture du signal de positionnement GPS et, dans le cas d’un téléphone, les circuits de détection de la gravité et des mouvements qui permettent à votre appareil de savoir comment il est orienté et comment il est déplacé par rapport à la planète.

Mais le sujet de ce chapitre, et des trois prochains, est l’unité centrale CPU. C’est le cœur, l’âme ou le cerveau de l’ordinateur ; vous choisirez un des trois termes selon vos convenances.

Nous avons vu dans le chapitre précédent un accumulateur triple octet qui réunissait un compteur pour générer des adresses mémoire, un additionneur et des verrous. Toutes les opérations étaient sous le contrôle des circuits qui utilisaient des codes lus dans des cellules mémoire, afin d’additionner et de soustraire des nombres, puis d’écrire le résultat en mémoire.

Une unité centrale ressemble beaucoup à cet accumulateur triple octet, mais elle en constitue une généralisation pour pouvoir exécuter de nombreux codes d’opérations différents. Une CPU est donc plus polyvalente que l’additionneur que nous venons d’étudier.

L’unité centrale que nous allons construire dans la suite du livre travaille sur des valeurs de 1 octet de large. Il va donc s’agir d’une CPU ou d’un processeur 8 bits qui sera en mesure d’accéder à 64 ko de mémoire vive RAM, ce qui suppose d’utiliser des adresses mémoire sur 2 octets, soit 16 bits. L’unité de travail fondamentale d’un processeur 8 bits est l’octet, mais il doit être capable de traiter dans une certaine mesure des valeurs sur 2 octets pour gérer ses adresses mémoire.

Le processeur dont nous allons suivre la conception n’existe pas dans le monde réel, mais il est, en théorie du moins, tout à fait capable de lire du code et des données en mémoire pour réaliser différentes opérations arithmétiques et logiques. Si l’on compare ses capacités dans ces deux domaines, cette machine d’étude est équivalente à n’importe quel processeur réel, aussi sophistiqué soit-il.

Au cours des ans, les processeurs 8 bits ont été remplacés par des CPU 16, puis 32, puis 64 bits. Pour autant, les plus récents processeurs ne sont pas devenus significativement plus riches en ce qui concerne la nature des tâches de traitement. Les générations se distinguent par la vitesse de traitement qui, dans certains cas, fait toute la différence. Lorsque l’on demande à un processeur de décoder le flux de données d’un film à visionner, un processeur 8 bits sait réaliser ce décodage, mais bien trop lentement pour que le visionnage du film reste captivant pour un humain.

Inversement, même si un processeur 8 bits réalise toutes ces opérations mathématiques et logiques annotées à la fois, il est capable de traiter des nombres codés sur plusieurs octets. Supposons en guise d’exemple que nous ayons besoin de faire additionner les deux valeurs sur 16 bits 1388h et 09C4h (soit 5 000 et 2 500 en décimal). Voici les valeurs qu’il faudrait stocker en mémoire pour que le processeur réalise cette opération :

Figure 21.1

La litanie de valeurs hexadécimales que vous voyez ne vous inspire sans doute pas grand-chose. Il s’agit en effet d’un mélange de codes d’instructions et de données, et on ne sait pas quelles lignes sont des codes d’instructions. Voici donc une version commentée :

Figure 21.2

Cette séquence d’instructions et de données constitue un programme d’ordinateur (vous l’aviez sans doute deviné). Ce programme très simple additionne les deux valeurs sur 16 bits 1388h et 09C4h. Le processeur commence par additionner les octets Bas des deux valeurs, c’est-à-dire 88h et C4h, puis stocke immédiatement le résultat à l’adresse mémoire 0010h. Il additionne ensuite les 2 octets Haut 13h et 09h en tenant compte d’une éventuelle retenue de la première addition. Le résultat est stocké à l’adresse 0011h puis la CPU s’arrête. Le résultat sur 16 bits est disponible aux adresses 0010h et 0011h et peut être examiné.

Comment ai-je choisi les valeurs des cinq codes d’instructions 3Eh, C6h, 32h, CEh et 76h ? Pour l’instant, puisque nous n’avons même pas commencé à construire notre processeur, je n’ai pu que m’inspirer de quelque chose d’existant : les véritables codes d’instructions de la famille de microprocesseurs Intel 8080, celle qui était utilisée dans le Altair 8800 de MITS, le premier ordinateur personnel commercialisé avec succès. L’ordinateur personnel IBM PC n’utilisait pas le 8080 mais son successeur, le 8088.

Dans ce chapitre, et les quelques chapitres suivants, je vais continuer à utiliser le processeur 8080 comme modèle pour mon propre processeur, mais seulement en tant que modèle. Je ne vais en effet définir qu’un sous-ensemble du jeu d’instructions 8080 qui en définit 244. Une fois ma CPU achevée, elle saura en exécuter un peu plus de la moitié, ce qui suffira largement à vous donner une bonne idée de ce qui se passe dans le cœur, l’âme ou le cerveau d’un ordinateur.

J’ai désigné ces codes sous le terme de codes d’instructions ou codes d’opérations, souvent abrégés en codop. On parle aussi de codes machine parce qu’ils sont directement interprétés par la machine matérielle, c’est-à-dire les circuits qui incarnent l’unité centrale de traitement. Le bref programme que nous venons de découvrir est un exemple de programme en code machine.

Le jeu d’instructions du processeur 8080 ne comprend que des instructions sur 1 octet. Néanmoins, certaines des instructions sont obligatoirement suivies de 1 ou de 2 octets. Dans notre programme d’exemple, les trois codes 3Eh, C6h et CEh sont toujours suivis d’un second octet : ce sont des instructions sur 2 octets. L’octet qui suit fait partie de l’instruction. L’instruction 32h est suivie de 2 octets, car elle attend une adresse mémoire sur 16 bits. Il s’agit donc d’une instruction sur 3 octets. D’autres instructions n’attendent pas d’octet complémentaire ; c’est le cas du code 76h d’arrêt de la CPU. Cette variabilité de la longueur des instructions rend la conception du processeur plus complexe.

L’enchevêtrement de code et de données du programme d’exemple ne représente pas la meilleure approche pour résoudre un besoin d’addition de deux nombres sur 16 bits. Il est souvent préférable de faire en sorte que les codes et les données soient dans des zones différentes de l’espace mémoire. Nous verrons cela plus précisément dans le prochain chapitre.

Un processeur regroupe plusieurs composants. Dans la suite de ce chapitre, nous allons nous concentrer sur le plus important d’entre eux : l’unité arithmétique et logique, abrégée en ALU (Arithmetic and Logic Unit). C’est cette partie du processeur qui réalise les additions et les soustractions (parmi d’autres tâches fondamentales).

Dans un processeur 8 bits, l’ALU ne peut réaliser ses additions et soustractions que sur 8 bits à la fois par construction. Nous avons pourtant souvent besoin de traiter des nombres sur 16, 24 ou 32 bits, voire plus. Nous avons vu que ce genre de grands nombres ne peut être traité qu’en effectuant les additions et soustractions octet par octet, en commençant par les octets de poids faible. Chaque addition ou soustraction d’octets suivante doit prendre en charge la retenue produite par l’opération précédente.

Autrement dit, notre ALU doit être capable de réaliser au minimum les opérations suivantes :

Additionner deux nombres sur 8 bits.

Additionner deux nombres sur 8 bits en réinjectant la retenue éventuelle de l’addition précédente. C’est dans ce cas une addition avec retenue.

Soustraire un nombre sur 8 bits à un autre.

Soustraire un nombre sur 8 bits à un autre en réinjectant la retenue éventuelle de la soustraction précédente. C’est dans ce cas une soustraction avec retenue2.

Abrégeons les descriptifs de ces quatre opérations3 :

Add ;

Add avec retenue ;

Sub ;

Sub avec retenue.

Plus loin, je vais encore plus abréger les descriptions. Souvenez-vous que l’addition avec retenue et la soustraction avec retenue se servent du bit de retenue de l’opération précédente. Ce bit peut être à 0 ou à 1 selon qu’il y a ou qu’il n’y a pas eu de retenue. L’ALU doit donc sauvegarder quelque part le bit de retenue d’une opération pour pouvoir la réutiliser dans l’opération suivante.

La gestion de la retenue rend évidemment les opérations arithmétiques de base beaucoup plus complexes que sans retenue.

Supposons en guise d’exemple, que nous ayons besoin d’additionner une paire de nombres sur 32 bits, soit 4 octets chacun. Nous additionnons d’abord les 2 octets Bas, ce qui peut générer ou pas une retenue. Choisissons de donner le nom Carry à un drapeau indicateur de retenue. Ce drapeau (flag) peut avoir la valeur 0 ou 1. Nous pouvons ensuite faire l’addition des 2 octets suivants avec ce drapeau armé par l’addition précédente et continuer ainsi avec les deux autres octets.

Pour additionner deux nombres sur 32 bits, il y a donc quatre opérations successives à réaliser afin de traiter les quatre couples d’octets :

Add ;

Add avec retenue (Carry) ;

Add avec retenue ;

Add avec retenue.

Le raisonnement est le même pour une soustraction, sauf que le nombre à soustraire est d’abord converti en complément à deux, comme vu dans le Chapitre 16 : on inverse l’état de tous les bits puis seulement pour le premier des quatre octets, on ajoute 1 en faisant passer à 1 l’entrée de retenue de l’additionneur. Soustraire un nombre sur 32 bits à un autre réclame donc également quatre opérations :

soustraction ;

soustraction avec retenue ;

soustraction avec retenue ;

soustraction avec retenue.

Je propose d’encapsuler le circuit capable de réaliser ces additions et soustractions dans une boîte de simplification :

Figure 21.3

Nous sommes toujours en terrain connu : deux entrées 8 bits sont additionnées ou soustraites et le résultat est présenté sur une sortie 8 bits. Cette boîte présente cependant quelques nouveautés non négligeables.

Dans mes schémas d’additionneur 8 bits, j’ai jusqu’à maintenant repéré l’entrée et la sortie de retenue avec les termes CI pour Carry In et CO pour Carry Out. Dans la Figure 21.3, c’est un peu différent. J’utilise le terme CY pour repérer le signal du drapeau de retenue. Nous verrons plus loin que l’autre signal, CY Out, est le même que le signal de sortie de retenue CO de l’additionneur. En revanche, CY In est le drapeau indicateur de retenue de l’addition de la soustraction précédente, et ce n’est pas nécessairement le même état que l’entrée de retenue CI de l’additionneur.

L’autre nouveauté dans ce schéma est la paire d’entrées F₀ et F₁. La lettre F signifie ici fonction. Ces deux entrées servent à sélectionner l’une des quatre actions à réaliser :

Tableau 21.1

F₁
F₀
Opération

0

0

Addition

0

1

Addition avec retenue

1

0

Soustraction

1

1

Soustraction avec retenue

Je rappelle que nous sommes en train de concevoir un équipement qui va exploiter des codes d’instructions lus en mémoire. Si nous les concevons correctement, 2 des 8 bits de ces codes devraient suffire à définir l’état des entrées de fonctions de ce module. C’est un peu la même chose que les bits des codes de l’addition et de la soustraction du chapitre précédent. L’essentiel du module Addition/Soustraction devrait vous être déjà connu :

Figure 21.4

La boîte marquée Complément à 1 se contente d’inverser l’état des entrées lorsque le signal de contrôle Inv est à 1. C’est la première étape obligatoire d’une conversion en complément à deux lorsque l’on réalise une soustraction.

La sortie CO de l’additionneur est promue sortie CY Out globale du module d’addition/soustraction. Ce que le diagramme ne montre pas est le signal Inv du circuit de complément à un ainsi que le signal d’entrée de retenue CI de l’additionneur. Le signal Inv doit tout simplement être à 1 dans le cas d’une soustraction. En revanche, l’entrée de retenue CI est un peu plus complexe à gérer. Essayons d’éclaircir la situation avec un tableau de logique :

Tableau 21.2

F₁
F₀
Fonction
Inv
CI

0

0

Addition

0

0

0

1

Addition avec retenue

0

CY

1

0

Soustraction

1

1

1

1

Soustraction avec retenue

1

CY

La lecture de ce tableau montre que le signal Inv de l’inverseur du complément à un copie l’état de F₁. En revanche, l’entrée CI est un peu plus délicate. Elle vaut 1 dans le cas d’une soustraction. La situation est celle du premier octet d’une soustraction sur plusieurs octets, lorsqu’il s’agit d’ajouter 1 au complément à un pour obtenir le complément à deux. Lorsque F0 vaut 1, CI équivaut au drapeau de retenue CY de l’opération d’addition ou de soustraction précédente. Tous ces cas peuvent être gérés en ajoutant le circuit suivant :

Figure 21.5

Une version interactive du module Addition/Soustraction complet est disponible sur le site Web compagnon.

Selon vous, quelles sont les autres opérations arithmétiques élémentaires qu’une unité arithmétique comme celle-ci devrait pouvoir réaliser, en plus de l’addition et de la soustraction ? Si vous pensez à la multiplication et la division, vous allez être déçu. Si vous songez à la relative complexité rencontrée pour réussir des additions et des soustractions, vous devinez ce qu’il en serait des multiplications et des divisions ! De tels circuits sont tout à fait possibles, mais cela dépasse largement les limites de ce livre. D’ailleurs, le processeur Intel 8080 ne sachant pas réaliser les multiplications et les divisions, mon processeur qui s’en inspire s’en passera. Si vous êtes patient, vous verrez cependant dans le Chapitre 24 comment un processeur tel que celui que nous concevons ici pourra se doter des outils élémentaires pour faire des multiplications.

Au lieu de nous focaliser sur les multiplications, intéressons-nous à l’autre caractéristique du descriptif de l’ALU : unité arithmétique et logique. Au cours de ce livre, j’emploie en général le terme logique pour parler des opérations booléennes. En quoi ces opérations nous seraient-elles utiles ?

Supposons la série de codes de caractères ASCII stockés dans les emplacements mémoire suivants, à partir d’une adresse arbitraire :

Figure 21.6

Supposons maintenant que vous vouliez forcer toutes les lettres (en « bas de casse »).

Si vous consultez les Tableaux 13.3 à 13.5 du Chapitre 13, vous voyez que les codes ASCII des lettres de l’alphabet en majuscules vont de 41h à 5Ah, alors que ceux des lettres en minuscules vont de 61h à 7Ah. Autrement dit, les deux valeurs pour la même lettre sont numériquement décalées de 20h. Lorsque vous savez qu’une lettre est en majuscule, vous la convertissez facilement en ajoutant 20h à son code ASCII. Si vous ajoutez 20h à 54h, qui est le code du T majuscule, vous obtenez 64h qui est le code du t minuscule. Voici l’addition à effectuer en binaire :

Cette astuce n’est pas applicable aux minuscules. Si vous ajoutez 20h au code 6Fh du o minuscule, vous obtenez 8Fh qui déborde de la plage sur 7 bits des codes ASCII de base4 :

Étudions les motifs de bits en détail. Comparons, par exemple, les deux variantes de la lettre A dont les codes ASCII sont 41h et 61h :

A : 01000001

a : 01100001

Voici la même comparaison pour la lettre E (codes ASCII 5Ah et 7Ah) :

Z : 01011010

z : 01111010

Pour toutes les lettres, la seule différence entre majuscule et minuscule se situe au niveau du sixième bit en partant de la droite. Vous passez de majuscule en minuscule en forçant ce bit à 1. S’il l’est déjà (la lettre est en minuscule), cela ne pose aucun souci.

Au lieu d’ajouter la valeur 20h, il est plus efficace d’appliquer un OU booléen pour chaque paire de bits. Vous vous souvenez du tableau de vérité du OU vu dans le Chapitre 6 :

OU

0

1

0

0

1

1

1

1

Rappelons que le résultat d’un OU logique est 1 dès que l’un des deux opérandes est à 1.

Repartons du T majuscule. Au lieu d’ajouter 20h, appliquons une opération OU entre bits de même rang de la valeur 54h qui correspond au T majuscule et la valeur 20h :

Le résultat est à 1 dès que l’un des deux bits d’entrée est à 1. Cette approche a l’avantage de ne pas chercher à convertir les lettres qui sont déjà en minuscules (ce qui génère un résultat incorrect). Voici justement le résultat d’un OU logique entre le o minuscule et la valeur 20h :

Revenons à notre programme. En appliquant l’opération OU avec 20h à chacune des lettres présentes en mémoire, nous pouvons toutes les convertir sans risque :

Figure 21.7

L’opération que nous venons de découvrir s’appelle un OU entre bits. L’opération traite deux à deux les bits de même rang de deux valeurs. Étant donné que ce genre d’opération possède bien d’autres domaines d’emploi en dehors de la conversion de caractères, cela justifie l’ajout des circuits suivants à notre unité ALU :

Figure 21.8

Ce circuit va réaliser un OU logique entre chacune des huit paires de bits de 2 octets A et B. Voici la version simplifiée de ce circuit :

Figure 21.9

Essayons maintenant de convertir un bloc de texte de minuscules en majuscules. Le processus est un peu différent, puisqu’il faut cette fois-ci forcer un bit à 0. Ce n’est pas une porte OU qu’il nous faut, mais une porte ET. Rappelons le tableau de vérité du ET vu dans le Chapitre 6 :

ET

0

1

0

0

0

1

0

1

Au lieu d’un OU avec la valeur 20h, nous appliquons un ET avec la valeur DFh (soit 1101 1111), qui est tout simplement l’inverse de 20h. Voici la conversion du o minuscule en majuscule :

Le code 6Fh devient 4Fh qui est effectivement le code ASCII du O majuscule.

Si la lettre est déjà en majuscule, l’opération n’a pas d’effet négatif. Nous le prouvons avec T majuscule :

La lettre reste elle-même après l’opération ET avec DFh. En appliquant cela à la totalité du texte en mémoire, nous le forçons en majuscules :

Figure 21.10

Il n’est donc pas inutile d’équiper notre ALU d’une batterie de huit portes ET afin de réaliser des ET entre les bits de 2 octets :

Figure 21.11

Voici la version simplifiée de ce composant :

Figure 21.12

L’opération ET entre bits permet également de tester si un bit en particulier est à 1 ou à 0. Supposons un octet contenant le code ASCII d’une lettre. Vous voulez savoir si elle est en majuscule ou pas. Il suffit d’appliquer un ET entre bits avec la valeur 20h. Si le résultat est égal à 20h, c’est que la lettre était en minuscule. Si le résultat vaut 00h, elle est en majuscule.

Une troisième opération entre bits très utile est le OU exclusif, le XOR. Rappelons son tableau de vérité vu dans le Chapitre 14. C’était à ce moment que nous avions vu que cette opération était très utile pour les additions :

XOR

0

1

0

0

1

1

1

0

Voici la batterie de huit portes XOR qui va permettre de réaliser des OU exclusifs entre bits :

Figure 21.13

Et voici la version simplifiée du composant :

Figure 21.14

L’opération XOR permet notamment d’inverser l’état des bits. Si vous appliquez un XOR entre la valeur 20h et chaque code ASCII des lettres du nom « Tom Sawyer », toutes les majuscules seront converties en minuscules et toutes les minuscules passeront en majuscules ! En réalisant un XOR avec la valeur FFh, vous inversez l’état de tous les bits d’une valeur.

J’avais présenté dans le Tableau 21.2 les bits de fonction F₁ et F₀ du module d’addition/soustraction. Pour notre unité ALU, il nous en faut un troisième :

Tableau 21.3

F₂
F₁
F₀
Opération

0

0

0

Addition

0

0

1

Addition avec retenue

0

1

0

Soustraction

0

1

1

Soustraction avec retenue

1

0

0

ET entre bits

1

0

1

XOR entre bits

1

1

0

OU entre bits

1

1

1

Comparaison

Les valeurs n’ont pas été distribuées au hasard. Nous verrons que ce sont les codes utilisés réellement pour les instructions de la famille de processeurs Intel 8080. Vous aurez remarqué que nous avons ajouté les trois opérations entre bits ET, XOR et OU mais également une opération dont nous n’avons pas encore parlé, la comparaison. Nous y venons sous peu.

En début de chapitre, j’avais présenté un tout petit programme qui utilisait les codes d’opérations 6Ch et CEh pour réaliser une addition avec l’octet suivant en mémoire. Le code 6Ch est celui de l’addition simple alors que CEh est une addition avec retenue. Ces deux instructions sont dites immédiates parce qu’elles travaillent sur la valeur qui se trouve juste après le code lui-même en mémoire. Dans le processeur Intel 8080, ces deux codes d’opération font partie d’une famille de huit que voici :

Tableau 21.4

Instruction
Codop (Code d’opération)5

Addition Immédiate

C6h

Addition avec retenue Immédiate

CEh

Soustraction Immédiate

D6h

Soustraction avec retenue Immédiate

DEh

ET Immédiat

E6h

XOR Immédiat

EEh

OU Immédiat

F6h

Comparaison Immédiate

FEh

Voici le format générique de tous les codes d’instructions :

1 1 F₂ F₁ F₀ 1 1 0

Dans cette équation, les bits F₂, F₁ et F₀ correspondent à ceux du Tableau 21.3. Nous utilisons ces 3 bits dans le circuit présenté dans la figure suivante qui intègre les sous-circuits pour les opérations entre bits ET, XOR et OU :

Figure 21.15

Les groupes d’entrées A et B sont envoyés dans les trois boîtes logiques ET, XOR et OU. Une seule des trois boîtes doit voir ses sorties activées. C’est à cela que servent les trois boîtes TRI qui sont des tampons triple état 8 bits qui permettent de sélectionner l’une des boîtes ou bien aucune des trois en fonction de la valeur des signaux de fonction F₀, F₁ et F₂. Lorsque F₂ vaut 0, ou bien lorsque les trois entrées F₂, F₁ et F₀ valent 1, aucune des sorties n’est sélectionnée. Résumons ce circuit dans une boîte de synthèse :

Figure 21.16

Nous venons de définir le composant logique de l’unité arithmétique et logique.

Dans le tableau des fonctions, nous constatons que lorsque les trois signaux Fx sont à 1, cela déclenche l’opération Compare. À quoi sert-elle ?

On a de temps à autre besoin de savoir si une valeur est inférieure, supérieure ou égale à une autre. Comment y parvenir ? Il s’agit en fin de compte d’une soustraction. Il suffit de soustraire l’octet B à l’octet A puis de tester le résultat. S’il est nul, c’est que les deux nombres sont égaux. S’il n’est pas nul et si le drapeau de retenue Carry est armé (à 1), c’est que l’octet B est plus grand que A. S’il n’est pas nul et si le drapeau de retenue n’est pas armé, c’est que l’octet A est plus grand.

L’opération Compare ressemble à l’opération de soustraction Subtract, sauf que le résultat de l’opération est abandonné. Seule la valeur du drapeau de retenue nous intéresse.

Mais comme nous l’avons observé plus haut, l’opération de comparaison doit également permettre de savoir si les deux valeurs sont égales, c’est-à-dire que le résultat est nul. Voilà pourquoi il nous faut un autre drapeau, et nous l’appellerons le drapeau Zero. Il devrait être sauvegardé en même temps que le drapeau de retenue.

Puisque nous y sommes, prévoyons un troisième drapeau pour le signe. Ce drapeau sera armé si le bit de poids fort du résultat d’une opération est à 1. Si le nombre est codé en complément à deux, ce drapeau de signe permet donc de savoir si le nombre est négatif ou positif. Il est négatif si le drapeau de signe est armé.

Pour être exact, le Intel 8080 prévoit cinq drapeaux et non trois. Je ne vais pas essayer de construire le drapeau de retenue auxiliaire qui permet de savoir si une retenue est apparue pour les 4 bits de poids faible puis pour les 4 bits de poids fort. Cette action est requise pour une instruction du 8080 qui s’appelle Decimal Adjust Accumulator. Cette opération convertit la valeur dans l’accumulateur qui est présumée codée en binaire vers le codage BCD ou binaire codé décimal. Nous avons parlé du code BCD dans le Chapitre 18 pour nos affichages de l’heure. Cette instruction ne sera pas prévue dans notre processeur d’étude. L’autre drapeau que je laisse dans l’ombre est celui de parité qui vaut 1 si le nombre de bits à l’état 1 du résultat d’une opération logique ou arithmétique est pair. Ce signal est facile à recréer avec sept portes XOR, mais ce drapeau est beaucoup moins utile que les autres.

La pratique a montré que pour certaines activités de programmation, l’opération de comparaison était bien plus nécessaire que l’addition et la soustraction. Si vous devez, par exemple, écrire un programme pour trouver du texte sur une page Web, le programme devra comparer les caractères contenus dans la page avec ceux du texte à trouver.

Complétons maintenant notre unité arithmétique et logique. Elle réunit le module addition/soustraction, le module de logique et quelques circuits de support assez spécifiques :

Figure 21.17

Les deux boîtes TRI sont des tampons triple état. Le module logique active une des sorties seulement pour l’une des trois combinaisons de F₀, F₁ et F₂ qui correspondent à une opération logique ET, XOR ou OU. Le tampon TRI en sortie du module Addition/Soustraction n’est actif que si F2 vaut 0, ce qui est le cas pour une addition ou une soustraction.

Nous trouvons en bas deux verrous dont les entrées d’horloge Clk sont reliées à une entrée Clock en bas à gauche, signal qui s’applique à la totalité de l’unité ALU. Un autre tampon triple état à gauche est contrôlé par un signal Enable, qui entre aussi dans l’ALU. Le tampon triple état en bas à droite constitue la sortie composite des deux modules principaux (Addition/ Soustraction et Logique).

La plupart des portes logiques individuelles dans ce schéma sont consacrées à la gestion du drapeau de retenue CY. Ce drapeau doit être armé si le signal F₂ est à 0 (dans le cas d’une addition ou d’une soustraction) ou si F₁ et F₀ sont tous deux à 1, (ce qui dénote une opération Compare).

Les trois drapeaux sont injectés en entrée du verrou en bas au centre. Une porte NOR à huit entrées permet de savoir si le résultat de l’opération est nul. Cela constitue le drapeau Zero, abrégé en Z. Le bit de poids fort de l’octet de données résultant est le drapeau de signe, abrégé en S. Bien qu’il n’y ait que trois drapeaux, ils sont considérés comme 3 bits d’un octet en sortie de l’ALU. Vous remarquez que le drapeau de retenue Carry est également réinjecté en haut en entrée CY du module Addition/Soustraction.

Nous pouvons maintenant créer une boîte de synthèse cachant tous ces détails :

Figure 21.18

Nous avons ainsi achevé la conception de notre unité arithmétique et logique !

L’ALU est un composant essentiel d’un processeur, mais ce dernier a besoin d’autres éléments pour réaliser des opérations arithmétiques et logiques sur des nombres. Il doit notamment disposer d’un mécanisme pour charger les nombres dans l’ALU et un autre pour exporter les résultats et les faire circuler. C’est notre prochain défi.

1 N.d.T. : accès aléatoire s’entend ici d’un accès direct à n’importe quelle adresse en opposition à l’accès séquentiel qui oblige à passer les adresses en revue depuis la première jusqu’à celle désirée.

2 N.d.T. : la retenue des soustractions s’appelle en anglais borrow et non carry.

3 N.d.T. : nous conservons volontairement les noms abrégés en anglais parce qu’ils servent de base aux mnémoniques des codes d’instructions machine, comme nous allons le voir.

4 N.d.T. : les anglophones ont pu se contenter de 7 bits pour coder leur alphabet mais les lettres accentuées du français ont nécessité l’emploi des codes supérieurs à 7Fh.

5 N.d.T. : dans la suite de ce livre, nous abrégerons en codop les termes code d’opération et code d’instruction.

Chapitre 22  Registres et bus

Parmi les opérations les plus courantes réalisées dans un ordinateur, nous trouvons tous les déplacements de données, c’est-à-dire d’octets. Un déplacement se produit dès que l’on demande de charger ou d’enregistrer un fichier, de lire de la musique ou une vidéo, ou de participer à une téléconférence. Lorsque les octets ne sont pas transférés assez vite, on observe des ruptures dans le flot sonore ou visuel et une perte de qualité. Tout le monde en a déjà été témoin.

À un niveau plus détaillé, il y a des déplacements d’octets au sein de l’unité centrale elle-même. Des octets sont copiés depuis la mémoire vers le processeur et vers l’unité ALU. Les résultats produits dans l’ALU sont parfois réinjectés dans l’ALU pour servir à d’autres opérations arithmétiques ou logiques, puis renvoyées en mémoire.

Ces opérations de déplacement d’octets au sein du processeur sont moins prestigieuses que les calculs réalisés par cette ALU, mais elles n’en sont pas moins indispensables.

Lorsque des octets sont déplacés au sein du processeur, ils sont stockés de façon temporaire dans des verrous mémoire (latch). Nous en avons utilisé dans le Chapitre 20 consacré à l’accumulateur triple octet. Cette machine était dotée de quatre verrous 8 bits : un pour stocker le code d’instruction et trois autres pour les 3 octets de données.

Le processeur que je propose de concevoir ici s’inspire du Intel 8080. Il devra posséder plus de quatre verrous. Je ne vais pas les présenter tous immédiatement, car je voudrais attirer d’abord l’attention sur sept verrous 8 bits spéciaux, qui sont directement contrôlés par des instructions du processeur. Ces verrous spéciaux portent le nom de registres. Un des principaux usages des registres est le stockage des octets pendant qu’ils sont traités dans l’unité ALU.

Les sept registres sont indispensables, mais l’un d’eux l’est encore plus que les autres : c’est l’accumulateur.

Pourquoi l’accumulateur serait-il spécial ? Vous vous souvenez que dans le chapitre précédent, nous avions vu que l’ALU possède deux entrées de données A et B. Dans le processeur Intel 8080 (et donc dans le mien), la première des deux entrées correspond toujours à la valeur qui se trouve à ce moment dans l’accumulateur et la sortie de l’ALU est toujours stockée également dans l’accumulateur. L’octet qui est dans l’accumulateur peut ensuite être transféré dans un des autres registres ou vers la mémoire.

Les sept registres sont identifiés par des lettres majuscules. L’accumulateur reçoit la lettre A. Quatre des autres registres reçoivent une lettre dans l’ordre alphabétique B, C, D et E. Les deux derniers registres ont reçu un nom de baptême spécial, pas F et G. Ils sont souvent utilisés en couple pour traiter une adresse sur 16 bits permettant d’accéder à un emplacement mémoire. C’est pour cette raison qu’ils ont été baptisés H et L, initiales de High (Haut) et Low (Bas), c’est-à-dire les deux octets constituant une adresse sur 16 bits.

Pour résumer, l’Intel 8080 (et mon processeur modélisé) s’appuie sur sept registres portant les noms A, B, C, D, E, H et L.

Dans le chapitre précédent, j’avais indiqué que le code d’opération 3Eh correspond à l’action « Transfert de l’octet situé à l’adresse suivante vers la CPU ». Autrement dit, ce code déclenche un déplacement du prochain octet depuis la mémoire vers l’accumulateur, c’est-à-dire le registre A. Le code 3Eh fait partie d’une famille de codes réalisant le même type d’action :

Tableau 22.1

Code Opération
Description

06h

Déplace le prochain octet vers le registre B.

0Eh

Déplace le prochain octet vers le registre C.

16h

Déplace le prochain octet vers le registre D.

1Eh

Déplace le prochain octet vers le registre E.

26h

Déplace le prochain octet vers le registre H.

2Eh

Déplace le prochain octet vers le registre L.

36h

Déplace le prochain octet vers la case mémoire

située à l’adresse [HL].

3Eh

Déplace le prochain octet vers le registre A.

Notre code 3Eh est en bas de tableau. Vous remarquez que l’ordre croissant des codes ne suit pas entièrement l’ordre alphabétique des noms des registres, mais c’est ainsi.

Intéressons-nous au code 36h. Il est différent de tous les autres car il ne charge pas l’octet de données dans un des sept registres, mais en mémoire à l’adresse 16 bits présente dans le couple de registres H et L, symbolisé par [HL].

Si nous disposions d’un processeur capable de comprendre ce genre d’instruction, par exemple, un Intel 8080, nous pourrions faire exécuter le petit programme suivant, qui exploite trois de ces codes machine :

Figure 22.1

Les 3 octets 26h, 2Eh et 36h sont présents dans le Tableau 22.1. Le premier code déplace l’octet qui est présent à l’adresse suivante en mémoire (la valeur 00h) dans le registre H. La deuxième action déplace la valeur 08h dans le registre L. Les deux registres H et L contiennent alors l’adresse mémoire complète 0008h sur 16 bits. Le code machine 36h demande le restockage du prochain octet (55h) qui est déjà en mémoire dans l’emplacement mémoire indiqué par le contenu de la paire de registres [HL]. Nous aboutissons alors au code 76h que nous connaissons déjà, puisqu’il provoque l’arrêt d’exécution du processeur.

L’utilisation du contenu des deux registres H et L pour connaître l’adresse mémoire sur 16 bits correspond à l’opération d’adressage indirect. Dans ce petit exemple, cette opération peut apparaître comme une complexité superflue, mais ce mode d’adressage est extrêmement utile.

Lorsque l’on étudie les champs de bits des huit codes d’opération du tableau précédent, on discerne un motif répétitif. Les huit codes suivent le format ci-dessous :

0 0 D D D 1 1 0

Dans ce masque, le triplet DDD est un code sur 3 bits indiquant la destination de l’octet à déplacer, comme le montre le tableau suivant :

Tableau 22.2

Code
Registre ou mémoire

000

B

001

C

010

D

011

E

100

H

101

L

110

[HL] ou M (pour Mémoire)

111

A

Le code 110 stipule que la destination est une adresse mémoire qui se trouve dans la paire de registres H et L sur 16 bits. Personnellement, je préfère désigner l’adresse mémoire au format [HL], mais sachez que la documentation Intel du 8080 indique la lettre M pour Mémoire ; M n’est pas le nom d’un nouveau registre.

Si tous les codes d’opération reconnus par un processeur 8 bits sont codés sur 1 octet, quel est le nombre de codes différents imaginable ? Il est évident que c’est 256. Le Intel 8080 n’utilise que 244 codes et en laisse donc 12 inutilisés. Nous avons pour l’instant découvert deux séries de huit codes d’instruction, le premier dans le Tableau 21.4 du chapitre précédent et l’autre dans le Tableau 22.1 que nous venons de voir. Nous avions en outre dans le Chapitre 21 utilisé le code 32h qui demande le stockage d’un octet de données à l’adresse suivant le code de l’instruction, ainsi que le code 76h pour arrêter le processeur.

Prenez une bonne inspiration avant de découvrir ce tableau qui présente 64 codes d’une seule traite :

Tableau 22.3

Opération arithmétique ou logique

Source

ADD

ADC

SUB

SBB

ANA

XRA

ORA

CMP

B

80h

88h

90h

98h

A0h

A8h

B0h

B8h

C

81h

89h

91h

99h

A1h

A9h

B1h

B9h

D

82h

8Ah

92h

9Ah

A2h

AAh

B2h

BAh

E

83h

8Bh

93h

9Bh

A3h

ABh

B3h

BBh

H

84h

8Ch

94h

9Ch

A4h

ACh

B4h

BCh

L

85h

8Dh

95h

9Dh

A5h

ADh

B5h

BDh

M

86h

8Eh

96h

9Eh

A6h

AEh

B6h

BEh

A

87h

8Fh

97h

9Fh

A7h

AFh

B7h

BFh

Ce tableau réunit plus d’un quart de toutes les instructions définies dans le processeur Intel 8080. Ces codes représentent l’essentiel de ses fonctions arithmétiques et logiques.

Intéressons-nous aux codes sur trois lettres de la seconde ligne d’en-tête. Ce sont les codes pour respectivement l’addition (sans et avec retenue), la soustraction (sans et avec retenue), les trois opérations logiques ET, XOR et OU et la fonction Compare. Ce sont les abréviations choisies par Intel dans la documentation du 8080.

Ces raccourcis sont des mnémoniques, des mots courts pour identifier des opérations dont le descriptif est plus long. Notez que ces mnémoniques jouent un rôle essentiel lors de l’écriture d’un programme pour le 8080.

Chacune de ces huit opérations arithmétiques ou logiques est à combiner avec une source de données indiquée dans la colonne de gauche. Cette source peut être l’un des sept registres ou l’accès mémoire par la paire de registres [HL].

Ces abréviations permettent de citer les codes des opérations de façon très efficace. Au lieu de spécifier « Code pour ajouter le contenu du registre E au contenu de l’accumulateur sans gérer la retenue éventuelle » ou « Code d’opération 83h », on peut écrire :

ADD E

La somme du contenu du registre E et du contenu de l’accumulateur est stockée à nouveau dans l’accumulateur.

De même, on évite de dire « Réaliser un OU exclusif entre l’accumulateur et l’octet mémoire stocké à l’adresse trouvée dans [HL] » ou « Code d’opération AEh », en écrivant simplement :

XRA M

Le résultat de l’opération est ici aussi stocké dans l’accumulateur.

Cette syntaxe abrégée porte le nom d’instruction en langage assembleur. Le terme « assembleur » est apparu au début des années 1950 pour décrire le processus consistant à assembler les éléments d’un programme informatique. Les mnémoniques des codes machine constituent une technique compacte et avenante pour symboliser les codes numériques des actions demandées au processeur. Ainsi, l’instruction en assembleur XRA M est plus lisible que le code d’opération AEh.

Les 64 instructions du tableau précédent obéissent au motif binaire suivant :

1 0 F F F S S S

Dans ce masque, le triplet FFF correspond au code de la fonction arithmétique ou logique reconnue par l’unité ALU décrite dans le Chapitre 21. Le second triplet SSS concerne le registre source ou la mémoire ; les valeurs autorisées sont les mêmes que celles du Tableau 22.2 des destinations DDD.

Dans le Tableau 22.1 des codes de déplacement d’octet vers un des registres ou vers la mémoire, ceux concernant les registres sont des instructions de déplacement immédiat, correspondant à l’abréviation MVI (MoVe Immediate). Nous en savons dorénavant assez pour ne plus nous servir des descriptions en langage clair et adopter les mnémoniques officielles des instructions en langage assembleur :

Tableau 22.4

Instruction assembleur
Codop

MVI B, donnée

06h

MVI C, donnée

0Eh

MVI D, donnée

16h

MVI E, donnée

1Eh

MVI H, donnée

26h

MVI L, donnée

2Eh

MVI M, donnée

36h

MVI A, donnée

3Eh

Dans ce tableau, j’ai placé le code en seconde colonne pour bien montrer que c’est l’écriture en langage assembleur qu’il faut privilégier dorénavant. Le mot donnée correspond à l’octet qui suit immédiatement le code d’opération en mémoire.

Nous avions découvert le code 32h dans la Figure 21.2 du Chapitre 21 dans le cadre de l’opération de stockage du contenu de l’accumulateur à l’adresse mémoire indiquée juste après le code de l’instruction. Il existe dans ce domaine deux actions apparentées puisque le code 3Ah provoque l’opération inverse : le chargement dans l’accumulateur de l’octet trouvé à cette adresse :

Tableau 22.5

Instruction assembleur
Codop

STA adr

32h

LDA adr

3Ah

L’abréviation STA signifie « STocker depuis Accumulateur » et l’abréviation LDA signifie « Lire (loaD) vers Accumulateur ». La mention adr désigne une adresse sur 16 bits fournie par les 2 octets qui suivent le code.

La mnémonique HLT du code 76h nous est déjà connue : il provoque l’arrêt du processeur.

Nous connaissons maintenant suffisamment d’instructions pour rédiger un petit programme qui va illustrer la correspondance entre codes d’opération et instructions assembleur :

Figure 22.2

L’instruction LDA charge dans l’accumulateur la valeur 66h qui se trouve à l’adresse 2044h. L’instruction MVI charge dans le registre B la valeur 33h dans le registre B. L’instruction ADD additionne la valeur trouvée dans la valeur de registre D à celle trouvée dans l’accumulateur, ce dernier contenant ensuite la valeur 99h. Il ne reste plus qu’à stocker le résultat en mémoire à l’adresse 2044h grâce à une instruction STA, ce valeur 99h écrasant l’ancien contenu 66h.

Les huit codes du Tableau 21.4 réalisent des opérations arithmétiques ou logiques sur la valeur de l’octet trouvé juste en mémoire après le code. Voici ces mêmes instructions avec leur mnémonique Intel 8080 officielle :

Tableau 22.6

Instruction assembleur
Codop

ADI donnée

C6h

ACI donnée

CEh

SUI donnée

D6h

SBI donnée

DEh

ANI donnée

E6h

XRI donnée

EEh

ORI donnée

F6h

CPI donnée

FEh

Nous avons déjà vu qu’il s’agissait d’instructions immédiates. En effet, l’opération est appliquée à l’octet situé juste après le code de l’opération en mémoire. On peut donc parler d’addition immédiate, d’addition avec retenue immédiate, de soustraction immédiate, etc. L’opération utilise toujours l’accumulateur et le résultat est toujours rendu disponible dans l’accumulateur.

Nous pouvons donc simplifier notre programme ainsi :

Figure 22.3

Dorénavant, nous évitons de charger d’abord la valeur 33h dans le registre B pour l’additionner au contenu de l’accumulateur. La valeur 33h est directement additionnée au moyen de l’instruction ADI.

Le processeur 8080 définit 63 instructions de déplacement d’octet d’un registre à un autre, d’une adresse mémoire désignée par [HL] vers un registre ou d’un registre vers cette adresse mémoire :

Tableau 22.7

Codes pour MOV destination, source

Source

B

C

D

E

H

L

M

A

B

40h

48h

50h

58h

60h

68h

70h

78h

C

41h

49h

51h

59h

61h

69h

71h

79h

D

42h

4Ah

52h

5Ah

62h

6Ah

72h

7Ah

E

43h

4Bh

53h

5Bh

63h

6Bh

73h

7Bh

H

44h

4Ch

54h

5Ch

64h

6Ch

74h

7Ch

L

45h

4Dh

55h

5Dh

65h

6Dh

75h

7Dh

M

46h

4Eh

56h

5Eh

66h

6Eh

7Eh

A

47h

4Fh

57h

5Fh

67h

6Fh

77h

7Fh

Ce sont toutes des instructions de déplacement utilisant la mnémonique en trois lettres MOV. Un code d’instruction spécifique a été attribué à chacune pour désigner en une fois la destination puis la source. Par exemple, le code 69h correspond à ceci :

MOV L, C

Notez que le registre destinataire est mentionné en premier et le registre source, en second. Méfiez-vous ! La confusion est fréquente dans les premiers temps. Cette instruction demande de déplacer vers le registre L l’octet trouvé dans le registre C. On peut expliquer l’opération par écrit en ajoutant une flèche qui montre le sens du déplacement de l’octet :

Déplacer L ← C

Ce que contenait le registre L est bien sûr détruit, écrasé, par la valeur issue du registre source. Le contenu de ce registre source ne change pas. Une fois l’opération finie, les deux registres C et L contiennent la même valeur.

Vous aurez remarqué que sept de ces instructions ne font rien dans la mesure où le registre source et le registre destination ne font qu’un :

MOV C, C

Déplacer le contenu d’un registre vers lui-même n’a aucun effet (N.d.T. : et pendant ce temps, les Shadoks pompaient…).

Il n’existe pas d’instruction MOV M, M qui aurait porté le code d’opération 76h. Nous devinons que ce code est de ce fait réutilisé pour l’instruction d’arrêt du processeur HLT.

Voici donc une reformulation du programme qui cherche à additionner une valeur à l’octet trouvé à l’adresse mémoire 2044h :

Figure 22.4

Cette version a pour but de montrer l’intérêt d’utiliser l’adressage indirect au moyen des deux registres H et L. Leur valeur est déterminée en début de programme comme égale à 20h et 44h. La première instruction MOV transfère la valeur trouvée à cette adresse dans l’accumulateur qui contient alors 66h. On y ajoute la valeur 33h, puis la seconde instruction de transfert envoie le résultat à l’adresse 2044h sans qu’il soit nécessaire de la spécifier à nouveau.

Si l’on étudie les champs de bits des 63 instructions de déplacement, on aboutit à un motif caractéristique :

0 1 D D D S S S

Dans ce masque, le bloc DDD désigne le registre destinataire et SSS, le registre source. On réutilise ici le Tableau 22.2 :

Tableau 22.8

Code
Registre ou mémoire

000

B

001

C

010

D

011

E

100

H

101

L

110

M (alias [HL])

111

A

Pour concevoir un processeur, une approche consiste à décider d’abord quelles instructions le processeur doit pouvoir exécuter, puis à réaliser les circuits concrétisant ces besoins. C’est pour l’essentiel ce que je fais dans ces pages. Je sélectionne un sous-ensemble du jeu d’instructions Intel 8080, puis je construis les circuits correspondants.

Pour implémenter toutes les instructions qui utilisent les sept registres, il faut que le processeur puisse stocker des octets dans sept verrous afin de pouvoir récupérer les valeurs des octets en fonction des codes sur 3 bits. Dans une première approche, je vais ignorer le code binaire 110 car l’accès à la mémoire est un cas particulier. Les autres codes vont servir d’entrée à des décodeurs 3-vers-8.

Le circuit ci-dessous est doté de sept verrous et de sept tampons triple état. Un premier décodeur 3-vers-8 mémorise la valeur d’entrée dans l’un des registres. Un autre décodeur 3-vers-8 sert à activer l’un des tampons triple état en sorte de sélectionner une valeur dans un des registres :

Figure 22.5

On donne à ce circuit (le plus complexe de ce chapitre) le nom de rangée de registres (array). Au premier regard, il semble touffu, mais son fonctionnement est assez simple.

Tout en haut au centre de la figure, nous trouvons un chemin de données 8 bits légendé In. Il correspond à l’octet de données d’entrée qui doit être stockée quelque part dans la rangée de registres. Les sept boîtes avec une lettre correspondent aux registres qui sont des verrous 8 bits. La place ne suffit pas pour indiquer leurs noms, mais chaque registre reçoit une entrée d’horloge Clk provenant du sélecteur de gauche et servant à valider l’écriture d’une valeur dans le verrou.

Tout en haut du diagramme à gauche et à droite, nous trouvons les deux décodeurs 3-vers-8 cités plus haut. Leurs entrées de sélection sont numérotées S₂, S₁ et S₀. La valeur exprimée sur les triplets d’entrée Select est l’une de celles permettant de sélectionner l’un des sept registres ou la mémoire (comme indiqué dans le Tableau 22.8). Vous comprenez d’ailleurs pourquoi la sortie 6 des décodeurs n’est pas employée : elle correspond à la valeur de sélection 110 qui ne concerne aucun registre, mais un emplacement en mémoire.

Le décodeur 3-vers-8 de gauche contrôle les entrées d’horloge des verrous. Le signal d’horloge est envoyé à un seul des sept verrous selon la valeur du triplet de signaux Sx. Cela correspond à l’opération de chargement de l’octet d’entrée dans le verrou sélectionné.

Sous les sept verrous des registres, nous trouvons autant de tampons triple état. Comme pour les verrous, ils sont contrôlés par un signal d’activation Enable qui provient du décodeur 3-vers-8 de droite. Le tampon qui est activé grâce à la valeur que possède le signal à tout moment permet de rendre l’octet qu’il contient disponible dans le chemin de données de sortie tout en bas.

Vous remarquez que l’accumulateur reçoit un traitement spécial. La valeur qui se trouve à tout moment dans cet accumulateur est toujours disponible sur la sortie spéciale Acc en bas à gauche.

La valeur sur 8 bits prise en charge en entrée de cette rangée de registres peut provenir de différentes sources : de la mémoire, de l’un des autres registres ou de l’ALU. Inversement, la valeur sur 8 bits qui est produite dans la rangée de registres peut être stockée en mémoire, dans l’un des autres registres ou dans l’ALU.

Pour ne pas vous perdre dans la forêt touffue que commence à constituer l’imbrication de différents sous-ensembles fonctionnels, je vous propose le diagramme par blocs simplifié suivant qui ne conserve que les chemins de données :

Figure 22.6

La figure montre que les chemins de données principaux sont sur 8 bits. Vous remarquez que toutes les entrées et sorties sont interconnectées, hormis le chemin qui va de la sortie Acc de la rangée de registres vers l’entrée A de l’ALU. Même les sorties de données DO du circuit mémoire RAM sont reconnectées à l’entrée de données DI de la mémoire !

Cette cause apparente de court-circuit est supprimée parce que toutes les sorties des composants se font sur des tampons triple état, dont un seul est actif à tout moment. La valeur rendue ainsi disponible peut être stockée en mémoire grâce au signal Write, dans un des sept registres, ou bien dans l’ALU si c’est le résultat d’une opération arithmétique ou logique.

La liaison électrique entre toutes les entrées et toutes les sorties porte le nom de bus de données. Ce chemin de données est partagé entre toutes les entrées et sorties de tous les composants. Dès qu’un des tampons triple état connecté au bus est activé, l’octet qu’il contient est rendu disponible à la totalité des participants du bus de données, et peut donc être utilisé par n’importe lequel des autres composants connectés.

Ce bus de données n’a qu’une largeur de 8 bits. Un autre bus est à prévoir pour les adresses mémoire sur 16 bits. Lui aussi verra le contenu provenir de différentes sources. Ce bus 16 bits s’appelle le bus d’adresse ; nous l’étudierons bientôt.

Dévoilons d’abord quelques détails ennuyeux. J’aurais vraiment aimé que la rangée de registres reste aussi simple que la figure soi-disant détaillée que j’en ai fournie un peu plus haut. En réalité, il va falloir ajouter quelques enrichissements.

Telle que je l’ai présentée, la rangée de registres est parfaite pour réaliser des déplacements MOV pour transférer un octet d’un registre à un autre. D’ailleurs, les décodeurs 3-vers-8 ont été conçus dans le but de réaliser cette instruction MOV : on peut activer le contenu d’un registre pour qu’il soit disponible sur le bus de données, ce qui permet de stocker la valeur dans un autre registre.

Le problème est que cette rangée de registres ne fonctionne pas correctement pour les deux instructions STA et LDA qui servent respectivement à stocker le contenu de l’accumulateur en mémoire et à charger une valeur de la mémoire vers l’accumulateur. D’autres instructions utilisent également l’accumulateur. En particulier, toutes les opérations arithmétiques et logiques stockent leur résultat dans l’accumulateur.

Il y a donc lieu d’améliorer la partie fonctionnelle de la série de registres concernant l’accumulateur pour permettre d’y stocker une valeur et d’en relire le contenu indépendamment des décodeurs 3-vers-8. On y parvient en ajoutant un peu de logique dans la rangée de registres. Sont concernés le verrou mémoire de l’accumulateur et son tampon triple état :

Figure 22.7

Sur la gauche, deux nouveaux signaux servent chacun d’entrée à une porte OU. Ils permettent de stocker une valeur se trouvant sur le bus de données dans l’accumulateur A indépendamment du décodeur de sélection d’entrée Input Select ou de la rendre disponible sur le bus de données par activation Enable indépendamment du décodeur de sélection de sortie Output Select.

La rangée de registres réclame une autre amélioration pour pouvoir adresser la mémoire RAM en combinant les deux registres H et L. Mais c’est une amélioration beaucoup plus ardue et nous allons attendre le plus possible avant de la réaliser !

Trois nouveaux verrous 8 bits doivent être reliés au bus de données pour mémoriser les octets de l’instruction :

Figure 22.8

Le code d’opération est toujours stocké dans le verrou 1. Une fois qu’il s’y trouve, il peut utilisé pour générer les autres signaux contrôlant le processeur. Nous verrons cela en détail dans le prochain chapitre.

Le verrou d’instruction 2 est prévu pour les instructions qui ont 1 octet complémentaire. C’est ainsi que l’opération de déplacement immédiat MVI est toujours suivie d’un octet qui doit être placé dans un des registres. Les opérations arithmétique et logiques immédiates telles que ADI sont elles aussi suivies d’un octet complémentaire. Par exemple, dans le cas de ADI, l’octet est la valeur qui doit être ajoutée au contenu actuel de l’accumulateur. Il faut donc activer la valeur se trouvant dans le verrou 2 sur le bus de données, ce qui se fait grâce au tampon triple état.

Pour les instructions constituées de 3 octets, comme STA et LDA, on utilise les verrous 2 et 3. Le deuxième et le troisième octet de l’instruction constituent une valeur sur 16 bits qui est une adresse mémoire.

Comme vous le savez, en plus du bus de données 8 bits, le processeur a besoin d’un bus d’adresses 16 bits pour pouvoir accéder à 64 ko de mémoire. L’adresse peut provenir de différentes sources :

Le contenu d’un registre spécial dont le nom est toujours abrégé en PC pour Program Counter1. Il s’agit d’une valeur sur 16 bits qui désigne (pointe) où se trouve la prochaine instruction exécutée. La valeur commence à 0000h et augmente de façon régulière jusqu’à rencontrer l’instruction d’arrêt HLT.

Les 2 octets que l’on trouve juste après le code d’opération STA ou LDA.

Les deux registres H et L qui forment une adresse sur 16 bits, par exemple dans l’instruction MOV A,M. Dans ce cas, on parle de la paire de registres HL.

Dans notre projet d’accumulateur triple octet, nous accédons à la mémoire par adresses croissantes grâce à un compteur sur 16 bits. Dans le présent projet, je ne vais pas utiliser de compteur. Nous verrons dans le Chapitre 24 que certaines instructions modifient le contenu du pointeur d’instruction pour que le prochain code à exécuter se trouve ailleurs qu’à l’adresse obtenue par croissance régulière. Le pointeur d’instruction est donc un verrou mémoire 16 bits de large dont le contenu est en général incrémenté après lecture de l’octet du code d’opération.

Voici la structure du pointeur d’instruction PC :

Figure 22.9

Les trois flèches des chemins de données sont plus larges que dans la figure précédente pour bien suggérer que la largeur est de 16 bits. Je rappelle que l’entrée tout en haut et la sortie tout en bas sont interconnectées sur le bus d’adresse. La valeur qui se trouve sur le bus peut donc être stockée dans le verrou en activant l’entrée d’horloge Clock. De même, la valeur présente dans le verrou peut être placée sur le bus d’adresse en activant le signal Enable.

Pour une instruction STA ou LDA, les 2 octets qui doivent suivre le code de l’opération sont stockés dans les verrous d’instruction 2 et 3. En conséquence, ces deux verrous doivent eux aussi être connectés au bus d’adresse au moyen d’un tampon triple état.

L’instruction MOV requiert une adresse mémoire au moyen de la paire de registres HL. Les valeurs dans ces deux registres doivent donc être accessibles sur le bus d’adresse. Je n’avais pas du tout noté cette contrainte lors de la conception de la rangée de registres. Dans ma première version, les registres H et L n’avaient accès qu’au bus de données.

J’ai d’ailleurs deux nouvelles instructions à vous présenter :

Tableau 22.9

Instruction
Description
Codop (Code
d’opération)

INX HL

Incrémente la valeur de la paire de registres HL

23h

DCX HL

Décrémente la valeur de la paire de registres HL

2Bh

L’instruction INX ajoute 1 à la valeur sur 16 bits de la paire HL. Inversement, l’instruction DCX enlève 1 à cette valeur. Autrement dit, INX incrémente et DCX décrémente.

Ces deux instructions sont extrêmement utiles, surtout INX. Supposons qu’il y ait 5 octets en mémoire à partir de l’adresse 1000h. Vous avez besoin de faire la somme de ces cinq valeurs. Vous pouvez vous contenter de définir une seule fois le contenu des deux registres HL, puis d’incrémenter cette adresse après avoir lu chaque octet :

Figure 22.10

Vous vous souvenez que l’ALU utilisait plusieurs drapeaux pour savoir si le résultat d’une opération est égal à zéro, négatif ou si une retenue est apparue suite à une addition ou soustraction. Les deux instructions INX et DCX n’ont aucun effet sur les drapeaux, et c’est volontaire.

(Notons au passage que le Intel 8080 prévoit des instructions INX et DCX pour les deux paires de registres BC et DE, mais elles sont beaucoup moins utiles, et je ne chercherai pas à les reproduire dans mon projet. Le 8080 propose également des incrémentations et décrémentations sur 8 bits INR et DCR applicables aux sept registres et à la mémoire pointée par la paire HL. Je ne chercherai pas non plus à les mettre en pratique.)

Pour pouvoir utiliser ces deux instructions INX et DCX, il nous faut ajouter un sous-ensemble capable de réaliser une incrémentation et une décrémentation sur 16 bits.

J’ai indiqué précédemment que le pointeur d’instruction PC qui contient sur 16 bits l’adresse en mémoire de la prochaine instruction doit lui aussi être incrémenté juste après la lecture de chaque octet d’une instruction. Il nous faut donc ici aussi un incrément sur 16 bits.

Vous devinez qu’un circuit pour augmenter ou diminuer de 1 une valeur est plus simple qu’un additionneur parce qu’il lui suffit de changer la valeur d’une unité. Voici une version 8 bits qui vous donne une idée de ce circuit. Les différents I indicés sont les entrées et les O indicés, les sorties. Le signal d’entrée Dec doit être à 0 pour incrémenter la valeur et à 1 pour la décrémenter :

Figure 22.11

Réunissons cet assemblage de portes XOR et ET avec un verrou 16 bits qui va mémoriser la valeur à faire évoluer et un tampon triple état pour rendre disponible la valeur modifiée :

Figure 22.12

Ce composant est un incrémenteur/décrémenteur. Comme dans le cas du pointeur d’instruction PC, les 16 bits d’entrée du verrou et ceux de sortie du tampon sont connectés au bus d’adresse. Le tampon de sortie doit pouvoir être activé par l’un des deux signaux Décrément ou Incrément. Pour le sens de variation de la valeur, le signal Décrément suffit avec ses deux états possibles.

Le bus d’adresse sur 16 bits sert d’abord à définir une adresse dans la mémoire RAM, mais il doit également servir à transférer une valeur sur 16 bits entre plusieurs composants de l’unité centrale.

Le pointeur d’instruction PC sert, par exemple, à définir une adresse mémoire pour l’accès à une instruction. Une fois que la valeur que contient le PC a été utilisée, elle doit être transférée dans l’incrémenteur/décrémenteur pour être augmentée de 1 puis stockée à nouveau dans le verrou du pointeur d’instruction.

Prenons un autre exemple : l’instruction MOV A, M se sert de la paire HL pour trouver 1 octet en mémoire. Mais cette instruction est souvent suivie de INX HL pour envoyer la valeur de la paire HL dans l’incrémenteur/décrémenteur afin de passer à l’adresse suivante avant de la stocker dans la paire HL.

Le surcroît de complexité que j’avais tenté de contourner le plus longtemps possible nous tend les bras. La rangée de registres présentée jusqu’ici est élégante et facile à comprendre, mais elle ne procure aucune solution pour que les valeurs se trouvant dans les registres H et L soient placées sur le bus d’adresse 16 bits. Voyons comment corriger cela :

Figure 22.13

Remarquez tout à fait à droite les chemins de données 16 bits en entrées et en sorties. Tous deux seront connectés au bus d’adresse, ce qui permettra d’écrire et de lire une valeur 16 bits dans les deux registres H et L.

Ce schéma qui augmente la richesse fonctionnelle des registres H L comporte plusieurs nouveaux signaux et composants qui ont été ajoutés à la rangée de registres :

Un signal HL Select à droite sert à contrôler la rangée supérieure de tampons triple état. Le signal permet de décider si les entrées des registres H et L doivent venir du canal d’entrée normal de la rangée de registres ou de l’entrée 16 bits.

Un signal d’horloge HL Clock à droite est injecté dans deux portes OU qui ont été ajoutées aux entrées d’horloge des verrous, ce qui permet de sauvegarder des valeurs lues sur le bus d’adresse.

Enfin, un signal HL Enable à droite active un nouveau tampon triple état 16 bits qui permet à la sortie composite des deux registres H et L d’être présentée sur le bus d’adresse.

Une petite usine à gaz, non ? Personne n’a jamais prétendu que concevoir un ordinateur serait facile.

Vous trouverez une version interactive de la rangée de registres complète sur le site Web compagnon.

Nous devons maintenant connecter ces composants à un bus d’adresse 16 bits. Le schéma suivant montre une portion du bus de données en haut servant d’entrée aux verrous d’instruction 2 et 3. Les sorties de ces derniers sont réunies dans un tampon triple état connecté au bus d’adresse :

Figure 22.14

Le bus d’adresse est visuellement deux fois plus large puisqu’il est sur 16 bits. Il fait le tour des composants dans la partie basse du schéma, aussi bien en entrée qu’en sortie, et il fournit également, bien sûr, l’adresse au circuit mémoire RAM.

Tout comme le schéma du bus de données quelques pages auparavant, celui-ci passe sous silence quelques signaux essentiels pour le bus d’adresse : les signaux pour activer les tampons triple état, et ceux pour enregistrer la valeur trouvée sur le bus dans différents verrous.

Ce sont ces signaux, une fois correctement synchronisés, qui provoquent l’exécution des instructions par le processeur. Ces signaux constituent la pulsation qui donne vie à l’ordinateur. Ils méritent donc leur propre chapitre. C’est justement le prochain.

1 N.d.T. : en français, on parle plutôt de pointeur d’instruction, mais tout en conservant le sigle PC de Program Counter.

Chapitre 23  Signaux de contrôle

Dans le milieu des ingénieurs, on considère en général que les dix derniers pourcents d’un projet correspondent en réalité à 90 % de l’effort total. Cette perspective guère enthousiasmante ne doit pas être négligée. Nous avons sérieusement progressé dans la conception de notre ordinateur pédagogique, mais nous n’avons pas terminé. Ce qui reste ne représente pas 90 % de l’effort, mais la dernière ligne droite va s’avérer plus longue que prévu.

L’unité centrale de traitement CPU, c’est-à-dire le processeur dont j’ai déroulé la conception jusqu’ici, s’inspire du processeur historique Intel 8080. Deux composants majeurs de ce projet sont l’unité arithmétique et logique ALU et la rangée de registres. Ils ont été décrits dans les deux derniers chapitres. L’ALU réalise les traitements arithmétiques et logiques sur les octets. La rangée de registres est constituée de verrous mémoire pour les sept registres correspondant aux lettres A, B, C, D, E, H et L. Nous avons également ajouté trois verrous mémoire pour stocker l’octet de l’instruction exécutée ainsi que les éventuels octets complémentaires qui suivent certaines instructions.

Tous ces composants sont interconnectés et reliés à la mémoire centrale RAM au moyen de deux bus : un bus de données sur 8 bits, qui permet de transférer les valeurs entre les différents composants, et un bus d’adresse sur 16 bits qui sert à sélectionner une cellule mémoire. Dans le chapitre précédent, nous avons également cité la nécessité d’un registre pointeur d’instruction PC contenant l’adresse d’une instruction en mémoire, associé à un circuit incrémenteur/décrémenteur permettant de faire varier une adresse mémoire sur 16 bits.

Les deux bus constituent les moyens de communication principaux entre les composants, mais ces derniers sont également connectés via un jeu complexe de signaux de contrôle. On les appelle ainsi parce que ces signaux contrôlent la façon dont les composants coopèrent pour exécuter les instructions trouvées dans la mémoire.

Les différents signaux de contrôle peuvent être répartis en deux grandes catégories :

les signaux qui placent une valeur sur l’un des deux bus ;

les signaux qui lisent une valeur sur l’un des deux bus.

Ce chapitre est entièrement consacré à ces opérations de lecture et d’écriture de données sur un bus.

Les signaux qui envoient une valeur sur un bus dépendent des entrées d’activation Enable des tampons triple état qui protègent les sorties des composants vers le bus. Les signaux qui lisent une valeur sur un bus contrôlent en général les entrées d’horloge Clock des différents verrous qui assurent la liaison entre le bus et les composants. La seule exception est la suivante : lorsqu’il s’agit d’écrire en mémoire la valeur qui se trouve sur le bus de données en utilisant le signal d’autorisation d’écriture Write de la mémoire RAM.

Le processeur exécute les instructions trouvées en mémoire sous l’effet d’une synchronisation entre les différents signaux. Ce cadencement contrôle la façon dont les valeurs sur 8 et sur 16 bits sont échangées entre les composants du processeur CPU et la mémoire. C’est selon ce principe que les codes d’instructions trouvés en mémoire contrôlent la partie matérielle de l’ordinateur. Matériel et logiciel travaillent ainsi de concert. Le processus peut faire penser à un marionnettiste qui contrôle toute une série de marionnettes pour aboutir à une chorégraphie parfaitement réglée, constituée d’opérations arithmétiques et logiques. Les signaux de contrôle de la CPU sont les ficelles.

Passons en revue les six composants principaux en découvrant comment chacun d’eux est connecté au bus de données ou d’adresse, et quels signaux de contrôle le concernent.

Les entrées d’adresse du circuit mémoire sont connectées au bus d’adresse 16 bits. La mémoire est également connectée au bus de données 8 bits grâce aux entrées Data In et Data Out :

Figure 23.1

Deux signaux de contrôle concernent la mémoire : le signal Write autorise la lecture de la valeur présente sur le bus de données pour écriture dans la mémoire ; le signal Enable active les tampons triple état des sorties de données de la mémoire pour que la valeur en sortie soit disponible sur le bus de données. On peut tout à fait relier un panneau de contrôle à ce circuit mémoire et permettre un humain au moyen d’interrupteurs et de lampes de lire et d’écrire dans le circuit mémoire.

Le composant le plus complexe est sans aucun doute la rangée de registres vue dans le Chapitre 22. Nous l’adresserons parfois en RA dans la suite (Register Array) :

Figure 23.2

Nous trouvons tout en haut de ce groupe de registres deux séries de trois entrées de sélection S. Les trois signaux SIx déterminent lequel des registres doit recevoir la valeur lue sur le bus de données. Le signal d’horloge RA Clock à gauche décide quand l’opération est réalisée. De même, les signaux SOx en combinaison avec le signal d’activation RA Enable à gauche décident de la mise à disposition du contenu d’un registre sur le bus de données.

Nous avons vu dans le chapitre précédent que ce groupe de registres était complexe sous deux aspects. Tout d’abord, il doit gérer deux signaux de contrôle complémentaires pour l’accumulateur, que nous adressons en Acc. Le signal d’horloge Acc Clock provoque la lecture de la valeur se trouvant sur le bus de données dans l’accumulateur et le signal Acc Enable active le tampon triple état afin de mettre à disposition la valeur de l’accumulateur sur le bus de données.

L’autre complexité est liée au fait que les registres H et L sont également connectés au bus d’adresse en combinaison avec les trois signaux de contrôle du côté droit dans la figure. Le signal HL Select fait sélectionner le bus d’adresse comme canal d’entrée de la paire de registres H et L. Le signal HL Clock provoque la lecture du contenu du bus d’adresse vers les registres H et L. Enfin, le signal HL Enable active un tampon triple état qui met à disposition le contenu des deux registres H et L sur le but d’adresse.

L’unité ALU vue dans le Chapitre 21 comprend trois entrées de fonction F₀, F₁ et F₂. Elle permet de décider quelle opération doit être réalisée (addition, soustraction, comparaison ou fonction logique) :

Figure 23.3

Vous remarquez que l’entrée B ainsi que la sortie Out de l’ALU sont connectées au bus de données alors que l’entrée A est connectée directement à la sortie du bloc de registres. Une petite complication de l’ALU provient du besoin de sauvegarder l’état des trois drapeaux de retenue CY, de zéro Z et de signe S, état qui varie en fonction de l’opération arithmétique ou logique réalisée.

L’ALU utilise en outre un signal d’horloge Clock qui décide quand le résultat de l’opération doit être stocké dans un verrou (tout en sauvegardant l’état des drapeaux dans un autre). Elle dispose enfin d’un signal Enable qui active le tampon triple état, rendant disponible le résultat de l’ALU sur le bus de données.

Nous trouvons ensuite un autre verrou mémoire 16 bits qui maintient la valeur actuelle du pointeur d’instruction PC, qui sert à désigner (pointer) une adresse mémoire1 :

Figure 23.4

Le pointeur d’instruction utilise trois signaux de contrôle : l’horloge Clock permet de stocker la valeur sur 16 bits trouvée sur le bus d’adresse dans le verrou. Le signal Enable active le tampon triple état pour rendre disponible le contenu du verrou sur le bus d’adresse. Enfin, le signal Reset à gauche force le contenu du verrou à zéro pour que la première adresse mémoire corresponde au début de l’espace mémoire, soit 0000h.

Nous trouvons ensuite les trois verrous 8 bits qui mémorisent les 3 octets possibles d’une instruction. Voici le schéma correspondant :

Figure 23.5

Certaines instructions sont compactes : elles ne sont pas accompagnées d’un ou deux octets de données. D’autres en ont besoin. Les trois signaux Clock à gauche servent à mémoriser jusqu’à 3 octets par instruction.

Le premier octet est toujours le code de l’opération, ou codop. Lorsque l’instruction attend un octet complémentaire, il peut être rendu disponible sur le bus de données au moyen du signal d’activation Enable du verrou 2. Si le code comporte un troisième octet, c’est qu’il s’agit d’une adresse mémoire sur 16 bits qui peut être mise à disposition sur le bus d’adresse au moyen du signal d’activation Enable des deux verrous 2 et 3.

Le dernier composant est le circuit qui incrémente ou décrémente une valeur sur 16 bits. Il est en général abrégé en Inc/Dec :

Figure 23.6

Le signal d’horloge Clock déclenche la lecture de la valeur sur le bus d’adresse pour stockage dans le verrou Incrémenteur/Décrémenteur. Les deux signaux d’activation Enable permettent d’activer soit l’incrémentation, soit la décrémentation de la valeur.

Pour vous donner une première idée de la façon dont ces différents signaux de contrôle doivent être coordonnés, partons d’un tout petit programme pour 8080 composé de six instructions. Certaines d’entre elles sont sur un octet et d’autres fonctionnent avec un ou deux octets complémentaires après le code opération :

Figure 23.7

Ce programme n’effectue pas un énorme traitement. La première instruction stocke la valeur 27h dans le registre A (l’accumulateur). L’instruction MOV copie la valeur dans le registre B. Nous ajoutons ensuite la valeur 61h au contenu de l’accumulateur qui contient 88h. La valeur trouvée dans le registre B y est ajoutée, ce qui amène la valeur totale à AFh. Avec l’instruction STA, nous stockons la valeur à l’adresse mémoire 000Ah. Nous terminons avec une instruction HLT qui provoque la halte du processeur, et marque donc la fin définitive d’exécution du programme.

Évaluons les besoins du processeur pour qu’il puisse réaliser cette série d’instructions. Le processeur dépend du pointeur d’instruction pour savoir quelle cellule mémoire lire afin d’y trouver la prochaine instruction, et copier cette instruction dans un à trois des verrous d’instruction. Au départ, le pointeur d’instruction est forcé à la valeur 0000h, afin d’aller chercher le code de la première instruction en mémoire. Dans l’exemple, il s’agit de l’instruction MVI, pour un déplacement immédiat (MoVe Immediate) qui doit transférer la valeur fournie 27h dans l’accumulateur.

Cinq étapes sont nécessaires pour réaliser la première instruction. Chaque étape suppose de placer quelque chose sur le bus d’adresse puis de le sauvegarder ailleurs, ou bien placer quelque chose sur le bus de données et le sauvegarder ailleurs, ou parfois les deux.

La première action consiste à scruter la mémoire au moyen de la valeur trouvée dans le pointeur d’instruction, soit 0000h pour stocker le code d’opération 3Eh qui est trouvé dans le verrou d’instruction 1. L’opération met en jeu quatre signaux de contrôle et les deux bus (adresse et données) :

PC Enable : met à disposition la valeur du PC sur le bus d’adresse, valeur qui est au départ 0000h.

Enable Data Out RAM : rend disponible sur le bus de données la valeur lue en mémoire à l’adresse spécifiée. Cette valeur est dans l’exemple 3Eh.

Clock Incrémenteur/Décrémenteur : recopie la valeur trouvée sur le bus d’adresse dans le composant Incrémenteur/Décrémenteur.

Clock Verrou d’instruction 1 : stocke la valeur trouvée sur le bus de données dans le verrou d’instruction 1.

La deuxième des cinq étapes consiste à augmenter de 20 (incrémenter) le pointeur d’instruction PC. Seul le bus d’adresse est concerné :

Enable Incrément : place sur le bus d’adresse la valeur après incrémentation trouvée dans le composant Inc/Dec. Cette valeur est donc égale à 0001h.

Clock PC : stocke la valeur d’adresse incrémentée dans le pointeur d’instruction PC.

Une fois que le premier octet de l’instruction a été stocké dans le verrou d’instruction 1, nous nous en servons pour les étapes suivantes. Dans notre exemple, la troisième et la quatrième étape sont les mêmes que la première et la deuxième sauf qu’elles traitent l’adresse mémoire 0001h et stockent la valeur dans le verrou d’instruction 2.

Ces étapes de lecture d’octet en mémoire portent le nom de recherche ou chargement d’instruction (fetch). Leur but est d’alimenter le travail en récupérant des octets d’instruction en mémoire et en les positionnant dans les verrous d’instruction. Dans le cas de l’instruction de déplacement MVI 27h, c’est la valeur 27h qui est stockée dans le verrou d’instruction 2. Il reste encore à transférer cette valeur dans l’accumulateur, ce qui correspond à la cinquième étape. Celle-ci correspond à la phase d’exécution effective de l’instruction :

Enable Verrou d’instruction 2 : place la valeur trouvée dans ce verrou sur le bus de données.

Clock Accumulateur : enregistre la valeur trouvée sur le bus de données dans l’accumulateur.

Vous constatez que chacune de ces cinq étapes ne fait intervenir, au plus, qu’une valeur sur le bus d’adresse et une autre sur le bus de données. L’une et/ou l’autre valeur est ensuite stockée ailleurs.

Passons ensuite à la deuxième instruction, MOV B, A, qui ne réclame que deux étapes pour la préparation puisqu’elle n’a pas plusieurs octets de suite. L’étape d’exécution est celle-ci :

Enable RA : place la valeur trouvée dans un registre sur le bus de données.

Clock RA : enregistre dans un registre la valeur trouvée sur le bus de données.

Mais il y a un souci. Dans la description des exécutions ci-dessus, je ne parle que de registres au sens générique sans indiquer explicitement lequel. Pourquoi ?

C’est très simple : le champ de bits qui caractérise le groupe d’instructions MOV du processeur 8080 suit le format suivant :

0 1 D D D S S S

Dans ce masque, DDD désigne le registre destinataire et SSS, le registre source. Le code d’opération a été enregistré dans le verrou 1. Vous vous souvenez que la rangée de registres RA est dotée de deux trios de signaux sélecteurs servant à désigner un des registres comme source et un autre comme destination. Vous voyez que les signaux sont directement tirés de la valeur de certains bits du code d’opération stocké dans le verrou d’instruction 1. Pour réaliser l’exécution, il ne reste donc plus qu’à activer toute la rangée de registres et les verrous de sortie.

Nous arrivons ensuite à une instruction d’addition immédiate :

ADI 61h

C’est une des huit instructions d’addition au format suivant :

11 F F F 1 1 0

Dans ce masque, les trois bits FFF désignent la fonction exacte à réaliser : addition, addition avec retenue, soustraction, soustraction avec retenue, opération logique ET, XOR, OU, ou encore comparaison. Vous vous souvenez que l’unité ALU comportait une entrée de sélection de fonction sur 3 bits. Autrement dit, les 3 bits de fonction qui font partie du code d’opération et qui sont stockés dans le verrou d’instruction peuvent être directement injectés dans l’ALU.

Une fois qu’ont été rapatriés les 2 octets qui constituent l’instruction ADI, il reste deux étapes pour en terminer l’exécution. Voici la première :

Enable verrou d’instruction 2 : place la valeur 61h sur le bus de données.

Clock ALU : stocke le résultat de l’ALU et les drapeaux dans des verrous.

La seconde étape d’exécution sert à transférer le résultat dans l’accumulateur :

Enable ALU : place le résultat de l’ALU sur le bus de données.

Clock Acc : copie la valeur du bus de données vers l’accumulateur.

L’instruction ADD qui vient ensuite demande elle aussi deux étapes d’exécution dont voici la première :

Enable RA : place le contenu du registre B sur le bus de données.

Clock ALU : sauvegarde le résultat de l’addition et les drapeaux.

La seconde étape est la même que pour l’instruction ADI précédente. Nous parvenons ensuite à l’instruction de stockage STA qui réclame six étapes de préparation. Les 2 octets qui suivent le code de l’opération sont stockés dans les verrous 2 et 3. Pour l’exécution proprement dite, nous utilisons trois signaux de contrôle :

Enable verrous d’instruction 2 et 3 : place le second et le troisième octet de l’instruction sur le bus d’adresse pour désigner une cellule mémoire.

Enable Acc : place la valeur de l’accumulateur sur le bus de données.

RAM Write : stocke la valeur trouvée sur le bus de données dans la mémoire.

Nous arrivons finalement à l’instruction HLT qui réalise l’opération définitive d’arrêt d’exécution du processeur. Nous verrons plus loin dans ce chapitre les détails de la réalisation de cette opération.

Les étapes que nous venons de parcourir correspondent à des cycles. Il en va un peu comme les cycles successifs d’une machine à laver le linge : laver, rincer, essorer. En termes techniques, on parle de cycles machine. Dans le processeur que nous sommes en train de construire, les octets des instructions sont récupérés dans la mémoire en un cycle qui est toujours suivi d’un autre cycle incrémentant la valeur du pointeur d’instruction. Selon que l’instruction comprend 1, 2 ou 3 octets, le processeur doit donc exécuter deux, quatre ou six cycles.

L’exécution à proprement parler requiert un ou deux cycles machine, selon la nature de l’instruction. Le tableau suivant rappelle quelles actions sont réalisées pendant le premier cycle d’exécution des instructions du petit programme d’exemple :

Tableau 23.1 : Premier cycle d’exécution.

Instruction
Bus d’adresse 16 bits
Bus de données 8 bits

MOV r, r

Enable Rangée de registres

Clock Rangée de registres

MOV r, M

HL Enable

Enable Data Out RAM

Clock Rangée de registres

MOV M, r

HL Enable

Enable Rangée de registres

Write RAM

MVI r, data

Enable Verrou Instruction 2

Clock Rangée de registres

MVI M, data

HL Enable

Enable Verrou Instruction 2

RAM Write

LDA

Instruction Latch 2 & 3 Enable

Enable Data Out RAM

Clock Accumulateur

STA

Instruction Latch 2 & 3 Enable

Enable Accumulateur

Write RAM

ADD r...

Enable Rangée de registres

Clock ALU

ADD M...

HL Enable

Enable Data Out RAM

Clock ALU

ADI data...

Enable Verrou Instruction 2

Clock ALU

INX/DCX HL

HL Enable

Incrementer-Decrementer Clock

Remarquez la présence des points de suspension dans la première colonne pour trois instructions. Les deux lignes mentionnant l’instruction ADD englobent toute la série : ADC, SUB, SBB, ANA, XRA, ORA et CMP. La ligne de ADI englobe également ACI, SUI, SBI, ANI, XRI, ORI et CPI.

Les quatre dernières lignes du tableau correspondent à des instructions qui ont besoin d’un deuxième cycle d’exécution. Le tableau suivant présente les actions réalisées pendant ce deuxième cycle :

Tableau 23.2 : Deuxième cycle d’exécution.

Instruction
Bus d’adresse 16 bits
Bus de donnée 8 bits

ADD r...

Enable ALU

Clock Accumulateur

ADD M...

Enable ALU

Clock Accumulateur

ADI data...

Enable ALU

Clock Accumulateur

INX HL

Enable Incrément

HL Select

HL Clock

DCX HL

Enable Décrément

HL Select

HL Clock

Pour que ces opérations soient possibles, il faut que le code soit décodé et transformé en signaux de contrôle qui vont piloter les composants et la mémoire. Ce sont les signaux qui vont activer les tampons triple état, les signaux d’horloge d’entrée des verrous, les signaux d’écriture en mémoire RAM et quelques autres entrées.

Nous allons voir comment tout cela est réalisé en détail dans la suite de ce chapitre. Le processus réclame plusieurs phases et différentes stratégies.

Voici tout d’abord un tableau présentant les codes d’opérations des différentes instructions :

Tableau 23.3

Instruction
Code d’opération

MOV r, r

01DDDSSS

MOV r, M

01DDD110

MOV M, r

01110SSS

HLT

01110110

MVI r, data

00DDD110

MVI M, data

00110110

ADD, ADC, SUB, SBB, ANA, XRA, ORA, CMP r

10FFFSSS

ADD, ADC, SUB, SBB, ANA, XRA, ORA, CMP M

10FFF110

ADI, ACI, SUI, SBI, ANI, XRI, ORI, CPI data

11FFF110

INX HL

00100011

DCX HL

00101011

LDA adr

00111010

STA adr

00110010

Pour plus de confort, je rappelle les codes sur 3 bits SSS et DDD qui déterminent les registres source et destinataire dans les codes d’opération :

Tableau 23.4

SSS ou DDD
Registre concerné

000

B

001

C

010

D

011

E

100

H

101

L

111

A

Je précise à nouveau que la séquence 110 n’est pas présentée dans la liste parce qu’elle fait référence à un adressage mémoire au moyen du contenu de la paire de registres HL.

Dans le Tableau 23.3, le masque de bits FFF des opérations arithmétiques et logiques désigne une fonction arithmétique ou logique parmi les huit possibles.

Une des parties les moins ardues de la circuiterie des signaux de contrôle du processeur est la connexion entre le verrou d’instruction 1 et, d’une part, les sélecteurs d’entrées Input Select et de sortie Output Select des registres et, de l’autre, la sélection de fonction Function Select de l’ALU :

Figure 23.8

La lettre C indicée sur les sorties signifie Code. Les trois premiers bits C₀, C₁ et C₂ sont injectés directement dans le sélecteur d’entrée du bloc de registres alors que les 3 bits C₃, C₄ et C₅ sont injectés dans le sélecteur de sortie du même bloc de registres, mais aussi sur les entrées de sélection de fonction de l’ALU. C’est une technique qui exploite l’identification par masque binaire.

En passant en revue les codes d’opération du Tableau 23.3, vous pouvez remarquer d’autres motifs : tous les codes qui commencent par 01 forment la famille des déplacements MOV, sauf 76h qui est celui d’arrêt HLT. De même, à l’exception des variantes immédiates telles que ADI, ACI et autres, toutes les instructions arithmétiques et logiques commencent par 10.

Pour décoder un code, la première étape consiste à diriger les bits en sortie du verrou d’instruction 1 vers trois décodeurs différents : un décodeur 2-vers-4 et deux décodeurs 3-vers-8. Ils vont servir à produire d’autres signaux, dont certains sont directement des instructions et d’autres des groupes d’instructions :

Figure 23.9

Le signal du groupe MOV en haut à droite englobe toutes les instructions qui commencent par les bits 01. De même, le groupe arithmétique et logique englobe les instructions commençant par les bits 10.

Il faut bien distinguer le groupe d’instructions MOV dont le travail est le déplacement d’octet entre des registres et l’autre groupe d’instructions de déplacement qui concerne les registres et le circuit mémoire. Ces dernières instructions sont identifiées par une valeur 110 pour la source et la destination. Les deux signaux Source et Destination mémoire dans la Figure 23.9 permettent de savoir quand les bits source et destination valent 110. Enfin, le groupe des déplacements MOV immédiats est celui des instructions dont le code commence par la valeur 00 et se termine par 110.

Les cinq premiers signaux en haut à droite de la Figure 23.9 reçoivent un traitement plus détaillé que voici :

Figure 23.10

Suite à ce décodage, chaque instruction ou chaque groupe d’instructions apparentées dispose de son signal spécifique. Les signaux sont disponibles lorsque le code est placé dans le verrou d’instruction et sont exploités pour participer de différentes façons au maintien de la cohérence dans le traitement de l’instruction.

Le code d’opération doit servir à savoir combien d’octets de suite il faut aller chercher en mémoire et combien de cycles machine sont à prévoir pour exécuter l’instruction :

Figure 23.11

Mais comment réagir lorsque le décodage d’un code n’aboutit à aucune instruction ? Je n’ai pas encore mentionné une instruction 8080 tout à fait spéciale qui porte le nom NOP et qui ne fait rien à part consommer un cycle d’exécution. Son sigle signifie NO Operation et son code est fort judicieusement égal à 00h.

Selon le schéma, lorsque aucun des signaux d’entrée à gauche n’est à l’état 1, alors toutes les sorties des portes OU sont à 0. Dans ce cas, l’état des signaux du côté droit correspond à une étape d’alimentation ou de chargement (fetch) d’un seul octet suivie d’un seul cycle d’exécution.

La chronologie de base du processeur va être produite par une version améliorée du petit circuit conçu autour de deux bascules que nous avions vu dans le Chapitre 20 :

Figure 23.12

L’oscillateur à gauche produit sur sa sortie une alternance généralement très rapide entre les deux états 0 et 1 qui sont les battements du cœur du processeur.

Le signal Reset en haut provient d’une source extérieure. En général, il correspond à un bouton qu’un humain sollicite pour forcer le redémarrage du processeur. En temps normal, ce signal Reset est à 0. Lorsqu’on appuie sur un bouton (par exemple), le signal passe à 1, ce qui provoque l’arrêt d’exécution du processeur et sa restauration dans l’état initial.

Dans l’exemple, ce signal Reset force à 0 les trois bascules, de telle sorte que les sorties Q soient ramenées à 0, et donc les sorties Q (prononcer Q-barre) à 1. Lorsque le signal Reset repasse à 0, les bascules se remettent à fonctionner normalement et le processeur procède.

Après cette réinitialisation du processeur, la sortie Q de la bascule du haut est à l’état 1. Elle constitue l’une des deux entrées d’une porte et permet à l’oscillateur de contrôler les entrées d’horloge des deux bascules du bas du schéma.

Le signal Halt en haut indique qu’il y a eu exécution d’une instruction d’arrêt HLT. Dans ce cas, la sortie Q de la bascule du haut passe à 0, ce qui interdit à l’oscillateur de continuer à animer le processeur. Pour réveiller le processeur, on utilise un signal Reset.

Tant que le processeur n’a pas été forcé à l’arrêt, les deux bascules du bas du schéma génèrent deux signaux portant les noms Horloge de cycle et Pulsation. Voici leur chronogramme :

Figure 23.13

Un cycle de l’horloge de cycle correspond à un cycle machine. Lorsque le signal d’horloge de cycle passe par un front montant (de 0 à 1), il se produit un nouveau cycle machine.

Dans le petit programme d’exemple du début de ce chapitre, la première instruction est un déplacement immédiat MVI (Move Immediate). Cette instruction consomme cinq cycles machine :

chargement du code d’opération (fetch) ;

incrémentation du pointeur d’instruction PC ;

chargement de l’octet qui suit le code ;

incrémentation du pointeur d’instruction PC ;

exécution de l’instruction.

Voici l’enchaînement de ces cinq cycles, en les identifiant sous forme abrégée :

Figure 23.14

Chacun de ces cycles suppose d’activer des tampons triple état différents, et donc de mettre à disposition des valeurs différentes sur le bus d’adresse ou le bus de données. Par exemple, au cours du premier cycle de chargement (fetch), le contenu du pointeur d’instruction est publié sur le bus d’adresse pendant que les sorties de données mémoire Data Out sont rendues accessibles sur le bus de données. Le signal Pulsation sert à contrôler l’entrée d’horloge du verrou d’instruction 1, ainsi que l’entrée d’horloge Clock du composant Incrémenteur/Décrémenteur.

Pendant le cycle d’incrémentation du pointeur d’instruction, la sortie de l’incrémenteur est disponible sur le bus d’adresse. Le signal Pulsation sert alors à injecter cette valeur incrémentée dans le pointeur d’instruction.

Nous avons vu plus haut un circuit permettant de savoir si une instruction était constituée de 1, 2 ou 3 octets et si l’instruction nécessitait un ou deux cycles d’exécution.

La suite des opérations de décodage du code d’opération consiste à produire les signaux qui vont permettre de savoir quel est le cycle en cours : premier, deuxième ou troisième cycle de chargement/préparation, cycle d’incrémentation du PC ou encore premier ou second cycle d’exécution.

Ce décodage fin est pris en charge par un circuit assez complexe que nous présentons ci-dessous :

Figure 23.15

Les entrées sont du côté gauche et les sorties, du côté droit. Dans certains cas, les noms sont les mêmes des deux côtés, ce qui peut perturber la lecture. C’est ainsi que l’entrée Chargement 2 octets indique que l’instruction est constituée de 2 octets. En revanche, la sortie Cycle de chargement 2 indique que c’est le deuxième octet de l’instruction qui est en cours de chargement.

Tout en haut, nous trouvons un signal Reset qui est le même que celui du circuit précédent. Son état peut être inversé par l’action d’une personne pour forcer le redémarrage du processeur. Notez que le compteur 4 bits peut être remis à zéro mais, dans son cas, il se fonde aussi sur un signal prélevé dans le bas du schéma.

L’horloge de cycle sert à faire progresser le compteur. Puisqu’il est sur 4 bits, il peut compter de 0 à 15, soit 0000 à 1111 en binaire. Ses sorties sont directement envoyées dans un décodeur 4-vers-16 pouvant produire seize sorties séquentielles dont seulement neuf sont utilisées. Chaque sortie indique un cycle machine différent, qui peut être un cycle de chargement, un cycle d’incrémentation du pointeur d’instruction (Incrémentation PC dans le schéma) ou encore un cycle d’exécution.

Au fur et à mesure de la progression des sorties du décodeur en ordre croissant (0, 1, 2, 3 ; etc.), différents signaux sont générés :

0. Cycle de chargement 1.

1. Incrémentation du pointeur d’instruction PC.

2. Cycle de chargement 2, seulement si l’instruction n’est pas sur 1 octet de chargement.

3. Incrémentation du PC si chargement octet 2 ou 3.

4. Cycle de chargement 3, seulement si le signal Chargement 3 octets est à 1.

5. Incrémentation du PC si chargement octet 3.

Les deux premières étapes sont bien sûr toujours générées (cycle de chargement 1 et première incrémentation du PC). Une fois ces cycles réalisés, le code est connu et chargé dans le verrou d’instruction 1. Son décodage a commencé et tous les signaux d’entrée de gauche dans le schéma précédent sont disponibles.

Une instruction peut réclamer jusqu’à trois cycles de préparation/chargement, chacun devant être sanctionné par un cycle d’incrémentation PC puis, au plus, deux cycles d’exécution, soit huit cycles au maximum, ce qui correspond aux sorties du décodeur de 0 à 7.

L’identification des différentes étapes n’est pas évidente, car il faut tenir compte des combinaisons entre le nombre de cycles de chargement et celui des cycles d’exécution. C’est ainsi que le signal Cycle d’exécution 1 du côté droit peut être soit le troisième cycle d’une instruction qui n’a besoin que d’un octet de chargement, soit le cinquième cycle d’une instruction qui en demande deux, ou encore le septième d’une instruction qui demande trois cycles de chargement.

La partie la plus complexe est celle de la logique Reset en bas. Dans le cas d’une instruction simple constituée d’un seul cycle de chargement et d’un seul cycle d’exécution, cette réinitialisation correspond au quatrième cycle. Au contraire, pour une instruction avec trois cycles de chargement et deux d’exécution, c’est le neuvième cycle.

Le pointeur d’instruction PC est activé sur le bus 16 bits pendant les trois cycles de chargement, et les sorties de données mémoire sont activées sur le bus de données. Le signal Pulsation copie la valeur trouvée sur le bus d’adresse dans le composant Incr/Decr ainsi que la valeur trouvée sur le bus de données dans l’un des trois verrous d’instruction. C’est la raison d’être du circuit suivant qui génère tous les signaux pour les cycles de chargement d’instruction, mais pas les cycles d’incrémentation du PC :

Figure 23.16

Lors de chacun des trois cycles de chargement, il y a publication du contenu du pointeur d’instruction sur le bus d’adresse et des sorties de données mémoire sur le bus de données. Dans les trois cas, le signal Pulsation maintient le contrôle de l’entrée d’horloge du verrou Incr/Decr. Le même signal contrôle également l’horloge du verrou d’instruction concerné par chacun des trois cycles de chargement.

Vous constatez la présence de tampons triple état sur deux des signaux. Leur présence est justifiée par d’autres circuits (présentés plus loin) qui peuvent eux aussi prendre le contrôle du signal Enable des tampons triple état de sortie de données Data Out ainsi que du signal d’horloge Clock du verrou Incr/Decr. Le signal qui se trouve sur la gauche du tampon triple état est simultanément le signal d’entrée et le signal Enable.

En ce qui concerne les signaux nécessaires au cycle d’incrémentation du PC, ils sont pris en charge entièrement par le circuit suivant :

Figure 23.17

Au point où nous en sommes, tous les signaux nécessaires pour gérer les cycles de chargement d’instruction et d’incrémentation du pointeur PC ont été définis. Il nous reste à étudier les signaux des cycles d’exécution. Sachez qu’ils sont plus complexes parce que leurs détails dépendent des caractéristiques de l’instruction à exécuter.

Le circuit assez complexe de la Figure 23.15 comporte deux signaux de sortie Cycle d’exécution 1 et Cycle d’exécution 2. Dans la suite, ces deux signaux seront désignés par les sigles CE1 et CE2 :

Figure 23.18

Chacun de ces deux signaux est combiné avec le signal Pulsation pour produire deux signaux de pulsation d’exécution marquées PE1 et PE2.

Commençons par une instruction dont le traitement est assez simple : l’instruction HLT qui provoque l’arrêt d’exécution du processeur :

Figure 23.19

Le signal HLT à gauche est issu du décodeur d’instruction illustré à la Figure 23.10. Le signal Halt de la sortie à droite est injecté dans le circuit doté d’un oscillateur en Figure 23.12.

Les relations établies entre les autres instructions et les signaux de contrôle à générer sont suffisamment complexes pour que nous prenions soin d’éviter de les traiter en faisant appel à un grand nombre de portes logiques. Nous allons gérer cela en créant des matrices de diodes servant de mémoire morte ROM, un peu comme celle que nous avions adoptée pour notre horloge dans le Chapitre 18.

La première matrice de diodes va prendre en charge tous les signaux d’activation Enable et d’horloge Clock en relation avec le bus d’adresse sur 16 bits, aussi bien pour le premier que pour le second cycle d’exécution :

Figure 23.20

Notez que les signaux produits en bas de la figure ne sont destinés qu’au bus d’adresse. Nous verrons plus loin les signaux du bus de données. Ce schéma reprend les noms des signaux spécifiés dans la colonne du bus d’adresse des Tableaux 23.1 et 23.2.

Le tampon triple état de gauche est activé par le signal du cycle d’exécution 1 qui détermine la valeur se trouvant sur le bus d’adresse au cours de ce premier cycle d’exécution. Cette valeur correspond au contenu des registres HL pour toutes les instructions de déplacement MOV, MVI et les instructions arithmétiques qui exploitent le stockage mémoire désigné par les registres H et L.

Le même couple de registres HL est activé pour les deux instructions INX et DCX qui servent à incrémenter ou décrémenter le contenu de HL. En revanche, c’est dans les verrous d’instruction 2 et 3 que l’on obtient l’adresse mémoire pour charger ou stocker un octet pour les instructions LDA et STA.

Pour les instructions INX et DCX, le signal Pulsation d’exécution 1 provoque la sauvegarde du contenu des registres HL dans le verrou Incrémenteur/ Décrémenteur.

Ces deux instructions INX et DCX sont les seules qui exploitent le bus d’adresse pendant le second cycle d’exécution puisqu’elles rendent disponible sur le bus d’adresse la valeur modifiée de HL (incrémentée ou décrémentée). C’est ensuite que Pulsation d’exécution 2 provoque le stockage de la nouvelle valeur d’adresse dans les deux registres H et L.

En ce qui concerne le bus de données 8 bits, la matrice de diodes de décodage est un peu plus complexe. Je l’ai de ce fait répartie sur deux schémas, un par cycle d’instruction. Voici celle du premier cycle d’instruction :

Figure 23.21

Ce circuit reprend les noms des signaux de la colonne du bus de données du Tableau 23.1. Les deux tampons triple état sont activés par les deux signaux Cycle exécution 1 et Pulsation d’exécution 1. Le premier tampon contrôle le contenu du bus de données et l’autre le lieu de stockage de la valeur.

Les trois variantes d’instructions MOV en haut à gauche du schéma sont suivies par l’indication de la destination et de la source. Ces cibles peuvent être n’importe lequel des registres ou une adresse mémoire spécifiée par la paire de registres HL. Lorsque la source est un registre, la rangée de registres (abrégée en RA dans le schéma) a accès au bus de données. Lorsque la source est la mémoire, c’est la sortie Data Out de la mémoire qui est rendue accessible. (N’oubliez pas que l’on accède à la mémoire RAM par le bus d’adresse sur 16 bits à ce moment et que la matrice de diodes du bus d’adresse définit cette valeur dans les registres HL.)

Lorsque la désignation est un registre, l’autre tampon triple état contrôle l’entrée d’horloge de la rangée de registres. Lorsque la destination est la mémoire, c’est le signal d’écriture Write RAM qui déclenche le stockage de la valeur en mémoire.

Dans le cas des deux instructions de déplacement immédiat MVI, c’est le contenu du verrou d’instruction 2 qui est autorisé sur le bus de données. La valeur est stockée soit dans un des registres, soit en mémoire.

Toutes les opérations arithmétiques et logiques sont regroupées dans ce schéma sous les noms d’instruction ADD et ADI (addition immédiate). La valeur qui est disponible sur le bus de données est soit celle d’un registre, soit la sortie Data Out de la mémoire, ou bien le verrou d’instruction 2, tout dépendant de l’instruction. Dans tous les cas, la valeur est mémorisée dans l’unité ALU. Ces instructions requièrent un traitement supplémentaire pendant le second cycle d’exécution, comme nous le verrons bientôt.

En ce qui concerne les opérations concernant l’accumulateur LDA de chargement et STA de stockage, c’est la matrice de diodes du bus d’adresse qui garantit que l’on accède à la mémoire au moyen du contenu des verrous d’instruction 2 et 3. Dans le cas de LDA, les sorties de données de la mémoire sont activées sur le bus de données et la valeur est stockée dans l’accumulateur. Dans le cas de STA, l’accumulateur est autorisé sur le bus de données pour que la valeur puisse être stockée en mémoire.

Nous avons dit qu’il fallait un second cycle d’exécution pour les instructions arithmétiques et logiques utilisant le bus de données. La matrice de diodes correspondante est beaucoup plus simple que dans les autres cas :

Figure 23.22

Pour toutes ces instructions, la valeur se trouvant dans l’unité ALU est autorisée sur le bus de données pour être stockée dans l’accumulateur, comme le confirme le contenu de la colonne Bus de données du Tableau 23.2.

Au point où nous en sommes, la version simplifiée d’un microprocesseur de type 8080 que j’ai choisi de concevoir au cours des trois chapitres qui se terminent ici est complète. Vous pouvez admirer une simulation fonctionnelle du résultat sur le site Web compagnon du livre.

Les concepteurs des processeurs passent souvent beaucoup de temps à chercher à rendre leurs machines les plus rapides possibles. En effet, certains choix de conception des circuits logiques peuvent s’avérer plus efficaces et apporter de meilleures performances que d’autres. Très souvent, il faut ajouter des portes logiques pour rendre un circuit numérique plus rapide.

Si j’avais l’intention de rendre plus rapide le processeur que nous venons de concevoir, je commencerais par chercher à optimiser les cycles de chargement des instructions (fetch). Ces chargements imposent un second cycle machine uniquement pour incrémenter le pointeur d’instruction. Je chercherais à inclure cette opération logique dans le cycle de chargement, en sorte de faire les deux opérations en même temps. Cela suppose sans doute d’ajouter un composant d’incrémentation dédié, ce qui au final diviserait par deux le temps de chargement d’une instruction depuis la mémoire !

Même de petites retouches peuvent avoir de grands effets. Si le processeur que vous concevez est destiné à servir dans des milliers d’ordinateurs, chacun d’eux exécutant des millions d’instructions à chaque seconde, la possibilité d’économiser un seul cycle machine sera un avantage énorme pour chacun des utilisateurs.

Prenons comme exemple un programme très simple que peut exécuter ce processeur. Nous supposons que nous avons stocké 5 octets en mémoire à partir de l’adresse 1000h. Nous voulons qu’un programme en fasse l’addition. Le voici :

Figure 23.23

Les deux premières instructions arment les contenus des registres H et L, puis le programme utilise le contenu de HL pour accéder aux octets et accumuler le sous-total en incrémentant HL après chaque accès mémoire.

Vous remarquez qu’il y a un certain nombre de répétitions. Une instruction INX est suivie d’une instruction ADD quatre fois de suite. Ce n’est pas gênant pour un programme aussi petit que celui-ci, mais si nous devions additionner vingt valeurs ? Une centaine ? Et si les opérations ne concernaient pas des octets, mais des valeurs sur 16 ou 32 bits, ce qui supposerait d’ajouter des instructions pour réaliser les additions ?

Pourrait-on s’épargner de telles répétitions ? Pourrait-on imaginer une instruction qui provoquerait l’exécution à répétition d’un groupe d’autres instructions ? À quoi ressemblerait-elle ? Comment cela fonctionnerait-il ?

Ce sujet est tellement essentiel qu’un chapitre entier lui est consacré.

1 N.d.T. : rappelons que nous utilisons l’abréviation PC pour le pointeur d’instruction parce que ce sigle correspond au terme anglais Program Counter (« compteur de programme », un terme moins précis que « pointeur d’instruction »).

Chapitre 24  Boucles, sauts et appels

Nos existences sont pleines de répétitions. La succession des jours épouse le rythme de rotation de la Terre, les semaines obéissent à l’orbite lunaire et les saisons résultent de la danse de la Terre autour du Soleil. Même si chacune de nos journées est unique, nos vies sont fondamentalement structurées par des routines qui reviennent d’un jour à l’autre.

Le principe de répétition est tout autant fondamental en informatique. Personne ne verrait l’utilité d’un ordinateur s’il ne savait additionner que deux nombres (du moins je le suppose). En revanche, additionner un millier ou un million de nombres semble une mission sur mesure pour un ordinateur.

Cette relation entre travail répétitif et automatisation avait déjà été remarquée par Lady Ada Lovelace. Dans son fameux article de 1843 à propos du moteur analytique de Charles Babbage, elle avait notamment écrit ceci :

Dans un but de brièveté et de clarification, nous dirons qu’un groupe récurrent se nomme un cycle. Nous désignerons donc comme cycle d’opération tout ensemble d’opérations répété au moins une fois. Cela reste un cycle, qu’il soit répété une seule fois ou un nombre infini de fois. C’est l’occurrence de la répétition qui le constitue. Dans de nombreuses situations d’analyse, on constate l’existence d’un groupe récurrent constitué d’un ou de plusieurs cycles, c’est-à-dire du cycle d’un cycle ou d’un cycle de cycles.

Dans la terminologie actuelle, on appelle ces cycles des boucles. Ce que Ada appelle « cycle d’un cycle » est de nos jours une boucle imbriquée.

Il semble que le processeur dont j’ai décrit la conception au cours des chapitres précédents soit déficient dans ce domaine fonctionnel. Nous avons vu à la fin du chapitre précédent un programme d’exemple additionnant 5 octets stockés en mémoire à partir de l’adresse 1000h, puis stockant le résultat ailleurs :

Figure 24.1

L’adresse mémoire est stockée dans la paire de registres HL. Le premier octet à additionner est lu en mémoire vers l’accumulateur au moins d’une instruction MOV (rappelons que le symbole M n’est pas le nom d’un registre mais l’abréviation du mot Mémoire). Une série d’instructions ADD permet ensuite d’ajouter les 4 octets au contenu de l’accumulateur. Après chaque lecture d’un octet, la valeur correspondant à l’adresse dans la paire de registres HL est incrémentée au moyen d’une instruction INX. Le résultat final est enfin stocké en mémoire au moyen d’une instruction STA.

Comment modifier ce programme pour enchaîner l’addition d’une centaine ou d’un millier de valeurs ? Devons-nous bêtement multiplier les couples d’opérations INX et ADD pour qu’il y en ait autant que de nombres à additionner ? Cela ne semble pas très intelligent et cette solution ne conviendrait pas à des besoins divergents. Elle n’est pas généralisable.

Il serait intéressant de disposer d’une nouvelle instruction grâce à laquelle on pourrait répéter certains blocs d’instructions antérieurs ; dans notre exemple, ce seraient les deux instructions INX et ADD. À quoi ressemblerait une telle instruction ?

En première approche, cette instruction devrait être très différente de toutes celles que nous connaissons jusqu’à maintenant. Vous pourriez craindre qu’il faille revoir de fond en comble la conception du processeur. Mais ne soyez pas désespéré.

Vous savez que le pointeur d’instruction voit sa valeur augmenter de 1 après chaque lecture d’un octet d’instruction par le processeur. C’est de cette façon que le processeur s’alimente en travail à réaliser. Si une instruction doit provoquer un rebouclage, elle doit certainement intervenir sur l’adresse que contient le pointeur d’instruction, mais d’une façon différente.

Nous savons que les instructions d’accès mémoire telles que LDA et STA sont toujours suivies de 2 octets qui constituent une adresse mémoire sur 16 bits. Imaginons une instruction elle aussi suivie de 2 octets, mais en considérant que l’adresse définie par ces octets n’est pas celle à laquelle il faut stocker ou lire une donnée, mais celle d’une instruction en code machine. Cette adresse va être stockée dans le pointeur d’instruction PC. Bien sûr, cela détournera le cours normal de l’exécution puisque, effectivement, le pointeur d’instruction va sauter à une autre adresse que l’adresse immédiatement supérieure à l’actuelle comme lors d’une incrémentation.

Nous pouvons relier le concept de saut à cette instruction (jump signifie « saut » en anglais). C’est d’ailleurs l’acronyme utilisé dans les processeurs Intel 8080. La famille de microprocesseurs Motorola 6809 a choisi comme mnémonique pour cette instruction le mot BRA qui signifie « branchement ».

Notre instruction JMP est donc suivie de 2 octets qui constituent une adresse sur 16 bits. Voici un exemple d’utilisation de notre nouvelle instruction. La valeur est 0005h.

Figure 24.2

Une fois que les deux instructions INX et ADD ont été exécutées, l’arrivée à l’instruction JMP provoque l’exécution à l’adresse 0005h, ce qui fait à nouveau exécuter la paire d’instructions INX et ADD. C’est une boucle.

L’adoption de cette instruction JMP est donc très simple. Mais ne soyons pas trop enthousiastes : nous avons un sérieux souci. En effet, ce petit programme avec son innocente instruction JMP ne va jamais s’arrêter de s’exécuter. Il n’y a rien de prévu pour sortir de la boucle ; on parle à juste titre de boucle infinie ou infernale. La valeur de l’adresse dans HL va augmenter sans cesse, et l’octet trouvé à cette adresse va être ajouté à la somme courante dans l’accumulateur. La valeur dans HL va finir par atteindre le plafond FFFFh tout en haut de l’espace mémoire. Le fait d’incrémenter cette valeur la fait retomber à 0000h et le programme va continuer d’additionner des octets dans l’accumulateur, sauf qu’à cause du décalage, il va essayer d’additionner comme valeurs des octets qui sont des codes d’instructions !

Les boucles sont donc indispensables à la programmation, mais il est indispensable de savoir en sortir, bref, de garder le contrôle sur le nombre de tours de boucle.

Disposons-nous d’un mécanisme dans notre processeur qui permettrait de contrôler l’exécution d’un saut, de décider si un saut doit être exécuté ou pas ?

Oui, nous en avons les moyens. Vous vous souvenez sans doute que notre unité ALU du Chapitre 21 générait et mémorisait un certain nombre de drapeaux : le drapeau de retenue Carry, le drapeau de zéro Z et le drapeau de signe S. Ils permettent de savoir respectivement si la dernière opération de l’ALU a provoqué une retenue, si le résultat est égal à zéro ou si ce résultat est un nombre négatif, c’est-à-dire un nombre qui, en complément à deux, a son bit de poids fort à 1.

Nous pourrions donc concevoir une instruction qui n’effectue le saut que si le drapeau de zéro Z est armé ou que s’il ne l’est pas. Nous pouvons même définir toute une petite famille d’instructions de saut conditionnel en exploitant les drapeaux :

Tableau 24.1

Instruction
Description
Code d’opération

JMP adr

Saut (jump)

C3h

JNZ adr

Saut si drapeau de zéro non armé

C2h

JZ adr

Saut si drapeau de zéro armé

CAh

JNC adr

Saut si drapeau de retenue non armé

D2h

JC adr

Saut si drapeau de retenue armé

DAh

JP adr

Saut si drapeau de signe non armé

F2h

JM adr

Saut si drapeau de signe armé (valeur négative)

FAh

Je n’invente pas ces instructions et ces codes ! Elles ont été exactement définies ainsi pour le microprocesseur Intel 8080. Rappelons qu’il me sert de guide pour la conception de mon propre processeur pédagogique. Dans la première colonne, la mention adr est l’adresse mémoire sur 2 octets qui suit le code d’opération.

La première instruction du tableau, JMP, est le saut inconditionnel : le saut est réalisé systématiquement, donc quel que soit l’état des drapeaux de l’ALU. Toutes les autres instructions sont des sauts conditionnels : elles ne modifient la valeur de l’adresse de la prochaine instruction à exécuter (dans le pointeur d’instruction) que si un drapeau parmi ceux disponibles est armé (à l’état 1) ou bien non armé. (Le processeur 8080 permet de plus des sauts conditionnels fondés sur un autre drapeau dont j’ai fait mention dans le Chapitre 21, mais que je ne vais pas exploiter dans mon processeur, le drapeau de parité.)

Découvrons le fonctionnement de ces sauts conditionnels dans un vrai programme. Nous supposons qu’il faut additionner 200 valeurs sur 1 octet stockées en mémoire à partir de l’adresse 1000h.

La technique à utiliser suppose de mémoriser dans un des registres une valeur initiale qui va servir de compteur. Ce compteur va commencer à 200 (le nombre d’octets à additionner), soit le nombre de tours de boucle à effectuer. Chaque fois qu’un octet est lu en mémoire et ajouté au sous-total courant, le compteur est décrémenté ( – 1). Le compteur indique donc à tout moment combien il reste d’octets à additionner. Le travail est terminé une fois que le compteur atteint 0.

Autrement dit, notre programme doit jongler avec deux opérations arithmétiques en même temps. Il doit d’une part réaliser le travail principal d’addition des octets, et d’autre part faire progresser le compteur en direction de 0 après chaque addition.

Cela nous pose un petit problème : vous savez que toutes les opérations arithmétiques et logiques se servent de l’accumulateur. Le programme doit donc transférer un octet depuis un registre vers l’accumulateur, réaliser une opération puis extraire le résultat et le renvoyer dans les registres, pour le sous-total et pour le compteur.

Décidons de stocker le sous-total courant dans le registre B et le compteur de boucles dans le registre C. Il va falloir copier tour à tour chacune des deux valeurs dans l’accumulateur pour lui appliquer une opération arithmétique, puis renvoyer vers B ou vers C le résultat du traitement, avant la prochaine répétition.

Ce programme commence à être un peu plus long que tous ceux vus jusqu’ici. Je décide donc de le présenter en trois parties.

La première partie, que l’on retrouve souvent dans les programmes, correspond à la phase d’initialisation, sorte de préparation ou de « mise en place » :

Figure 24.3

Les préparatifs consistent d’abord à implanter dans la paire de registres d’adressage HL la valeur sur 16 bits 1000h qui est l’adresse de la première cellule contenant la suite de nombres à additionner. Nous installons la valeur 200 décimal, soit C8h dans le registre, pour stipuler le nombre de valeurs à traiter. Il ne reste plus qu’à installer dans le registre B le contenu de la première cellule de valeur.

La deuxième partie du programme contient toutes les instructions de la boucle de répétition :

Figure 24.4

Dans cette partie, nous commençons par copier la valeur du compteur dans l’accumulateur et la décrémentons illico d’une unité avec une instruction de soustraction immédiate SUI. La valeur 200 est donc ramenée à 199 dès le premier tour. Nous arrivons ensuite à la fameuse instruction JZ. Lorsque la valeur du compteur sera égale à 0 (ce n’est vraiment pas encore le cas), l’instruction va forcer un saut à l’adresse 0015h qui est la première adresse après la fin du bloc. Ce genre d’instruction réalise ce que l’on appelle une sortie de boucle (break).

Le contenu de l’accumulateur vaut donc 199 lors du premier tour puis est sauvegardé dans le registre C. Nous incrémentons ensuite le contenu de la paire HL. Le sous-total qui se trouve dans le registre B est copié dans l’accumulateur pour être additionné à la valeur trouvée en mémoire à l’adresse indiquée par HL. Le nouveau sous-total est sauvegardé dans le registre B. Nous arrivons enfin à un saut inconditionnel JMP qui fait revenir le flux d’exécution en début de boucle à l’adresse 0007h.

Chaque exécution de cette série d’instructions est une itération ou un tour de boucle. Le contenu du registre C diminue jusqu’à 1. À ce moment, le fait de décrémenter fait tomber la valeur à 0, et l’instruction JZ qui suit fait sauter à l’adresse 0015h :

Figure 24.5

Le registre B contient à ce moment le total des 200 nombres. Cette valeur est copiée dans l’accumulateur en vue de l’opération de stockage mémoire STA puis le programme s’arrête.

Notez que ce programme peut aisément être réutilisé, par exemple en stockant les nombres additionnés ailleurs en mémoire ou en faisant additionner plus ou moins de nombres. Ces différentes informations sont centralisées en début de programme pour pouvoir être facilement modifiées. Lors de la conception d’un programme informatique, c’est une saine habitude que de réfléchir à la façon dont il pourra être modifié et enrichi plus tard.

On peut généralement écrire un programme de différentes manières. Il existe plusieurs façons de formuler celui de l’exemple en sorte de n’utiliser qu’une seule instruction de saut. Cette nouvelle version débute presque comme la précédente :

Figure 24.6

La seule différence est que nous stockons dès le départ la valeur 199 dans le registre C, donc avant le premier tour. Nous allons voir un peu plus loin pourquoi.

La partie centrale de la boucle de répétition a été réorganisée. Dorénavant, nous commençons par incrémenter HL puis procédons à l’addition :

Figure 24.7

La cinquième de ces instructions demande de copier dans A la valeur du compteur qui avait été placée dans le registre C, puis de décrémenter cette valeur avant de la restocker dans C. Nous atteignons alors l’instruction de branchement conditionnel JNZ qui reboucle en 0007h tant que la précédente instruction (la décrémentation SUI) n’a pas activé le drapeau Zéro.

Lorsque l’instruction SUI fait passer le compteur à 0, le programme ne reboucle plus et continue après l’instruction JNZ. Cela correspond à la clôture du programme, ce qui suppose de stocker la valeur finale en mémoire avant d’arrêter l’exécution :

Figure 24.8

L’élimination de l’une des deux instructions de saut allège le programme de 3 octets, au prix d’une plus grande complexité apparente. Comprenez-vous pourquoi nous stockons la valeur 199 dès le départ dans le registre C et non la valeur 200 ? C’est parce que cette valeur va être décrémentée après la première opération d’addition et non avant. Si la liste ne comprenait que deux nombres, ils auraient déjà été additionnés avant le premier saut par JNZ. Dans ce cas, la première valeur dans le registre C aurait dû être égale à 1 et non à 2. Mais ce programme ne fonctionnerait plus du tout dans le cas particulier (et limite, mais pas impossible) d’une liste ne contenant qu’un octet. Devinez-vous pourquoi ?

Il arrive souvent que l’on se trompe au moment de décider du nombre de tours de boucle de répétition. Ce genre d’erreur est tellement répandu en informatique que les Anglo-Saxons lui ont donné le nom d’erreur off-by-one, ou « faute de un ».

Une situation différente consiste à ne pas savoir combien de nombres il faut additionner ; on sait seulement que le dernier nombre de la liste doit avoir la valeur 00h. Cette valeur sert dans ce cas de marqueur de fin de liste. On parle alors de sentinelle. Pour exploiter cette possibilité, on peut utiliser une instruction de comparaison qui va confronter la valeur lue en mémoire avec la valeur 00h, et ainsi savoir quand il faut arrêter de boucler.

Cette nouvelle formulation de notre exemple utilisant une sentinelle est l’occasion de présenter le code source du programme de façon plus allégée. Je ne montre plus les valeurs en mémoire mais seulement les instructions. Pour symboliser les adresses mémoire, je vais utiliser des labels ou étiquettes. Ils ressemblent à des mots du langage courant et doivent toujours être suivis d’un signe deux-points1 :

Debut:   MVI L,00h
          MVI H,10h
          MVI B,00h
Boucle:  MOV A,M
          CPI 00h
          JZ Fin
          ADD B
          MOV B,A
          INX HL
          JMP Boucle
Fin:      MOV A,B
          STA Resultat
          HLT
Resultat:

Dans la boucle, une fois qu’une valeur a été envoyée dans l’accumulateur avec MOV A, M, nous faisons comparer cette valeur avec 00h par l’instruction CPI. Si l’accumulateur A contient la valeur nulle, le drapeau de zéro est armé et l’instruction JZ provoque un saut vers le label Fin : . Dans le cas contraire, l’addition est réalisée avec le sous-total dans B, puis l’adresse dans HL est incrémentée pour préparer le tour suivant.

Les labels nous épargnent de devoir gérer les adresses mémoire des instructions. Il reste néanmoins possible de connaître les adresses qui correspondent aux labels. Si notre programme commence, par exemple, à l’adresse mémoire 0000h, et puisque les trois premières instructions occupent 2 octets chacune, le label Boucle : correspond à l’adresse mémoire 0006h. Les sept instructions de la boucle occupent au total 12 octets. De ce fait, le label Fin : est situé à l’adresse mémoire 0012h et Resultat : à l’adresse 0017h.

Vous pressentez peut-être déjà que le saut conditionnel est une opération indispensable dans tout processeur, mais sachez que son importance est encore plus grande.

En 1936, un étudiant de 24 ans de l’université de Cambridge nommé Alan Turing avait décidé de résoudre un problème de logique mathématique qui avait été posé par le mathématicien allemand David Hilbert. Il s’agit du Entscheidungsproblem, soit le problème de la décision. Existe-t-il un processus qui permettrait de déterminer qu’une instruction arbitraire en logique mathématique est vraie ou fausse, c’est-à-dire si elle est décidable ?

© Getty Images

Alan Turing avait opté pour une approche tout à fait inusitée dans sa réponse à la question. Il avait supposé l’existence d’une machine à calculer simplifiée et fondée sur des règles élémentaires. Il n’avait pas construit cette machine, c’était une machine mentale. Mais cela lui permettait de prouver que le problème de la décision était faux, et au passage poser certains concepts fondamentaux de l’informatique numérique, concepts ayant eu un impact largement plus vaste que ce problème spécifique de logique mathématique.

La machine à calculer mentale inventée par Turing porte dorénavant le nom de machine de Turing. En termes de potentialités de traitement informatique, elle est équivalente à tous les ordinateurs numériques construits depuis. Pour en savoir plus, je vous invite à plonger dans mon autre livre, The Annotated Turing : A Guided Tour through Alan Turing’s Historic Paper on Computability and the Turing Machine (N.d.T. : pas encore traduit en français).

Les ordinateurs se distinguent les uns des autres par leur vitesse d’exécution, par la quantité de mémoire vive et d’espace de stockage et par les équipements périphériques. En revanche, ils sont tous équivalents en termes de capacités de traitement. Tous savent réaliser les mêmes types de tâches parce que tous disposent d’une fonction tout à fait remarquable : la capacité d’effectuer ou non un saut vers une autre partie du programme en fonction du résultat d’une opération arithmétique ou logique.

De ce fait, tous les langages de programmation qui disposent d’une fonction de saut conditionnel sont fondamentalement similaires. On les considère comme des langages complets dans le sens « turingien ». C’est le cas de quasiment tous les langages de programmation actuels. En revanche, les langages de balisage tels que le HTML (HyperText Markup Language) que l’on utilise pour créer des pages Web ne sont pas des langages Turing complets.

En complément aux instructions de saut précédentes, une autre instruction est très utile pour réaliser des sauts. Elle fonctionne sur la base de la valeur trouvée dans la paire de registres HL :

Tableau 24.2

Instruction
Description
Code d’opération

PCHL

Copie le contenu de HL dans le pointeur d’instruction PC.

E9h

Les sept instructions de saut ainsi que PCHL sont assez facilement ajoutés à la circuiterie temporelle vue dans le chapitre précédent. Dans la Figure 23.9, nous avions trois décodeurs x-vers-y dont les entrées se répartissaient les 8 bits du code d’opération :

Figure 24.9

Différentes combinaisons des sorties de ces trois décodeurs permettent de générer les signaux des instructions de saut correspondantes.

Toutes les instructions de saut, sauf PCHL, peuvent être regroupées en entrées d’une porte OU à sept entrées :

Figure 24.10

Le signal de sortie de cette porte peut être ajouté au circuit qui évalue le nombre d’octets de chargement et d’exécution en Figure 23.11. Le signal de groupe des sauts fait savoir qu’il faut charger 3 octets en mémoire (le code d’opération et une adresse sur 2 octets). L’instruction PCHL n’occupe en revanche que 1 octet. Toutes ces instructions de saut ne réclament qu’un seul cycle d’exécution, et ne manipulent que le bus d’adresse.

En ce qui concerne l’exécution des instructions de saut, nous allons définir un signal pour indiquer que le saut à réaliser est conditionnel. Il y a lieu de combiner les signaux de l’octet d’instruction décodé avec les drapeaux de l’ALU (ce que nous avons vu dans le Chapitre 21).

Figure 24.11

Il ne reste plus qu’à ajouter ces signaux dans la matrice de diodes ROM de décodage vue en Figure 23.20 :

Figure 24.12

L’instruction JMP et tous les sauts conditionnels autorisent les verrous d’instruction 2 et 3 sur le bus d’adresse alors que l’instruction PCHL autorise le contenu de HL sur ce même bus. Dans tous les cas, l’adresse est ensuite stockée dans le pointeur d’instruction PC.

Je propose une version interactive de ce processeur amélioré dans le site Web compagnon.

Quasiment tous les traitements que vous voudrez faire faire à un ordinateur auront recours à une répétition, et donc à la mise en place d’une boucle. C’est par exemple le cas de la multiplication. Je vous avais promis dans le Chapitre 21 de vous montrer comment faire en sorte que ce processeur minimal sache multiplier. Voici venu ce moment.

Prenons le cas le plus simple qui consiste à multiplier 2 octets, par exemple les valeurs décimales 132 et 209, soit en hexadécimal 84h et D1h. Les deux valeurs vont être appelées respectivement multiplicateur et multiplicande. Le résultat sera le produit.

Lorsque l’on multiplie une valeur sur 1 octet par une autre, le résultat réclame fréquemment 2 octets pour son stockage. Dans mon exemple, on calcule facilement que le résultat sera égal à 27 588d, soit 6BC4h. Laissons le processeur nous le confirmer.

Jusqu’ici, je me suis servi des registres H et L sous la forme d’une paire pour stocker une adresse sur 16 bits. Sachez que vous pouvez utiliser chacun des deux registres H et L indépendamment, en tant que registre 8 bits régulier, ou utiliser la paire de registres pour stocker une valeur sur 2 octets au lieu d’une adresse. Ici, je vais me servir de H et L pour stocker le produit de la modification. L’exemple commence par stocker le multiplicateur dans le registre C et le multiplicande dans le registre B. Les registres H et L sont forcés à zéro :

Debut:  MVI B,D1h ; Stocke le multiplicande dans B
         MVI C,84h ; Stocke le multiplicateur dans C
         MVI H,00h ; Initialise HL à zéro
         MVI L,00h

Vous remarquez que j’ai ajouté de petits descriptifs en marge droite juste après un signe point-virgule. Il s’agit de commentaires. Le signe point-virgule est reconnu dans la syntaxe Intel comme indiqué dans la documentation du processeur 8080.

La façon la plus simple de multiplier un nombre consiste à répéter une addition. Je vais additionner le multiplicateur au contenu des registres H et L un nombre de fois égal à la valeur du multiplicande.

Nous devons d’abord vérifier que le multiplicande (stocké dans B) n’est pas égal à zéro. S’il est nul, la multiplication est déjà terminée :

Boucle:  MOV A,B  ; Test si B est nul
          CPI 00h
          JZ Fin   ; Fini si nul

Si la valeur n’est pas nulle, le multiplicateur qui se trouve dans C est ajouté au contenu des registres H et L. Notez qu’il s’agit d’une addition sur 16 bits : nous ajoutons d’abord C à L en déplaçant le contenu de L vers l’accumulateur, puis nous ajoutons 00h à H avec une éventuelle retenue issue de la première addition :

MOV A,L  ; Ajout de C à HL
ADD C
MOV L,A
MOV A,H
ACI 00h
MOV H,A

Nous décrémentons ( – 1) la valeur contenue dans le registre B, ce qui confirme que nous aurons une addition de moins à faire avec HL. Le programme peut alors revenir au début, à Boucle : pour faire un nouveau tour :

MOV A,B  ; Décrementer B
SBI 01h
MOV B,A
JMP Loop ; Répéter le calcul

Quand se produit le saut conditionnel vers Fin : , c’est que la multiplication est terminée. Les deux registres H et L contiennent le produit :

Fin:      HLT      ; HL contient le résultat

Ce n’est pas la meilleure technique pour multiplier 2 octets, mais elle est facile à comprendre. On parle en général de solution par force brute. Dans cette version, nous ne cherchons pas à travailler le plus vite possible. Le code ne tente même pas de comparer d’abord les deux nombres à multiplier en sorte d’utiliser le plus petit des deux comme multiplicateur de boucle. Il suffirait d’ajouter quelques instructions pour que le programme effectue 132 additions de la valeur 209 plutôt que 209 additions de la valeur 132.

Y a-t-il une meilleure solution pour effectuer nos multiplications ? Voyons comment nous procédons manuellement pour une multiplication décimale :

Les deux valeurs des calculs intermédiaires correspondent à 132 fois 9, puis 132 fois 2 décalé de deux positions à gauche (soit, en fait, 132 fois 200). Nous ne perdons même pas de temps à écrire deux fois 132 fois 0, puisque le résultat est nul. Au lieu d’effectuer 209 additions ou même 132 seulement, nous n’avons que deux nombres à additionner !

À quoi ressemble la même multiplication en binaire ? Le multiplicateur (132 en décimal) s’écrit 1000 0100 en binaire. Le multiplicande qui vaut 109d s’écrit donc 1101 0001 :

Pour chaque bit du multiplicande (1101 0001) en partant de la droite, nous multiplions le bit par chacun de ceux du multiplicateur (1000 0100). Si le bit est à 1, le résultat est la même valeur que le multiplicateur, avec un décalage vers la gauche pour chaque bit. Si le bit est à 0, le résultat est à 0 et peut donc être ignoré.

Sous le premier trait horizontal, nous ne trouvons que quatre instances du multiplicateur au lieu des huit théoriques, tout simplement parce que le multiplicande ne possède que quatre bits à 1 (1101 0001).

Cette technique limite le nombre d’additions à réaliser. Cette façon optimisée de multiplier devient encore plus précieuse lorsqu’il s’agit de multiplier des valeurs sur 16 ou 32 bits.

Mais l’opération augmente la complexité. Il faut dorénavant tester quels bits du multiplicande sont à 1 ou à 0.

Ces tests de bits se servent de l’instruction logique ANA du 8080 (AND avec Accumulateur). Cette instruction réalise une opération logique entre bits de 2 octets. Le résultat est à 1 seulement si les deux bits de même rang des 2 octets sont à 1. Le résultat est à 0 dans les autres cas.

Stockons le multiplicande dans le registre D. Dans l’exemple, la valeur est D1h :

MVI D,D1h

Comment savoir si le bit de poids faible contenu dans le registre D est à 1 ? Il suffit de lancer une opération ANA entre le contenu et la valeur 01h. Stockons d’abord cette valeur de comparaison dans le registre E :

MVI E,01h

Nous savons que l’unité ALU ne fonctionne qu’avec l’accumulateur. Il faut donc d’abord transférer l’un des deux nombres dans cet accumulateur :

 MOV A,D
 ANA E

Le résultat de ce ET logique n’est à 1 que si le bit de poids faible (le plus à droite) dans D est à 1. Autrement dit, le drapeau de zéro est armé si ce même bit est à 0. Le drapeau permet donc de réaliser un saut conditionnel.

Pour le bit suivant, l’opération logique doit être réalisée avec la valeur 02h. Pour les bits suivants, l’opération doit comparer avec 04h, puis 08h, 10h, 20h, 40h et 80h. Si vous lisez cette suite de valeurs, vous devinez que chacune d’elles est le double de la précédente : en décimal, 1, 2, 4, 8, 16, 32, 64 et 128. Cette information n’est pas inutile.

Au départ, le registre E contient 01h. Pour doubler sa valeur, il suffit de l’additionner à elle-même :

MOV A,E
ADD E
MOV E,A

E contient ensuite 02h. Après exécution des trois instructions, le contenu passe à 4h, puis à 8h, etc. Ce que fait cette opération revient à décaler progressivement la valeur d’un bit de sa position la plus à droite vers la position la plus à gauche, soit de 01h jusqu’à 80h en valeur.

Pour faire l’addition et obtenir le résultat, il faut également décaler le multiplicateur. En conséquence, sa valeur ne va plus tenir dans les limites d’un registre sur 8 bits. Il faut donc le traiter comme une valeur sur 16 bits et le multiplicateur est stocké dans le registre C, avec le registre B forcé à zéro. Nous pouvons ensuite manipuler les deux registres B et C comme s’ils formaient une paire mémorisant un multiplicateur sur 16 bits. Il pourra ensuite faire l’objet d’un décalage pour les additions sur 16 bits. Nous pouvons désigner la paire de registres B et C sous le terme BC.

Voici la procédure d’initialisation des registres pour ce multiplicateur amélioré :

Debut :  MVI D,D1h ; Multiplicande
          MVI C,84h ; Multiplicateur dans BC
          MVI B,00h
          MVI E,01h  ; Test de bit
          MVI H,00h ; Résultat dans HL sur 2 octets
          MVI L,00h

Dans la boucle, nous commençons par tester l’état d’un bit du multiplicande :

Boucle:  MOV A,D
          ANA E     ; Teste si bit à 0 ou à 1
          JZ Sauter

Si le bit est à 1, le résultat est différent de 0, auquel cas le code qui suit est exécuté pour additionner la valeur dans la paire de registres BC à la valeur dans la paire HL. Il faut cependant gérer individuellement les registres 8 bits. Une instruction ADD est utilisée pour les octets de poids faible, tandis qu’une instruction ADC est requise pour ceux de poids fort afin de tenir compte d’une retenue éventuelle :

MOV A,L   ; Ajoute BC à HL
ADD C
MOV L,A
MOV A,H
ADC B
MOV H,A

Dans un vrai processeur Intel 8080, on peut remplacer le bloc de six instructions par la seule instruction DAD BC qui, en une ligne, réalise l’addition du contenu de BC à celui de HL. DAD est l’une des instructions 8080 capables de travailler avec des valeurs sur 16 bits.

Le traitement suivant consiste à doubler la valeur de BC, ce qui revient à réaliser un décalage vers la gauche pour l’addition suivante. Le code qui suit sera exécuté, que BC ait été ajouté à HL ou pas :

Sauter:   MOV A,C    ; Doublement du multiplicateur BC
          ADD C
          MOV C,A
          MOV A,B
          ADC B
          MOV B,A

Il fait ensuite doubler la valeur du registre E qui sert de testeur de bits. Si la valeur est différente de 0, on saute en arrière jusqu’au label Boucle : pour faire un nouveau tour.

MOV A,E   ; Doublement du testeur de bits dans E
ADD E
MOV E,A
JNZ Boucle

Le code qui suit le label Boucle : va être exécuté exactement huit fois. Lorsque le contenu de E a doublé huit fois, le registre 8 bits déborde et E vaut alors 0, ce qui provoque la fin de la multiplication :

Fin:      HLT        ; HL contient le résultat

Le traitement devient clairement plus complexe si le but est de multiplier deux valeurs sur 16 ou sur 32 bits, puisqu’il faudra utiliser un plus grand nombre de registres. Sachez cependant qu’une fois que vous avez épuisé tous les registres disponibles pour stocker les valeurs intermédiaires, vous pouvez commencer à utiliser une portion de l’espace mémoire pour vos stockages temporaires. Une telle zone mémoire joue alors le rôle d’ardoise ou de pense-bête (scratch pad).

L’exercice que nous réalisons dans ce chapitre n’a pas pour but de vous faire peur, ou de vous décourager de vous lancer dans une carrière de programmeur informatique. L’objectif est de montrer comment un assemblage astucieux de portes logiques qui répondent à des codes numériques stockés en mémoire permet d’enchaîner des opérations élémentaires pour réaliser des tâches complexes.

Les multiplications sont devenues extrêmement faciles en réalité de nos jours parce que les programmeurs utilisent des langages de haut niveau (c’est leur nom). Nous y reviendrons dans le Chapitre 27. Le monde du logiciel a ceci de merveilleux que chacun bénéficie énormément de l’apport de ses prédécesseurs et collègues pour alléger son travail.

Les multiplications par une machine binaire supposent d’effectuer des décalages de bits, ce que nous venons de pratiquer pour notre addition. Si vous deviez programmer un vrai microprocesseur Intel 8080 et non le modèle pédagogique de ce livre, vous disposeriez de plus de moyens pour programmer ces décalages. En effet, le 8080 définit quatre instructions pour décaler des bits sans devoir gérer les détails d’addition de registres à eux-mêmes. Ce sont les instructions dites de rotation :

Tableau 24.3

Instruction
Description
Code d’opération

RLC

Rotation gauche

07h

RRC

Rotation droite

0Fh

RAL

Rotation gauche avec retenue

17h

RAR

Rotation droite avec retenue

1Fh

Toutes ces instructions appliquent leur traitement à la valeur qui se trouve dans l’accumulateur et ont un effet sur le drapeau de retenue Carry.

La première, RLC, effectue un décalage des bits de l’accumulateur vers la gauche (L pour Left). Le bit de poids fort avant décalage est envoyé dans le drapeau de retenue CY tout en étant réinjecté dans le bit de poids faible :

L’instruction RLC propose le même traitement, mais vers la droite (R pour Right). Le bit de poids faible est injecté dans le drapeau de retenue CY et aussi dans le bit de poids fort :

L’instruction RAL effectue un décalage vers la gauche, donc une multiplication par 2 de la valeur mais, de plus, le drapeau de retenue existant est injecté comme bit de poids faible. Cette technique est utilisée pour les décalages de valeurs multi-octets :

Enfin, l’instruction RAR ressemble à RAL mais en sens inverse :

Ces instructions de rotation sont bien pratiques dans des cas particuliers, mais pas indispensables au point de m’obliger à les ajouter à mon processeur en cours de conception.

Nous savons maintenant exécuter une série d’instructions plusieurs fois grâce aux instructions de saut et aux boucles. Souvent, vous aurez envie de définir une entité à partir d’un groupe d’instructions. Supposez que vous ayez créé une série d’instructions réalisant un traitement que vous voulez pouvoir déclencher depuis plusieurs endroits différents de votre programme. Ce sera par exemple le cas d’une multiplication polyvalente. Ce regroupement d’instructions en tant qu’entité porte un nom : fonction, procédure ou sous-programme. Certains parlent également de routine.

Le processeur 8080 permet d’exploiter ce concept de sous-programme au moyen de l’instruction nommée CALL. Cette instruction d’appel ressemble à l’instruction de saut JMP, dans la mesure où son code doit être suivi d’une adresse mémoire :

CALL adr

Comme JMP, l’instruction CALL provoque un saut de l’exécution à l’adresse fournie. L’énorme différence avec JMP est que l’adresse en vigueur avant le saut est mémorisée. Nous allons voir dans peu de temps que cette adresse est stockée dans un endroit tout à fait spécial.

L’instruction CALL est complétée d’une instruction qui porte le nom RET pour retour. Elle ressemble aussi à JMP, sauf qu’elle provoque le branchement ou le saut vers l’adresse qui avait été mémorisée au début de l’instruction CALL, ce qui permet de reprendre le flux d’exécution normal. Un sous-programme se termine généralement par une instruction de retour RET.

Voici les instructions machine 8080 pour CALL et RET :

Tableau 24.4

Instruction
Description
Code d’opération

CALL

Saute vers sous-programme

CDh

RET

Revient de sous-programme

C9h

Le processeur Intel 8080 définit également des appels et des retours conditionnels, mais ils sont utilisés bien moins fréquemment que CALL.

Prenons un exemple concret. Vous devez écrire un programme qui va afficher la valeur d’un octet, par exemple 5Bh. Nous avons vu dans le Chapitre 13 que l’on pouvait adopter le codage ASCII pour les lettres, les chiffres et les symboles. En revanche, une traduction est requise : vous ne pouvez pas afficher directement la valeur 5Bh en la faisant passer pour un code ASCII. Cette valeur correspond à la représentation visuelle du crochet droit de gauche ([, soit 91d). Pour afficher la valeur hexadécimale 5B de façon lisible, il faut convertir la valeur en deux codes ASCII successifs :

35h qui est le code ASCII du chiffre 5 ;

42h qui est le code ASCII du caractère B majuscule.

Après cette conversion, la valeur devient lisible par des humains, tout du moins ceux qui savent lire l’hexadécimal.

Pour répondre à ce besoin, il nous faut d’abord séparer l’octet en deux : les quatre du haut et les quatre du bas, que l’on appelle aussi des quartets (nibbles en anglais). Dans notre exemple, la valeur 5Bh donne les deux valeurs 05h et 0Bh.

On peut ensuite convertir chaque valeur sur 4 bits en un code ASCII. Les valeurs de 0h à 9h correspondent aux code ASCII des chiffres de 30h à 39h qui sont ceux des dessins des chiffres de 0 à 9. (Revoyez si nécessaire les tableaux des codes ASCII dans le Chapitre 13.) Pour les valeurs entre Ah et Fh, les codes des lettres de l’alphabet correspondantes valent de 41h à 46h. Construisons un petit sous-programme qui sait convertir une valeur sur 4 bits se trouvant dans l’accumulateur en un code ASCII :

Figure 24.13

J’ai choisi dans cette figure de faire à nouveau apparaître les adresses mémoire, comme lors des premiers exemples, afin de mieux montrer certains détails. La première instruction de cette routine est à l’adresse 1532h, sans que cette valeur ait une signification particulière. C’est simplement l’endroit où la série d’instructions de la routine a été implantée en mémoire. Dans la routine, il est supposé que l’accumulateur contient la valeur à convertir. Cette valeur prépositionnée est généralement désignée en tant qu’argument ou paramètre d’entrée2.

La routine commence par une instruction de comparaison immédiate CPI qui positionne les drapeaux de l’ALU (unité arithmétique et logique), comme si une soustraction avait été réalisée. Si l’accumulateur contient la valeur 05h (par exemple), le fait de soustraire 0Ah à cette valeur génère une retenue négative, ce qui arme le drapeau de retenue Carry. Puisque le drapeau est à 1, l’instruction JC provoque le saut vers l’instruction correspondant au label Chiffre : , ce qui ajoute la valeur 30h au contenu de l’accumulateur. Nous obtenons donc 35h, qui est bien le code ASCII du chiffre 5.

En revanche, si l’accumulateur contient, par exemple, la valeur 0Bh, la soustraction de 0Ah ne génère pas de retenue négative. Le drapeau de retenue n’est pas forcé à 1 et aucun saut ne se produit. La valeur 07h est alors ajoutée à l’accumulateur (0Bh plus 07h donne 12h dans l’exemple). La seconde instruction ADI ajoute alors 30h, ce qui donne 42h, le code ASCII du B majuscule. Vous constatez qu’en faisant une double addition, nous avons créé une petite astuce qui permet d’utiliser la seconde instruction d’addition ADI aussi bien pour une lettre que pour un chiffre.

Dans les deux cas, nous aboutissons à une instruction de fin de routine RET.

Revenons maintenant à la routine qui doit convertir la totalité d’un octet en deux codes ASCII. Cette autre routine va comporter deux appels CALL vers le label Chiffre : , une fois avec le quartet (4 bits) faible et l’autre avec le quartet fort. En début de routine, l’octet à convertir doit être dans l’accumulateur. Les résultats seront placés dans les deux registres H et L. Nous appelons cette routine VerAscii ; elle est positionnée en mémoire à partir de l’adresse 14F8h :

Figure 24.14

Dans cette routine, nous commençons par stocker l’octet initial dans B. Puis nous faisons réaliser un ET entre bits par l’instruction ANI en utilisant la valeur 0Fh afin de ne retenir que les 4 bits de poids faible. Nous arrivons ensuite à un appel CALL vers le sous-programme Chiffre situé à partir de l’adresse 1532h. Le résultat de cette routine est stocké dans L. L’octet initial est alors récupéré depuis le registre B, puis une rafale de quatre instructions de rotation RRC provoque un décalage des 4 bits de poids fort de quatre positions pour en faire des bits de poids faible. Une autre instruction ANI est suivie d’un nouvel appel à Chiffre et le résultat est stocké dans le registre H. La routine peut alors se terminer par RET.

Voyons comment cela fonctionne. Nous supposons qu’en un autre point de l’espace mémoire se trouve un bloc de code avec une instruction d’appel CALL pointant vers la routine VerAscii qui est implantée à partir de 14F8h :

Figure 24.15

Dans cette figure, la cellule vide à l’adresse 0628h contiendrait la suite du programme qui ne nous intéresse pas ici. Ce qui est essentiel, c’est que les deux registres H et L contiennent les codes ASCII des deux chiffres 5 et B.

Mais comment CALL et RET fonctionnent-ils vraiment ?

Comme nous l’avons vu précédemment, l’instruction CALL provoque le stockage d’une adresse dans un lieu très spécial qui permet de la retrouver plus tard afin de reprendre le flux normal d’exécution. Ce lieu spécial se nomme la pile (stack). C’est un endroit réservé dans l’espace mémoire, situé le plus loin possible de la zone en cours d’utilisation. Dans un processeur 8 bits tel que l’Intel 8080, la pile est implantée tout au bout de l’espace mémoire.

Le 8080 possède un registre 16 bits spécial qui est le pointeur de pile SP (pour Stack Pointer). Lors de la réinitialisation d’un 8080, le contenu de ce registre est forcé à l’adresse de départ 0000h3.

En théorie, on peut modifier la valeur stockée dans le pointeur de pile au moyen de l’instruction SPHL (qui écrit dans SP la valeur trouvée dans HL) ou LXI SP (qui stocke dans SP une adresse fournie immédiatement après). Continuons à raisonner sans modifier la valeur initiale 0000h.

Lorsque le 8080 doit exécuter une instruction telle que CALL VerAscii, voici la séquence d’opérations réalisée :

Le contenu du pointeur de pile est décrémenté. Comme il valait au départ 0000h, il prend la valeur FFFFh, qui est la valeur maximale sur 16 bits et correspond à l’endroit le plus éloigné de l’origine dans une mémoire sur 16 bits.

L’octet de poids fort de l’adresse indiquée après CALL est stocké en mémoire à l’adresse spécifiée par le pointeur de pile. Cet octet vaut bien sûr 06h. (Dans l’exemple, l’adresse de CALL vaut 0628h et est à ce moment la valeur que contient le pointeur d’instruction, pas le pointeur de pile.)

L’adresse dans le pointeur de pile est décrémentée et devient alors FFFEh.

L’octet de poids faible de l’adresse qui suit CALL est à son tour sauvegardé à l’adresse indiquée par le pointeur de pile. Cet octet vaut bien sûr 28h.

L’adresse de saut de l’instruction CALL (14F8h) est alors chargée dans le pointeur d’instruction PC, ce qui provoque par saut l’exécution de l’instruction située à cette adresse. C’est l’adresse de début de la routine VerAscii.

Voici à quoi ressemble, après cette opération, le haut de l’espace mémoire RAM :

Figure 24.16

On voit donc que l’instruction CALL a, comme le Petit Poucet, semé de petites miettes sur son chemin pour pouvoir reprendre le cours normal des choses en fin d’exécution de la routine.

Au cours de l’exécution de la routine VerAscii, nous trouvons un autre appel CALL, cette fois-ci vers la routine Chiffre. Dans le corps VerAscii, l’instruction qui suit celle du saut est à l’adresse 14FEh. Lors de l’appel à la sous-routine, cette adresse est donc préservée sur la pile qui prend l’aspect suivant :

Figure 24.17

Le pointeur de pile contient à ce moment la valeur FFFCh, et nous sommes en pleine exécution de la sous-routine Chiffre. Voici ce qui se produit au moment du retour par RET de cette sous-routine :

L’octet désigné par le pointeur de pile SP est lu. Sa valeur est FEh.

Le pointeur de pile est incrémenté.

L’octet suivant désigné par le pointeur de pile est lu. Sa valeur est 14h.

Le pointeur de pile est incrémenté.

Les 2 octets qui viennent d’être lus sont chargés dans le pointeur d’instruction, ce qui provoque le saut vers l’adresse 14FEh qui se situe dans la routine VerAscii, la routine appelante (celle qui avait appelé Chiffre).

La pile est alors dans l’état dans lequel elle se trouvait avant l’appel à Chiffre (pour être exact, le premier des deux appels à Chiffre de ce programme) :

Figure 24.18

Le pointeur de pile contient alors la valeur FFFEh. La valeur 14FEh qui vient d’être utilisée est toujours présente en mémoire, mais elle n’a plus d’importance. Lors du second appel à Chiffre, ce sera une autre adresse de retour qui sera stockée sur la pile :

Figure 24.19

En effet, il nous faut préserver l’adresse qui suit le second appel à Chiffre dans la routine VerAscii. Lors de l’instruction RET de ce nouvel appel, nous sautons à l’adresse 1509h dans la routine VerAscii. Voici l’aspect que prend alors la pile :

Figure 24.20

Nous pouvons alors parvenir à l’instruction de retour RET de la routine VerAscii elle-même. Nous récupérons l’adresse 0628h sur la pile et sautons à cette adresse qui est celle qui suivait l’instruction d’appel à VerAscii.

Vous venez de voir comment fonctionne une pile d’appels.

La pile d’un logiciel est considérée comme une technique de stockage dite LIFO (Last-In First-Out, en français DEPS, Dernier Entré-Premier Sorti). La valeur la plus récemment empilée sur la pile est la première valeur dépilée. C’est pourquoi on représente souvent une pile comme une pile d’assiettes dans une cantine.

L’action consistant à poser une valeur en haut de la pile correspond au verbe empiler (push). L’action complémentaire consistant à enlever la valeur du haut de la pile correspond à dépiler (pop). Le 8080 définit une famille d’instructions PUSH et POP permettant de sauvegarder le contenu de certains registres sur la pile pour le récupérer plus tard :

Tableau 24.5

Instruction
Description
Code d’opération

PUSH BC

Sauve les registres B et C sur la pile

C5h

PUSH DE

Sauve les registres D et E sur la pile

D5h

PUSH HL

Sauve les registres H et L sur la pile

E5h

PUSH PSW

Sauve le PSW sur la pile

F5h

POP BC

Récupère les registres B et C de la pile

C1h

POP DE

Récupère les registres D et E de la pile

D1h

POP HL

Récupère les registres H et L de la pile

E1h

POP PSW

Récupère le PSW de la pile

F1h

L’abréviation PSW qui signifie Program Status Word correspond à ce qu’Intel appelle le mot de statut du programme. C’est tout simplement la combinaison du contenu de l’accumulateur sur un octet et des drapeaux de l’ALU sur un autre octet.

Les instructions d’empilement et de dépilement PUSH et POP sont très pratiques pour sauvegarder le contenu des registres lors des appels de sous-programmes. Dans la partie d’un programme qui va appeler une routine, on prévoit souvent de préserver le contenu des registres avant CALL et de récupérer les valeurs après RET. La routine peut ainsi se servir des registres sans craindre de détruire les données qui étaient utilisées par le code appelant la routine. La routine peut elle-même empiler des valeurs de registres en début d’exécution et les dépiler juste avant sa fin.

Le nombre d’instructions PUSH et POP doit toujours être équilibré, tout comme les couples CALL et RET. Si un sous-programme appelle deux fois PUSH mais une seule fois POP, et si vous lancez ensuite une instruction RET, l’exécution va arriver à un endroit que vous n’avez certainement pas choisi !

On appelle « code errant » un programme qui dépile au moins une fois de trop, ce qui a pour effet que le pointeur de pile revient (par rebouclage de la valeur) à l’adresse qui correspond au début de l’espace mémoire et plus du tout à l’endroit le plus inaccessible tout en haut ! Cela correspond à l’erreur de débordement inverse de la pile, Stack Underflow. Le résultat est que le contenu de la pile va écraser des instructions du programme. Il existe aussi le problème inverse : si le nombre d’opérations d’empilement sur la pile d’imbrication d’appels est trop important, cela finit par écraser des instructions du programme. Il s’agit dans ce cas de débordement de la pile, Stack Overflow. Notez que cette erreur est tellement fréquente et redoutée qu’elle a donné son nom à l’un des sites Web les plus fréquentés par les programmeurs qui cherchent des réponses à leurs problèmes techniques : stackoverflow.com.

En théorie, les instructions CALL et RET ne sont pas indispensables pour qu’un processeur soit considéré comme Turing complet. En pratique, elles sont très appréciables, et certains les considèrent comme incontournables. En effet, le concept de sous-programme est l’outil principal pour organiser un programme en langage assembleur, et ce concept continue à jouer un rôle essentiel dans de nombreux autres langages de programmation.

J’aurais beaucoup aimé doter notre processeur pédagogique de ces instructions CALL, RET, PUSH et POP, mais cela supposerait de revoir des éléments essentiels de la conception, et les chapitres précédents auraient été moins compréhensibles.

Je suis persuadé que vous avez une bonne idée de la manière dont on pourrait ajouter ces instructions : il nous faudrait un nouveau verrou 16 bits portant le nom de pointeur de pile et connecté au bus d’adresse. Il ressemblerait beaucoup à celui utilisé pour le pointeur d’instruction PC. C’est la partie facile. Il faudrait également savoir comment empiler le pointeur d’instruction sur la pile pendant les instructions CALL et le dépiler pour les instructions RET, ce qui supposerait de rendre disponibles les 2 octets du PC sur le bus de données, ce qui n’est pas le cas actuellement.

Je n’ai pas ajouté les instructions de gestion de la pile à mon processeur, mais je propose néanmoins un émulateur 8080 complet sur le site Web compagnon.

Les derniers chapitres que nous venons de parcourir nous ont permis de voir comment un processeur 8 bits de type Intel 8080 exécutait les codes des instructions. Le 8080 avait été lancé en 1974 par Intel. De nos jours, il est considéré comme vraiment primitif en comparaison de ses successeurs. Les processeurs ont effectivement grandi pour passer à 16, 32 et même 64 bits, et ont parallèlement augmenté en complexité.

Pourtant, tous les processeurs continuent à fonctionner selon le même principe : ils exécutent des instructions qui servent à lire des octets en mémoire, à leur appliquer des traitements arithmétiques et logiques, puis à stocker les résultats à nouveau en mémoire.

Nous pouvons maintenant nous intéresser aux autres composants qui constituent un ordinateur réel.

1 N.d.T. : nous avons décidé de franciser les noms des labels pour montrer que le programmeur est libre du choix de ses identifiants et pour prévenir qu’il faut bannir toute lettre accentuée ou caractère spécifique au français tel que le ç dans les labels. Attention : jamais d’espace avant le signe deux-points !

2 N.d.T. : un débat récurrent en langue française oppose les termes argument et paramètre. Certains considèrent que l’argument est la valeur vue de l’extérieur de la routine et le paramètre, celle exploitée de l’intérieur. D’autres ne parlent que de paramètres, ce que nous ferons ici. Il existe des paramètres d’entrée et d’autres de sortie

3 N.d.T. : ne prenez pas peur, l’adresse dans SP va être décrémentée avant sa première utilisation pour ne pas écraser le programme placé à partir de cette adresse.

Chapitre 25  Périphériques

L’unité centrale de traitement CPU est évidemment le composant majeur de tout ordinateur, mais elle doit être entourée de sa cour de matériels. Nous savons déjà qu’un ordinateur doit accéder à une certaine quantité de mémoire vive RAM dans laquelle sont stockées les instructions en code machine à exécuter et les données que ces instructions doivent manipuler. Nous savons par ailleurs que le contenu des composants de mémoire vive est volatile : il disparaît lorsque l’alimentation électrique est interrompue. Un ordinateur doit donc disposer d’un autre équipement permettant de stocker aussi bien le code que les données de façon quasi pérenne en cas d’arrêt de l’alimentation électrique.

L’ordinateur doit également disposer d’un mécanisme pour échanger les informations entre les équipements et la mémoire vive et pour donner à voir les résultats du programme. De nos jours, les ordinateurs sont quasiment tous dotés d’un microphone, d’une caméra vidéo et d’une paire de haut-parleurs, sans compter une carte réseau filaire et sans fil, un adaptateur Bluetooth et éventuellement un récepteur GPS.

Il s’agit dans tous les cas d’équipements d’entrée ou de sortie. Ils sont regroupés sous l’abréviation I/O qui signifie Input/Output. Ce sont des éléments périphériques.

Le périphérique le plus visible est par définition l’écran d’affichage vidéo. C’est vers lui que vous regardez, quel que soit le type d’ordinateur concerné (ordinateur de bureau, portable, tablette, telliphone). Certains lisent d’ailleurs ce livre dans sa version numérique, donc sur un écran !

De nos jours, tous les affichages vidéo sont alimentées en données par des images numériques organisées en lignes et colonnes de pixels, chaque pixel étant un point élémentaire d’image visible ou invisible (pixel signifie picture element). Si vous regardez votre écran de très près ou avec une loupe, vous distinguerez des points colorés. Le nombre de lignes et de colonnes de pixels correspond à la résolution de l’affichage.

La résolution standard HD-TV des téléviseurs actuels correspond à 1 920 × 1 080, soit 1 920 pixels dans le sens horizontal et 1 080 pixels dans le sens vertical. Au total, cela représente environ 2 millions de pixels, chacun pouvant être affiché dans une couleur spécifique. De nos jours, le HD est la résolution minimale en informatique.

Les pixels ne sont pas tous allumés en même temps, mais par balayage. Le contenu de chaque image à afficher est stocké dans une zone réservée de la mémoire, puis chacun des pixels est envoyé vers l’écran, en commençant en haut à gauche pour lire toute la première ligne et en descendant de colonne en colonne. Pour éviter un effet de clignotement gênant, le processus est réalisé très vite. La totalité de l’écran est en général balayée au moins 60 fois par seconde. Les circuits électroniques qui gèrent cet affichage dynamique constituent la carte adaptateur vidéo.

Quel espace mémoire faut-il prévoir pour stocker le contenu d’une image au format 1 920 × 1 080 ?

Chacun des 2 millions de pixels est affiché dans une couleur qui est une combinaison des trois couleurs primaires rouge, vert et bleu, c’est-à-dire une couleur RGB. (Si vous avez fait de la peinture, vous connaissez sans doute les trois couleurs primaires cyan, magenta et jaune, mais il s’agit de couleurs en synthèse soustractive alors qu’ici nous sommes en synthèse additive.) Toutes les nuances de couleurs affichables peuvent être choisies en faisant varier l’intensité de chacune des trois couleurs primaires (les composantes). L’intensité de chaque couleur est normalement contrôlée par 1 octet ; la valeur 00h stipule une absence totale et la valeur FFh, l’intensité maximale. Dans un tel système, on peut combiner 256 nuances différentes de rouge, de vert et de bleu, ce qui donne au total plus de 16 millions de teintes. (Si on considère qu’en informatique, on peut toujours améliorer les choses, vous ne serez pas étonné de découvrir que certaines entreprises cherchent à augmenter le nombre de nuances de couleurs et la résolution. Bien sûr, cela obligera à utiliser plus de 8 bits par composante.)

Si vous avez déjà eu un contact avec le langage HTML qui sert à concevoir les pages Web, vous avez peut-être appris qu’en HTML, on spécifie une couleur avec une valeur hexadécimale sur six chiffres précédée du signe dièse #. Voici les codes des seize couleurs standard fondamentales définies dans le standard HTML 4.01 de 1999 :

Tableau 25.1

Couleur
Hexadécimal
Couleur
Hexadécimal

Noir

#000000

Vert

#008000

Argent

#C0C0C0

Citron-vert

#00FF00

Gris

#808080

Olive

#808000

Blanc

#FFFFFF

Jaune

#FFFF00

Marron

#800000

Bleu marine

#000080

Rouge

#FF0000

Bleu

#0000FF

Pourpre

#800080

Turquoise

#008080

Fuchsia

#FF00FF

Vert d’eau

#00FFFF

Les autres valeurs possibles permettent de définir d’autres nuances. Ce qui suit le préfixe est en fait une série de trois paires de valeurs hexa entre 00h et FFh : la première correspond au niveau de rouge, la deuxième au niveau de vert et la troisième au niveau de bleu. Pour les noirs, les trois composantes sont à 00h et, pour le blanc, elles sont à FFh. Pour obtenir une nuance de gris, il faut donner la même valeur intermédiaire aux trois composantes.

Si nous reprenons notre affichage en résolution 1 920 × 1 080, chacun des 2 millions de pixels requiert 3 octets pour ses trois composantes, ce qui donne 6 millions d’octets, soit 6 Mo par image.

Dans le chapitre précédent, tous les accès du processeur à la mémoire vive considéraient celle-ci comme un bloc monolithique. En réalité, la mémoire qui contient le code et les données ne constitue qu’une partie de l’espace mémoire total, une autre partie étant dédiée à la préparation de l’image vidéo à afficher. C’est cette implantation de l’image vidéo dans la mémoire vive qui permet aux machines de rafraîchir très vite les images, par simple écriture de données dans la mémoire. C’est ce qui produit des animations graphiques très rapides.

Le processeur 8 bits conçu au cours des derniers chapitres dispose d’adresses mémoire sur 16 bits, ce qui autorise 64 ko de mémoire. Il semble évident qu’il est impossible d’implanter une zone de 6 Mo pour la mémoire vidéo dans 64 ko ! (En théorie, il serait possible de répartir le travail en plusieurs blocs mémoire de petite taille placés tour à tour dans le petit espace mémoire du processeur, mais cela ralentirait énormément le fonctionnement et les affichages.)

C’est cet important besoin d’espace mémoire pour l’affichage qui est la cause d’une relativement lente progression de la qualité d’affichage ; il a fallu attendre que les circuits mémoire voient leur prix chuter et que des processeurs plus puissants soient capables d’accéder à autant de mémoire rapidement. Un processeur 32 bits peut par définition accéder à l’espace mémoire par blocs de 32 bits de largeur unitaire. C’est pour cette raison que la zone mémoire de l’affichage vidéo est en général organisée avec 4 octets par pixel et non les 3 octets qui, en théorie, suffiraient pour les trois composantes rouge, vert et bleu. Autrement dit, la mémoire vidéo requise pour un affichage en 1 920 × 1 080 occupe 8 Mo de mémoire et non les 6 théoriques.

En général, le contenu de l’espace mémoire vidéo est organisé dans le même ordre que l’image à actualiser. Le stockage commence par le pixel le plus à gauche de la première ligne d’affichage, et donc 3 octets pour les trois composites composantes rouge, vert et bleu, suivis d’un octet inutilisé. Pour afficher quoi que ce soit sur l’écran, qu’il s’agisse de graphiques ou de texte, le programme doit décider quels pixels allumer, éteindre ou modifier dans la mémoire vidéo graphique.

Les affichages graphiques en informatique s’appuient souvent sur des outils mathématiques et géométriques. La totalité de l’image ou une zone rectangulaire sélectionnée est considérée comme un système de coordonnées ; chaque pixel est déterminé par ses coordonnées dans les sens horizontal et vertical (X, Y). Le pixel aux coordonnées (10,5) est à 10 pixels de la colonne la plus à gauche et à 5 pixels du haut. Pour tracer sur un fond noir une diagonale entre ce point et un autre situé en (15,10), cela suppose de donner une couleur aux pixels (10, 5), (11, 6), (12, 7), (13, 8), (14, 9) et (15, 10). De nombreux outils logiciels sont disponibles pour tracer des lignes et des courbes bien plus sophistiquées.

L’affichage de texte est considéré comme un sous-ensemble de l’affichage graphique1.

Dans chaque police de caractères, chacun des caractères (chaque glyphe) est défini en tant que série de lignes droites et de courbes accompagnée d’informations de détail permettant d’optimiser l’affichage en vue d’une lisibilité maximale.

Les graphiques dits « 3D », soit en trois dimensions par effet de relief, sont beaucoup plus complexes à créer et utilisent des effets d’ombrage qui simulent une source de lumière. De nos jours, les logiciels profitent en général d’un coprocesseur graphique GPU (Graphics Processing Unit) qui prend en charge tous les traitements mathématiques et géométriques complexes que demande l’affichage de graphiques en 3D.

Lors de l’apparition des premiers ordinateurs personnels, il n’était pas question d’affichage en haute résolution. Le premier affichage graphique pour l’IBM-PC correspondait au standard CGA (Colors Graphics Adapter) qui offrait trois modes graphiques :

160 × 200 pixels en 16 couleurs (avec 1 octet par pixel) ;

320 × 200 pixels en 4 couleurs (2 bits par pixel) ;

640 × 200 pixels en 2 couleurs (1 bit par pixel).

Dans chacun des trois modes, on n’avait besoin que de 16 000 octets d’espace mémoire. En effet, 320 pixels en largeur sur 200 pixels en hauteur avec un quart d’octet par pixel occupent 16 000 octets.

Les tout premiers écrans informatiques n’étaient même pas capables d’afficher le moindre tracé graphique. Seul le texte était envisageable. C’est une excellente technique pour réduire les besoins en espace mémoire. C’est ce mode texte qui correspondait au standard de base des premiers IBM-PC, le standard MDA (Monochrome Display Adapter). Le MDA permettait d’afficher 25 lignes de 80 caractères de texte en une seule couleur, le vert sur fond noir. Chaque caractère était défini par un code ASCII sur 8 bits accompagné d’un octet d’attribut permettant de faire varier la luminosité et d’ajouter un effet de vidéo inverse, un soulignement ou un clignotement. Pour stocker le contenu d’une image affichée, il fallait donc prévoir 25 × 80 × 2 octets, soit 4 000 octets. La carte vidéo contenait des circuits qui exploitaient un composant mémoire en lecture seule (ROM) assurant la conversion du code de chaque caractère ASCII en plusieurs lignes et colonnes de pixels allumés sur un tube cathodique.

Nous savons qu’un processeur utilise des canaux internes appelés bus pour faire circuler les données et les adresses entre ses différents composants. De même, le processeur est en général connecté à des bus externes pour échanger des données entre processeur, mémoire et périphériques.

L’espace mémoire principal du processeur est partagé non seulement avec l’affichage vidéo, mais aussi avec des périphériques. Il s’agit dans ce cas d’un espace d’entrée/sortie mémoire (memory mapped). Le processeur peut se fonder sur un bus dédié à l’accès aux périphériques et tirer profit de mécanismes spécifiques pour exploiter les équipements d’entrée/sortie.

Le processeur que nous avons conçu dans les chapitres précédents s’est inspiré du microprocesseur Intel 8080. Le jeu d’instructions du 8080 en comprend deux qui portent les noms IN et OUT :

Tableau 25.2

Instruction
Description
Code d’opération

IN port

Lecture d’un octet depuis l’entrée

DBh

OUT port

Écriture d’un octet vers la sortie

D3h

Ces deux instructions doivent être suivies d’un numéro de port sur 8 bits. C’est un peu l’équivalent d’une adresse mémoire mais avec seulement 1 octet pour désigner un périphérique d’entrée/sortie. L’instruction IN réalise une lecture depuis le port mentionné en stockant la donnée dans l’accumulateur et l’instruction out envoie le contenu de l’accumulateur vers ce port. Le 8080 est doté d’un signal spécial qui permet de savoir s’il est en train d’accéder à la mémoire vive ou à un port d’entrée/sortie.

Intéressons-nous par exemple au dialogue avec un clavier d’ordinateur. Chacune des touches du clavier n’est qu’un interrupteur qui est fermé lorsque la touche est enfoncée. À chaque touche correspond un code numérique spécial. Imaginons que le clavier corresponde au port numéro 25h. Le programme pourrait lire la touche frappée en exécutant l’instruction suivante :

IN 25h

Après cette instruction, l’accumulateur contiendrait un code numérique qui permettrait de savoir quelle touche a été tapée.

Vous pourriez être tenté de supposer que le code est justement le code ASCII de la lettre ou du chiffre représenté sur la touche. En fait, il n’est ni pratique, ni désirable de faire dépendre le matériel des codes ASCII. Par exemple, la touche du A pourrait correspondre aussi bien au code ASCII 41h (le A) qu’au code 61h (le a) selon que l’utilisateur a utilisé ou pas la touche MAJ. En outre, il y a beaucoup de touches sur un clavier d’ordinateur, notamment les touches de fonction et les flèches du curseur, pour lesquelles il n’existe pas de code ASCII. Enfin, il n’est pas difficile pour un logiciel de créer la correspondance entre un code numérique de touche de clavier et un code ASCII éventuel lorsqu’il existe.

Mais tout d’abord, comment le programme est-il informé qu’une touche a été enfoncée ? La première technique consiste à interroger le périphérique clavier très fréquemment en le scrutant (scanning). La technique qui a été privilégiée, beaucoup plus efficace, consiste à faire en sorte que le clavier prévienne le processeur qu’une touche a été enfoncée. Pour qu’un périphérique interrompe le fonctionnement normal du processeur en lui faisant savoir qu’un événement qui le concerne s’est produit, il déclenche ce que l’on appelle une interruption, un signal spécial envoyé à la CPU.

Le 8080 définit huit instructions dites de redémarrage (RST signifiant restart) pour gérer les interruptions :

Tableau 25.3

Instruction
Description
Code d’opération

RST 0

Adresse d’appel 0000h

C7h

RST 1

Adresse d’appel 0008h

CFh

RST 2

Adresse d’appel 0010h

D7h

RST 3

Adresse d’appel 0018h

DFh

RST 4

Adresse d’appel 0020h

E7h

RST 5

Adresse d’appel 0028h

EFh

RST 6

Adresse d’appel 0030h

F7h

RST 7

Adresse d’appel 0038h

FFh

Chacune de ces instructions oblige le processeur à mémoriser le contenu du pointeur d’instruction sur la pile avant de sauter à l’adresse mémoire correspondant à l’instruction (0000h, 0008h, etc.). La première instruction de la série, RST 0, équivaut à une réinitialisation du processeur. Les autres permettent un saut ou un appel de sous-programme.

Voici comment cela fonctionne : le composant processeur 8080 possède une broche d’entrée pour un signal d’interruption externe. Lorsqu’un périphérique tel qu’un clavier arme ce signal en faisant basculer l’état de la ligne, il en profite pour diffuser l’octet du code de l’instruction d’interruption sur le bus de données. L’adresse mémoire correspondante contient les instructions pour gérer la réaction demandée par le périphérique.

Il s’agit dans le cas d’un mécanisme d’entrée/sortie piloté par interruption. Le processeur n’a plus à perdre du temps pour effectuer des sondages périodiques des périphériques. Il peut réaliser ses autres traitements jusqu’à ce qu’un périphérique active le signal d’interruption pour prévenir la CPU qu’un événement a eu lieu. C’est de cette façon que le clavier prévient la CPU qu’une touche a été enfoncée.

C’est également au moyen d’interruptions que doit être géré une souris, un pavé tactile ou l’écran tactile d’un téléphone ou d’une tablette. En apparence, la souris semble être directement reliée à l’affichage. Vous la bougez dans tous les sens et le pointeur se déplace de façon proportionnelle à l’écran. Mais ce n’est qu’une illusion. La souris transmet ses impulsions électriques qui indiquent dans quelle direction elle a été déplacée. Un pilote logiciel convertit ces valeurs en coordonnées graphiques qui sont utilisées pour actualiser sans cesse l’affichage du pointeur de souris à l’endroit correspondant. La souris envoie également à l’unité centrale des indications de bouton enfoncé et de molette manipulée.

Un écran tactile est en général une couche transparente collée devant l’écran d’affichage afin de détecter un changement de capacité électrique provoqué par un doigt. L’écran tactile peut indiquer les coordonnées d’un ou de plusieurs doigts en utilisant la même technique avec des paires (x, y) que pour les affichages graphiques vus plus haut. Le logiciel est informé de la présence ou de l’absence d’un doigt et de la quantité d’un déplacement par glissement. Cette information est ensuite exploitée par le programme pour faire défiler le contenu de l’écran ou faire apparaître un objet graphique suivant le parcours du doigt. Cela permet également d’interpréter le mouvement de pincement de deux doigts pour zoomer.

Dans un ordinateur, tout est numérique par définition. Il n’y a que des nombres. Mais le monde réel est essentiellement analogique. Notre perception de la lumière et du son semble continue.

La passerelle entre le monde des données analogiques du réel et les valeurs numériques se fonde sur deux équipements complémentaires :

le convertisseur analogique-numérique CAN, en anglais ADC ;

le convertisseur numérique-analogique CNA, en anglais DAC.

Le signal d’entrée d’un convertisseur analogique-numérique CAN est une tension pouvant varier de façon continue entre deux limites. Ce que produit le convertisseur en sortie est une valeur binaire qui représente cette tension de façon plus ou moins exacte. La plupart des CAN effectuent la conversion sur 8 ou sur 16 bits. Pour un CAN 8 bits, la valeur en sortie peut varier entre 00h, qui correspond à 0 V, et FFh pour 5 V, la valeur intermédiaire 80h correspondant à la tension médiane de 2,5 V.

Le convertisseur numérique-analogique fonctionne en sens inverse. En entrée, il reçoit une valeur binaire sur 8 ou 16 bits pour produire en sortie une tension correspondant à cette valeur.

Les CNA sont indispensables dans les écrans d’affichage, car ils convertissent les valeurs numériques des pixels en tensions contrôlant l’intensité de lumière émise par les lettres rouges, vertes et bleues qui constituent l’écran plat.

Les appareils photo numériques (APN) et les caméras vidéo utilisent des capteurs à pixels actifs qui convertissent la lumière en tension, elle-même convertie en valeur numérique ensuite dans un CAN. On obtient ainsi ce qu’on appelle un bitmap ou champ binaire : c’est une surface rectangulaire de valeurs de pixels colorés. Les pixels sont stockés sur le même modèle que l’image en mémoire vidéo : ligne par ligne en commençant en haut, et de gauche à droite.

Le poids d’un fichier image de type bitmap peut devenir très important. L’appareil photo de mon téléphone personnel prend des images de 4 032 pixels de large sur 3 024 pixels de haut. Une partie de ces données pourrait être éliminée sans modifier la perception de l’image par un humain. C’est pour cette raison que des ingénieurs et mathématiciens se sont concertés pour inventer plusieurs techniques de réduction du volume des images bitmap, ce qui s’appelle la compression.

Une technique de compression très simple porte le nom RLE, ou Run-Length Encoding. Lorsque le logiciel de compression détecte que 10 pixels successifs ont exactement la même couleur, il ne stocke que la valeur du premier puis le nombre 10 en tant que multiplicateur. Cela n’est efficace que dans des images comportant de vastes étendues unies.

Un autre mode de compression de fichier toujours très répandu correspond au format GIF qui signifie Graphics Interchange Format. Conçu au départ en 1987 par un des pionniers du service en réseau Compuserve, ce format exploite la technique de compression LZW (premières lettres des noms de ses trois créateurs Lempel, Zif et Welch). Cette technique consiste à rechercher des motifs de pixels identiques et pas seulement des suites de pixels identiques. Un fichier GIF peut être enrichi par un mécanisme d’animation simplifié permettant d’alterner parmi plusieurs images.

Un format plus évolué que le GIF est le format PNG de 1996 (Portable Network Graphics). Dans ce format, les pixels adjacents sont comparés. Ce sont les différences de nuances qui sont stockées, car cela produit généralement des valeurs plus petites et plus faciles à compresser.

Notez qu’un fichier GIF ou PNG n’est pas nécessairement plus compact que le fichier image bitmap initial ! La plupart le sont, mais certains au contraire grossissent, notamment lorsque l’image comporte énormément de nuances de couleurs différentes ou de détails.

Il faut dans ce cas adopter d’autres techniques. En 1992 a été proposé le standard JPEG qui est devenu très populaire pour toutes les images représentant des vues du monde réel. Les appareils photo des téléphones actuels produisent des fichiers au format JPEG pouvant être directement partagés ou transférés vers d’autres machines.

L’acronyme JPEG signifie Joint Photographic Experts Group. Ce format se distingue de ses prédécesseurs car il se fonde sur les résultats des recherches psycho-visuelles relatives à la manière dont l’œil humain perçoit les images. Le format JPEG permet notamment des compressions qui éliminent les transitions de couleur brutales, ce qui limite la quantité de données à conserver pour reproduire l’image. Les mathématiques en jeu sont assez complexes.

L’inconvénient du format JPEG est sa non-réversibilité, c’est-à-dire qu’il n’est pas possible de reconstruire exactement l’image originale avant la compression. Ceci le distingue des formats sans perte GIF et PNG qui, eux, sont réversibles. JPEG est un format de compression avec perte : des informations sont perdues et, dans certains cas limites, cela peut produire des artefacts et des distorsions visuelles.

En ce qui concerne le monde du son, les ordinateurs sont généralement équipés d’un microphone pour capturer des ondes sonores du monde réel et d’un haut-parleur pour produire des ondes sonores.

Le son est une vibration transmise par de la matière. Les cordes vocales vibrent, le tuba vibre, l’arbre qui tombe dans une forêt vibre ; tous ces objets provoquent des déplacements de molécules de l’air. Les molécules sont poussées en avant et en arrière, ce qui provoque des compressions et variations de pression plusieurs centaines ou milliers de fois par seconde. Cet effet se propage dans l’air jusqu’à atteindre nos tympans et nous permettre d’entendre.

Dans un microphone, les vibrations sont transformées en un courant électrique dont la tension varie en même temps que la pression sonore. Les petites crêtes et les légers creux de la gravure d’un cylindre tel que celui inventé par Thomas Edison suivent aussi de façon analogue les ondes sonores (ce que cet inventeur avait prouvé dans son premier phonographe en 1877). Ce sont les mêmes crêtes et creux que l’on trouve dans les disques vinyle qui redeviennent à la mode et continuent à être très appréciés des audiophiles et amateurs de technologies du passé.

L’ordinateur a besoin que cette tension soit numérisée, c’est-à-dire transformée en suite de nombres, ce qui est exactement le travail d’un convertisseur CAN.

Le son numérique est devenu accessible au grand public avec l’apparition du disque compact CD en 1983. Le lancement du CD a véritablement été le plus grand succès de l’électronique grand public. Inventé par les sociétés Philips et Sony, il avait été dès le départ prévu pour pouvoir stocker la totalité de la Neuvième Symphonie de Beethoven, soit 74 minutes sur un seul côté d’un disque de 12 cm de diamètre (c’est du moins ce que prétend la légende).

Sur un CD, le son est codé au moyen de la technique PCM de modulation par impulsions codées (Pulse Coded Modulation). Malgré son nom ambitieux, cette technique est assez simple : pour la création du contenu, la tension qui correspond à une onde sonore est convertie en valeurs numériques successives à un rythme constant, puis ces valeurs sont stockées. Lors de la lecture, les valeurs numériques sont converties en courant électrique en utilisant un convertisseur numérique analogique CNA.

Le nombre de captures d’échantillons de la tension analogique en une seconde se nomme le taux d’échantillonnage. C’est en 1928 que Harry Nyquist, des laboratoires de la société Bell Telephone, avait montré qu’il fallait choisir un taux d’échantillonnage au moins double à la plus haute fréquence à numériser. On considère que l’oreille humaine peut entendre les sons dans une plage entre 20 Hz et 20 000 Hz. C’est pourquoi la fréquence d’échantillonnage choisie est un peu supérieure au plafond physiologique humain, à exactement 44 100 échantillons par seconde.

La quantité de nuances différentes entre le son le plus doux et le son le plus fort pour être enregistré et relu sans être altéré correspond à la dynamique sonore qui, sur un CD, correspond au nombre de bits par échantillon. Une explication détaillée s’impose. Le courant électrique qui représente les ondes sonores sous forme analogique varie dans un sens et dans l’autre, et chaque crête forme l’amplitude. Ce que l’oreille humaine perçoit comme intensité d’un son est proportionnel au double de l’amplitude. La mesure d’amplitude utilise une unité d’intensité sonore mesurée au moyen de l’unité bel (les trois premières lettres du nom de famille d’Alexandre Graham Bell), ou plus exactement le décibel. 1 dB représente à peu près le plus petit changement d’intensité qu’un humain est en mesure de ressentir et de détecter.

L’expérience a montré qu’en choisissant 16 bits par échantillon, on disposait d’une plage de dynamique de 96 dB, qui s’étend à peu près du seuil d’audition (un son moins fort ne peut plus être entendu) au seuil de douleur, intensité qui nous amène à vouloir nous protéger les oreilles avec les mains. Un Compact Disc utilise 16 bits par échantillon, donc 2 octets.

Chaque seconde de son sur un CD représente 44 100 échantillons de 2 octets chacun. Mais de nos jours, on ne peut se passer de la stéréophonie. Il faut donc doubler le volume, ce qui nous amène à 176 400 octets par seconde, soit 10 584 000 octets par minute. (Vous comprenez maintenant pourquoi l’enregistrement numérique du son ne s’est pas répandu avant les années 1980.) Les 74 minutes d’un CD stéréo rempli correspond donc à un volume de données de 783 216 000 octets. Des variantes de format plus récentes ont légèrement augmenté cette capacité maximale.

Le CD est de moins en moins utilisé depuis quelques années, mais les concepts du son numérique restent les mêmes. Dans la mesure où vous n’avez pas toujours besoin d’une qualité de niveau CD pour enregistrer et relire chez vous, vous pouvez opter pour des taux d’échantillonnage plus bas, par exemple 22 050 Hz, 11 025 Hz ou même 8000 Hz. Vous réduisez encore le volume de données en choisissant d’échantillonner sur 8 bits seulement et vous pouvez encore diviser le volume d’enregistrement par deux en enregistrant en mono.

Comme dans le cas des images bitmap, il est souvent conseillé de compresser les fichiers audio pour réduire l’encombrement pour le stockage et pour le transfert via les réseaux. La technique de compression la plus répandue pour l’audio est le MP3, qui était au départ un sous-ensemble de la technique de compression des films MPEG (Moving Picture Experts Group). Le format MP3 est un format de compression avec perte mais, tirant profit des études de psycho-acoustique, il supprime d’abord les données dont l’absence est la moins audible lors de l’écoute du résultat.

Toutes ces données que sont les images bitmap au format GIF, PNG ou JPG et les fichiers audio au format MP3 occupent néanmoins de l’espace en mémoire, notamment pendant qu’un programme doit traiter les données correspondantes. Le reste du temps, ces données sont stockées dans des fichiers implantés sur des périphériques de stockage.

Vous savez que les circuits de mémoire vive ou mémoire à accès direct RAM, qu’ils soient construits avec des relais, des tubes à vide ou des transistors, perdent toutes les données qu’ils contiennent dès que l’alimentation électrique est interrompue. Un ordinateur digne de ce nom doit donc posséder un périphérique de stockage permanent. Il y a eu d’abord des cartes perforées telles que celles d’IBM. Même à l’époque des premiers ordinateurs personnels, des rouleaux de papier perforé permettaient de sauvegarder les programmes et les données à recharger en mémoire. Une première amélioration a consisté à utiliser les cassettes audio qui étaient très répandues dans les années 1980 pour la musique. Ces cassettes constituaient en quelque sorte une version simplifiée et miniaturisée des grosses unités de bandes magnétiques que l’on voyait tourner dans les salles informatiques.

La technique de la bande magnétique n’est cependant pas la meilleure pour stocker et récupérer des données parce qu’il n’est pas facile d’aller rapidement d’un endroit à un autre de la bande. Il faut patienter pendant le rembobinage ou le débobinage.

Le disque reconnu comme un moyen géométriquement bien plus approprié pour les accès rapides à n’importe quel endroit du support est le disque. Il est en permanence mis en rotation et comporte une ou plusieurs têtes de lecture soutenues par des bras qui se déplacent entre l’intérieur et l’extérieur du disque. On peut ainsi accéder très rapidement à n’importe quel endroit. Les bits sont codés par magnétisation de toutes petites régions du disque. Les premiers disques magnétiques pour l’informatique ont été inventés par IBM en 1956. Son appareil RAMAC (Random Access Method of Accounting and Control) est constitué de 50 disques métalliques de 60 cm de diamètre, l’ensemble pouvant stocker 5 Mo.

Les ordinateurs personnels ont pu bénéficier d’une version allégée du stockage magnétique sur disque sous la forme de disquettes souples (floppy). Il s’agit d’un disque en plastique recouvert d’une poudre magnétique et protégé par un étui en plastique noir. Les premières disquettes se présentaient au format de 8 pouces, puis elles ont été remplacées par les disquettes de 5,25 pouces et, enfin, par des disquettes 3,5 pouces dans un étui rigide. L’avantage de ces disquettes était qu’elles étaient amovibles, ce qui permettait de transférer des données d’une machine à une autre et de diffuser des logiciels commercialisés. De nos jours, les disquettes ont quasiment disparu, et on ne trouve plus qu’un ou deux modèles de lecteurs de disquettes 3,5 pouces. Néanmoins, la silhouette de la disquette orne encore souvent les icônes de sauvegarde de nombreux logiciels.

Pendant plusieurs décennies, les ordinateurs personnels ont surtout été dotés d’un disque dur, constitué de plusieurs disques métalliques rigides dans un boîtier étanche. Les performances des disques durs sont généralement bien meilleures que celles des disquettes et leur capacité est largement supérieure, mais la plupart ne sont pas amovibles, même s’ils sont démontables.

De nos jours, le stockage de masse prend de plus en plus souvent l’aspect d’un disque SSD, constitué de circuits mémoire similaires aux bascules vues dans ce livre. Ces disques sont soit dans le même format que les disques durs magnétiques qu’ils remplacent, soit dans un format encore plus compact, ou bien se présentent sous forme de cartes mémoire enfichables appelées clés USB de stockage.

Sur un périphérique de stockage de masse, on écrit des fichiers dont les tailles peuvent varier énormément selon la nature du contenu et l’origine. Pour organiser ce stockage, les périphériques sont normalement découpés en zones de taille fixe appelées secteurs. Les disquettes et les disques durs se caractérisent en général par une taille de secteur de 512 octets alors que les disques SSD offrent des tailles entre 512 et 4 096 octets.

Le stockage d’un fichier occupe au moins un, mais plus généralement plusieurs secteurs. Si le fichier est d’une taille inférieure à 512 octets alors que la taille de secteur est de 512 octets, un seul secteur sera occupé, mais l’espace en fin de secteur sera perdu. Un fichier de 513 octets va occuper deux secteurs, dont un presque vide. Un fichier de 1 Mo va occuper 4 096 secteurs.

Notez que les secteurs dans lesquels sont stockées les données ne sont pas nécessairement consécutifs : ils peuvent être disséminés en différents endroits libres du disque. En effet, lorsqu’un fichier est supprimé, les secteurs qu’il occupait sont libérés. Lorsqu’un nouveau fichier est créé, on lui attribue ces secteurs libres, mais si le fichier est plus gros que le précédent, on lui attribue d’autres secteurs qui ne sont pas situés à la suite des premiers.

Le suivi de tous ces espaces libérés et occupés, ainsi que le stockage et la relecture des données constituent une partie importante du travail d’un logiciel fondamental dans tout ordinateur : le système d’exploitation.

1 N.d.T. : les premiers écrans des systèmes des années 1960 et 1970 ne permettaient pas l’affichage de graphiques, mais uniquement des caractères ASCII. On parlait de terminaux en mode texte.

Chapitre 26  Systèmes d’exploitation

Au long des quelques chapitres précédents, nous sommes parvenus à construire, tout du moins dans notre imagination, ce qui semble être un ordinateur complet. Il dispose d’un processeur (son unité centrale ou CPU), d’une certaine quantité de mémoire vive RAM, d’un clavier, d’un écran (dont la mémoire vidéo est implantée dans la mémoire vive) et d’un périphérique de stockage de masse. La partie matérielle de notre machine est donc complète. Nous scrutons avec impatience l’interrupteur de mise sous tension qui va pouvoir donner vie à l’ensemble. Le projet vous fait peut-être penser aux travaux de Victor Frankenstein et de son monstre ou encore de Gepetto et de sa poupée de bois Pinocchio.

Mais il nous manque peut-être quelque chose et ce n’est pas l’éclair divin qui insuffle la vie dans la matière inerte. Pour le savoir, utilisez l’interrupteur et mettez la machine sous tension puis observez ce qui se passe.

L’écran semble s’allumer, mais il n’affiche que des choses incohérentes. Si le système est doté d’une carte vidéo graphique, l’écran va afficher toutes sortes de couleurs de façon complètement aléatoire. Si la carte vidéo est en mode texte, vous verrez apparaître des caractères au hasard. Cette situation est normale. En effet, les circuits mémoire à semi-conducteur perdent leur contenu à la mise hors tension. Lorsqu’ils sont remis sous tension, l’état du contenu est imprévisible. La mémoire vive contient des valeurs totalement aléatoires. Au démarrage, le processeur va tenter de considérer ces valeurs comme des codes d’instruction machine. Normalement, cela n’aura pas d’effet négatif sur l’intégrité du matériel notamment, mais le système ne vous sera d’aucune utilité pour quoi que ce soit.

Ce qui nous manque, c’est du logiciel. Au moment de la mise sous tension ou du redémarrage d’un processeur, il tente d’exécuter le code machine situé à une adresse prédéterminée. Dans le cas de l’Intel 8080, cette adresse est 0000h.

Si l’ordinateur est correctement conçu, cette adresse doit contenir le code d’une instruction (normalement, la première de toute une série) qui doit être exécutée au démarrage.

La question qui se pose est : comment faire en sorte que cette instruction soit présente à cette adresse mémoire ? La mise en place d’un logiciel dans un ordinateur va constituer l’un des aspects les plus perturbants du projet. On peut envisager d’utiliser un panneau de contrôle comme celui du Chapitre 19 et stocker l’un après l’autre chacun des octets dans la mémoire vive pour pouvoir les relire plus tard :

Figure 26.1

Ce panneau se distingue de la version précédente par l’ajout d’un interrupteur Reset. Il est connecté à l’entrée de même nom du processeur. Tant que cet interrupteur est en position 1, le processeur ne fait rien. Quand vous le basculez en position 0, vous obligez le processeur à exécuter le code machine à partir de l’adresse 0000h.

Pour utiliser ce panneau, vous commencez par basculer l’interrupteur Reset sur la position 1 afin de réinitialiser le processeur et le faire arrêter d’exécuter du code. Vous basculez alors sur 1 l’interrupteur de prise de contrôle local Takeover afin de pouvoir utiliser le bus d’adresse et le bus de données. À partir de ce moment, vous pouvez spécifier une adresse mémoire sur 16 bits avec les interrupteurs de A0 à A15. Les lampes D0 à D7 doivent alors indiquer la valeur sur 8 bits lue à cette adresse. Pour écrire une nouvelle valeur à cette adresse, vous utilisez les interrupteurs D0 à D7 puis vous basculez et rebasculez l’interrupteur d’écriture Write. Après avoir inséré des octets en mémoire, vous pouvez rebasculer l’interrupteur Takeover puis l’interrupteur Reset en position 0, ce qui provoque l’exécution du programme.

Nous venons de dérouler le processus permettant d’entrer un programme en code machine dans un ordinateur, celui que nous venons de concevoir. Reconnaissez que c’est un travail fastidieux. Il est probable que vous commettiez une petite erreur de temps à autre et ayez rapidement des ampoules au bout des doigts. Si votre cerveau commence à se liquéfier, dites-vous que ce sont les risques du métier.

Tous ces efforts devraient être récompensés par l’affichage sur l’écran des résultats d’exécution de votre petit programme. Un des premiers essais de programmation pourrait être un sous-programme pour convertir des valeurs numériques en codes ASCII. Supposons que vous ayez créé un programme dont le résultat est la valeur 4Bh. Vous ne pouvez pas directement envoyer cette valeur dans la mémoire vidéo. Au lieu d’afficher cette valeur numérique hexadécimale, vous verrez apparaître un K majuscule, puisque cette valeur (75d) est le code ASCII de cette lettre. Vous avez besoin d’afficher deux caractères ASCII, avec le code 34h pour le chiffre 4 et le code 42h pour la lettre B. Nous avons déjà vu dans le Chapitre 24 une routine nommé VerAscii qui réalisait cette conversion.

Pour sortir de l’ère paléolithique de l’informatique que nous inflige ce panneau de contrôle, nous allons rédiger un programme appelé gestionnaire de clavier qui se tient à l’écoute des codes des touches enfoncées sur le clavier, stocke ces valeurs en mémoire et en envoie une représentation à l’écran. Le fait d’afficher sur un écran directement ce qui était saisi sur un clavier correspond à l’opération d’écho. Elle donne l’impression qu’il y a une connexion directe entre clavier et écran, ce qui est faux.

Le programme de capture des frappes clavier peut être enrichi pour devenir capable d’exécuter des commandes simples. Le code de traitement de ces commandes est un processeur de commande. Dans sa première version, nous ne le ferons réagir qu’à trois commandes. Nous décidons que ces commandes seront identifiées par la première lettre du nom de la commande en anglais, lettre qu’il suffit de saisir sur la ligne de commande :

W pour l’écriture (Write) ;

D pour l’affichage (Display) ;

R pour l’exécution (Run)1.

La commande sera prise en compte par l’outil de gestion du clavier dès que vous confirmerez la saisie en frappant la touche Entrée ou Retour.

Si votre saisie commence par un W, vous annoncez que vous voulez faire écrire plusieurs octets en mémoire. Voici comment pourrait se présenter une ligne de commande pour écrire quelques valeurs :

W 1020 35 4F 78 23 9B AC 67

Cette commande demande au processeur d’écrire successivement les valeurs hexadécimales 35h, 4Fh, etc., à partir de l’adresse mémoire indiquée au départ : 1020h. Bien sûr, l’outil de gestion clavier doit convertir les codes ASCII des lettres saisies en valeurs numériques, opération inverse de celle du sous-programme VerAscii vu plus haut.

Si la ligne commence par un D, cela signifie que vous voulez faire afficher la valeur d’un ou de plusieurs octets en fournissant son adresse. Voici à quoi pourrait ressembler une telle commande d’affichage :

D 1030

Le processeur de commande répond à cette demande en affichant la valeur trouvée à l’adresse mentionnée, 1030h dans l’exemple. Vous pouvez ainsi connaître le contenu de la mémoire.

Enfin, si votre ligne de texte commence par un R, c’est que vous voulez faire exécuter une série d’instructions :

R 1000

Cet exemple demande d’exécuter le programme qui débute à l’adresse 1000h. Pour répondre à cette demande, le processeur va, par exemple, stocker l’adresse 1000h dans la paire de registres H et L, puis appeler l’instruction PCHL qui va justement charger dans le pointeur d’instruction l’adresse trouvée dans HL, ce qui provoque le saut de l’exécution à cette adresse.

La mise au point d’un tel gestionnaire de clavier couplé à un processeur de commande constitue une avancée significative. Vous n’avez ensuite plus à vous soucier de manipuler des interrupteurs sur un panneau de contrôle. La saisie au moyen d’un clavier est plus rapide, plus efficace et plus simple.

Il vous reste à gérer le problème de la volatilité des choses. Toutes les instructions que vous saisissez s’évaporent dès que vous éteignez l’ordinateur. Vous allez donc chercher à rendre ce code permanent en le stockant dans un composant mémoire appelé ROM, une mémoire en lecture seule. À l’époque des microprocesseurs de type Intel 8080, la création d’un circuit ROM spécifique était devenue accessible aux particuliers. On pouvait se procurer des circuits PROM (Programmable Read-Only Memory) qui étaient livrés vierges et pouvaient être écrits une seule fois. Une variante est ensuite apparue sous la forme EPROM (Erasable Programmable ROM) ; une fois écrite, celle-ci pouvait être réutilisée en effaçant le contenu par un rayonnement ultraviolet appliqué quelques minutes.

Ce nouveau circuit mémoire ROM contiendrait votre programme de gestion clavier. Il resterait à s’organiser pour qu’il corresponde à l’adresse mémoire 0000h, qui est jusqu’ici dans la mémoire vive RAM. Bien sûr, la mémoire vive RAM est conservée, mais son contenu devra commencer à une adresse supérieure, après la plus haute adresse dans la mémoire morte ROM.

La création d’un processeur de commande apporte non seulement une simplification à la saisie des octets en mémoire, mais cela en fait l’outil par lequel votre ordinateur devient interactif.

Une fois que le processeur de commande est ainsi implanté en mémoire morte, il devient possible d’écrire des données depuis la mémoire vers un disque dur, puis de relire le contenu du disque dur vers la mémoire. En stockant le code et les données sur disque, on se place à l’abri de toute rupture de l’alimentation qui effacerait le contenu de la mémoire vive. Bien sûr, le contenu de la mémoire morte n’est pas modifiable dans les conditions d’utilisation.

Une fois doté de votre équipement de stockage de masse, vous aurez envie d’ajouter une paire de commandes à votre processeur de commande : une commande S signifiant Stockage de données dans un groupe spécifié de secteurs du disque et une commande L pour la lecture du contenu d’un groupe spécifié de secteurs du disque vers la mémoire.

Pour garder le contrôle de vos opérations de stockage et de relecture disque, vous devrez bien sûr noter les numéros des secteurs concernés par chaque opération en vous servant d’un calepin. Attention : si vous stockez sur disque un bloc de code qui était situé à une certaine adresse, vous ne pouvez pas le charger en mémoire à une autre adresse en espérant que cela va fonctionner. En effet, toutes les instructions de saut et d’appel CALL vont devenir inefficaces parce qu’elles vont viser des adresses qui ne sont plus valables. Par ailleurs, votre programme ou vos données peuvent représenter un volume supérieur à la taille d’un secteur de disque, ce qui va obliger à répartir le contenu dans plusieurs secteurs. Lorsque certains secteurs auront été utilisés par un autre programme ou fichier de données, les opérations de suppression et d’insertion successives vont créer des discontinuités ; votre fichier ne pourra plus être stocké dans des secteurs consécutifs.

Vous allez rapidement constater qu’il serait agréable de s’épargner ce fastidieux travail de notation des numéros de secteurs disque attribués à chaque bloc de chaque fichier. C’est à ce moment que vous êtes mûr pour vous lancer dans la création d’un logiciel essentiel d’organisation qu’on appelle un système de fichiers.

Un système de fichiers est un logiciel qui gère l’organisation des données sous forme de fichiers. Un fichier est une séquence d’octets occupant au moins un secteur sur disque et associé à un nom unique qui, dans l’idéal, devrait suggérer son contenu. On peut comparer un disque à un tiroir contenant des fiches dont on peut lire les onglets qui sont leurs noms.

Le système de fichiers est très généralement un des composants principaux d’un logiciel plus global que l’on nomme le système d’exploitation. Le gestionnaire de clavier et le processeur de commande vus en début de chapitre peuvent bien sûr constituer les germes d’un système d’exploitation. Mais le chemin à suivre pour aboutir à ce système complet est vraiment long. Découvrons plutôt un système d’exploitation réel en sorte de nous faire une idée de ce qu’il apporte et de la façon dont il fonctionne.

En termes historiques, le plus important système d’exploitation pour microprocesseurs 8 bits est CP/M (Control Program/Monitor puis Control Program for Microprocessors). Ce logiciel a été conçu au milieu des années 1970 spécifiquement pour les microprocesseurs Intel 8080 par Gary Kildall (1942-1994), fondateur de la société DRI (Digital Research Incorporated).

Le logiciel système CP/M était stocké sur disque, mais le disque disposait d’une importante quantité d’espace libre pour y stocker ses propres fichiers. Le système de fichiers de CP/M était assez rustique, mais répondait à deux contraintes importantes : chaque fichier sur disque était identifié par un nom stocké sur le disque. D’autre part, un fichier pouvait être stocké dans des secteurs non consécutifs. Cela se produisait souvent au fur et à mesure des créations et suppressions de fichiers de taille variable ; le résultat porte le nom de fragmentation. Il est très utile de pouvoir stocker les différentes portions d’un fichier volumineux dans des secteurs disque qui ne se suivent pas. Une table d’allocation contient les numéros et secteurs disque alloués à chaque fichier ; cette table est elle aussi stockée sur le disque.

Le nom d’un fichier sous CP/M est constitué de deux parties : la première partie est le nom de fichier lui-même (le radical) et sa longueur est limitée à huit caractères. La seconde partie est le type ou l’extension de nom de fichier et se limite à trois caractères. Plusieurs types de fichiers standard sont définis. Le type TXT correspond à un fichier contenant du texte (des codes ASCII pouvant être affichés directement de façon lisible). L’extension COM (commande) désigne un fichier contenant des instructions en code machine 8080, c’est-à-dire un programme.

Cette convention de nommage a été désignée par l’expression « huit-point-trois » qui rappelle qu’il y a au maximum huit lettres avant le point et trois lettres après. Cette contrainte de longueur a disparu dans les systèmes d’exploitation modernes, mais la convention de nommage des fichiers reste très usitée.

Dans un ordinateur fonctionnant sous CP/M, la mémoire morte ROM contient un petit bloc de code que l’on appelle l’amorceur (bootstrap loader, en anglais). Ce tout petit programme a pour mission d’implanter dans la mémoire vive le code de la suite du système d’exploitation qui est stocké sur disque. L’amorceur charge le premier secteur trouvé sur le disque ou la disquette, puis en lance l’exécution. Le code qui se situe dans ce premier secteur réalise le chargement du reste du système en mémoire. L’ensemble de ce processus correspond à l’amorçage du système (booting)2.

Le système CP/M est organisé de façon hiérarchique en plusieurs couches ; celle de plus bas niveau correspond au système basique d’entrée/sortie BIOS (Basic Input/Output System). Il s’agit de l’ensemble des sous-programmes autorisés à accéder directement aux composants matériels, par exemple pour la lecture et l’écriture des secteurs disque. Tout fabricant d’ordinateurs qui décidait d’utiliser le système CP/M devait fournir son propre logiciel BIOS, car les détails varient selon les choix exacts de matériel.

Le niveau hiérarchique suivant du système correspond au système basique de gestion du disque BDOS (Basic Disc Operating System). Ce sous-système gère le stockage des données dans les secteurs disque manipulés par le BIOS en maintenant le lien entre données brutes et fichiers nominatifs.

Lorsque le processus de démarrage de CP/M est achevé, c’est-à-dire qu’il est entièrement chargé en mémoire, il lance l’exécution du programme interactif CCP (Console Command Processor) qui affiche un message à l’écran appelé invite3 :

A>

Les premiers ordinateurs personnels étaient souvent dotés de deux lecteurs de disquettes A et B, la lettre A correspondant au premier de ces deux lecteurs, celui qui était scruté par le système BIOS pour en charger le système d’exploitation CP/M4.

Le message affiché par le système lorsqu’il n’a plus rien à faire (l’invite ou l’incite) vous invite à saisir une ligne de commande puis à valider votre saisie par la touche Entrée. La plupart des commandes concernent les fichiers : affichage de la liste des fichiers d’un répertoire avec DIR, effacement d’un fichier avec ERA, renommage avec REN et affichage du contenu d’un fichier lorsque c’est possible avec TYPE. Lorsque vous saisissez un nom de fichier qu’il ne trouve pas localement, le système CP/M suppose que c’est le nom d’un programme qui est stocké quelque part dans un des sous-répertoires particuliers du disque.

CP/M était fourni avec toute une série de sous-programmes utilisables par les programmes des concepteurs de logiciels pour, notamment, effectuer des lectures clavier, l’affichage des caractères à l’écran, l’enregistrement des données dans un fichier disque et le chargement du contenu d’un fichier en mémoire.

Un programme d’utilisateur qui fonctionne sous CP/M n’a pas besoin d’accéder directement aux détails du matériel parce que la partie BDOS du système CP/M s’appuie sur les routines du BIOS pour prendre en charge tous les accès au matériel. C’est ce qui permet à un logiciel conçu pour CP/M de fonctionner sur n’importe quel ordinateur sur lequel CP/M a été installé, sans avoir à se soucier des éventuelles différences au niveau matériel. Ce principe de non-dépendance des périphériques et du matériel est une faculté indispensable au déploiement commercial des logiciels. Les programmes des utilisateurs sont ensuite devenus ce que l’on appelle des applications.

L’ensemble des sous-programmes qui font partie du système d’exploitation et qui peuvent être appelés par les logiciels conçus sur ce système constituent l’interface de programmation d’applications, abrégée en API (Application Programming Interface). En théorie, le concepteur d’un logiciel n’a besoin de connaître que la panoplie de fonctions API, sans se soucier des détails de réalisation de ces routines et des modalités d’accès au matériel. En pratique, il reste souvent utile d’avoir quelques connaissances sur ce fonctionnement interne.

Du point de vue de l’utilisateur d’un système informatique, le système d’exploitation est son interface utilisateur, son IHM (interface homme-machine)5.

Dans le cas du système CP/M, cette interface correspond au module CLI (Command Line Interface) mis en œuvre par la partie CCP. Aux yeux d’un programmeur qui crée un nouveau logiciel, le système d’exploitation constitue son interface API, soit l’ensemble des fonctions système disponibles aux programmeurs.

Dans le cas précis de CP/M, le système définit un point d’entrée unique pour toutes les routines de l’API, à l’adresse 0005h. Pour exploiter une routine API, le programme doit émettre un appel à cette adresse mémoire :

CALL 0005h

ou de façon abrégée :

CALL 5

C’est cette écriture abrégée qui a fait que cette interface est surnommée « CALL 5 ».

L’adresse à laquelle commence le code du sous-programme désiré doit être placée dans le registre C au moment de l’appel. Voici quelques exemples :

Tableau 26.1

Registre C
Fonction de l’appel CALL 5 de CP/M

01h

Entrée console (lecture d’un caractère du clavier)

02h

Sortie console (affichage d’un caractère à l’écran)

09h

Affichage d’une chaîne de caractères

15h

Ouverture d’un fichier (le fichier doit exister)

16h

Fermeture de fichier (libération des verrous sur le fichier)

20h

Lecture séquentielle (lecture d’une suite d’octets dans un fichier)

21h

Écriture séquentielle (écriture d’une suite d’octets dans un fichier)

22h

Création fichier (production d’un nouveau fichier)

Un certain nombre de ces fonctions ont besoin d’autres données pour opérer. Dans le cas par exemple de l’appel 09h, la paire de registres DE doit contenir l’adresse du premier caractère d’une série de caractères ou de mots qu’il faut afficher. La fonction reconnaît la fin de la chaîne en rencontrant le signe dollar.

Que fait exactement l’instruction CALL 5 ? Le système CP/M met en place à l’adresse mémoire 0005h une instruction JMP qui réalise un saut vers une région mémoire réservée au système et au sous-système disque BDOS. Une routine dans BDOS lit la valeur que contient le registre C et déclenche un autre saut vers la routine ainsi spécifiée.

CP/M fut pendant quelque temps un système d’exploitation pour 8080 très utilisé ; il conserve son importance en termes historiques. En effet, CP/M a énormément influencé un autre système d’exploitation 16 bits, QDOS (Quick Dirty Operating System, ou système d’exploitation vite-fait mal-fait). Écrit par Tim Paterson de la société Seattle Computer Products, ce système était prévu pour les processeurs 16 bits qui allaient succéder au 8080, c’est-à-dire le 8086 et le 8088. QDOS a ensuite été renommé 86-DOS et, un beau jour, Microsoft Corporation en a acquis la licence de commercialisation sous un nouveau nom, très connu celui-ci : MS-DOS (Microsoft Disk Operating System). C’est ce système qui a fait l’objet d’une licence par IBM pour équiper son premier ordinateur personnel IBM-PC en 1981. Pendant ce temps, une version 16 bits de CP/M appelée CP/M-86 a été proposée pour ce même IBM-PC, mais MS-DOS a été préféré et est rapidement devenu le standard, y compris sous le nom légèrement différent choisi par IBM, PC-DOS. Ce système d’exploitation a été proposé sous licence à tous les fabricants qui avaient choisi de créer des ordinateurs compatibles avec l’IBM-PC.

Comme son nom le suggère, MS-DOS est d’abord un système d’exploitation de disque, comme l’était Apple DOS créé en 1978 pour l’Apple II. Il n’offrait que peu d’autres fonctions en dehors de celles de lecture et d’écriture de fichiers sur disque.

En théorie, les logiciels applicatifs ne sont pas censés appeler directement le matériel, mais utiliser les fonctions de l’interface API fournies par le système d’exploitation. Dans les années 1970 et 1980, nombreux sont les programmeurs qui se sont permis de contourner l’API du système, notamment pour ce qui concerne l’affichage vidéo. En effet, en écrivant directement les octets dans la mémoire vidéo, on pouvait travailler beaucoup plus vite qu’en passant par une routine de l’API pour qu’elle le fasse pour vous. D’ailleurs, certaines applications qui avaient, par exemple, besoin d’afficher des graphiques n’avaient pas d’autre solution car le système d’exploitation ne proposait au départ aucune fonction de tracé graphique. C’est pourquoi de nombreux programmeurs créatifs dans les premières années préféraient les systèmes d’exploitation qui restaient en dehors du chemin et laissaient les applications offrir les meilleures performances possibles en exploitant au maximum les possibilités du matériel.

Le premier signe montrant que l’informatique personnelle allait radicalement contraster avec les grands et moyens systèmes répandus à cette époque dans les grandes et moyennes entreprises a certainement été le lancement du logiciel applicatif VisiCalc. Conçu et réalisé par Dan Bricklin (né en 1951) et Bob Frankston (né en 1949), il a été commercialisé en 1979 pour Apple II. VisiCalc se servait de l’écran comme d’un tableau à deux dimensions avec des lignes et des colonnes de chiffres et de lettres. Premier logiciel tableur diffusé à large échelle, VisiCalc permettait de se déplacer dans la feuille de calcul, de saisir des nombres, des lettres, mais aussi des formules, et ainsi de faire injecter le résultat des calculs dans certaines cellules.

Ce que VisiCalc permettait était impossible sur un grand système. Pour fonctionner, VisiCalc devait pouvoir rafraîchir l’affichage très fréquemment, ce qui supposait qu’il puisse écrire les données à afficher directement dans la portion de la mémoire vive réservée à la mémoire vidéo de l’Apple II. Cette zone mémoire fait bien sûr partie de l’espace adressable par le processeur, approche qui n’est pas du tout envisageable sur un grand ordinateur de type mainframe.

Plus l’ordinateur peut répondre vite à une frappe clavier en ré-affichant l’écran, plus les interactions entre utilisateur et machine deviennent intimes et « naturelles ». La plupart des logiciels conçus au cours des années 1980 pour les ordinateurs personnels écrivaient directement dans la mémoire vidéo. Cela était possible car IBM avait rendu le standard matériel de son ordinateur PC disponible aux autres fabricants, ce qui permettait aux concepteurs d’applications de contourner le système d’exploitation pour accéder directement à ce matériel sans craindre de voir leur programme dysfonctionner ou ne pas fonctionner du tout sur le matériel d’un fabricant ou d’un autre. Si les compatibles PC avaient utilisé une infrastructure matérielle différente pour la partie vidéo, il aurait été impossible en termes économiques de créer des variantes des applications pour s’adapter aux différentes marques de compatibles PC.

Mais les applications devenant de plus en plus sophistiquées, des problèmes ont commencé à apparaître. Les logiciels qui avaient le plus de succès prenaient possession de la totalité de la surface de l’écran et implantaient toute une interface utilisateur, prenant notamment contrôle du clavier. Chaque application avait sa propre philosophie concernant l’interface utilisateur. Les techniques apprises pour cohabiter avec une application n’étaient d’aucun secours pour une autre. La coexistence entre les différents programmes était difficile. Pour passer d’une application une autre, il fallait souvent provoquer la fin d’exécution du premier avant de démarrer l’autre.

Pendant ce temps, une vision radicalement différente de l’informatique personnelle avait été élaborée et des prototypes existaient depuis plusieurs années au centre de recherche de Palo Alto, le PARC, fondé par la société Xerox en 1970 dans l’intention de lancer des produits qui permettraient à ce fabricant de photocopieurs d’attaquer le marché de l’informatique.

Le premier projet important créé au PARC fut la machine Alto dans les années 1972 et 1973. Quand on pense aux capacités standard de l’époque, il s’agissait d’une configuration matérielle impressionnante. L’unité à poser au sol était dotée d’un processeur 16 bits, de deux disques durs de 3 Mo et de 128 ko de mémoire vive extensible à 512 ko. Il n’existait pas encore de microprocesseur 16 bits sur une seule puce à l’époque, et l’Alto avait été conçu autour de plus de 200 circuits intégrés.

Un aspect particulièrement novateur de l’Alto était son affichage vidéo. L’écran avait un format proche de celui d’une feuille de papier A4 (8 pouces sur 10) dans le sens Portrait (contrairement aux écrans actuels). Il fonctionnait en mode graphique et offrait 606 pixels de large sur 808 pixels de haut, soit 489 148 pixels. À chaque pixel correspondait 1 bit, il s’agissait donc d’un affichage noir et blanc. La partie vidéo de la mémoire vive occupait un espace de 64 ko, et faisait partie de l’espace d’adressage du processeur.

En écrivant des valeurs dans cette mémoire vidéo, l’application pouvait afficher des images ou faire apparaître du texte avec un choix de styles et de tailles. Au lieu de ne constituer qu’un moyen visuel d’afficher du texte saisi au clavier, l’écran devenait un espace d’affichage d’informations en deux dimensions, et un lieu d’interaction avec l’utilisateur.

En effet, la machine Alto disposait déjà de ce petit périphérique appelé souris. Celle-ci devait rouler sur une table et possédait déjà trois boutons. Inventée par l’ingénieur et inventeur Douglas Engelbart (1925-2013) au centre de recherche de Stanford, cette souris permettait à l’utilisateur de l’Alto de déplacer un pointeur sur l’écran et d’interagir avec les objets affichés.

Au cours des années 1970, les programmes conçus alors pour l’Alto avaient permis de concrétiser des caractéristiques tout à fait intéressantes. Plusieurs programmes pouvaient être placés chacun dans une fenêtre et rester affichés en même temps. Les capacités graphiques permettaient de libérer l’imagination de l’utilisateur. C’est ainsi que sont apparus les premiers objets d’interface graphique tels que les boutons, menus, fenêtres et petites images appelées icônes. La souris a été utilisée pour sélectionner une fenêtre ou solliciter un objet graphique à l’écran et déclencher une fonction de programme.

Avec ce genre de logiciel, on quittait le monde rigide de l’interface de dialogue textuelle entre utilisateur et machine pour proposer une véritable intimité, faisant de l’ordinateur une extension de la pensée, bien plus qu’un simple automate calculateur. Vous pouviez envisager qu’un logiciel, comme le suggère le titre d’un article de Douglas Engelbart en 1963, augmente les capacités intellectuelles humaines.

Alto a été pionnier en matière d’interface utilisateur graphique GUI (Graphical User Interface) Une partie importante de l’effort conceptuel requis est à mettre au crédit d’Alan Kay (né en 1940). Hélas, Xerox avait décidé de ne pas commercialiser son Alto (le prix unitaire aurait d’ailleurs été décourageant, à plus de 30 000 dollars de l’époque). Plus d’une décennie allait s’écouler avant que les concepts novateurs de l’Alto soient réutilisés dans un produit qui allait connaître un grand succès.

C’est en effet en 1979 que Steve Jobs s’était rendu au centre de recherche PARC avec un collègue de Apple Computer, et avait été sérieusement impressionné par ce qu’il y avait vu. Il lui avait ensuite fallu trois années avant de présenter un premier ordinateur Apple avec une interface graphique. Il s’agissait du méconnu Apple Lisa lancé en janvier 1983… Mais un an plus tard Apple lançait son successeur qui a eu beaucoup plus de succès : le Macintosh.

Le premier Macintosh était construit autour d’un microprocesseur Motorola 68 000, avec 64 ko de mémoire morte ROM pour le système d’exploitation, 128 ko de mémoire vive RAM, un lecteur de disquettes 3,5 pouces d’une capacité de 400 ko, un clavier, une souris et un écran vidéo à affichage noir et blanc capable d’afficher 512 pixels en largeur et 340 pixels en hauteur. (L’écran ne mesurait que 9 pouces en diagonale.) Il y avait donc au total 175 104 pixels, chacun étant géré sur 1 bit. Il fallait donc réserver 22 ko de mémoire pour la mémoire vidéo.

La partie matérielle du premier Macintosh était bien pensée, mais pas vraiment révolutionnaire. Ce qui a constitué une rupture du Mac par rapport aux autres ordinateurs disponibles en 1984 est le système d’exploitation du Macintosh que certains appellent le logiciel système. Il a ensuite été connu sous le nom Mac OS, puis macOS.

Les systèmes d’exploitation mono-utilisateurs qui fonctionnent en mode texte, comme c’est le cas de CP/M, de MS-DOS ou Apple DOS, n’occupent pas beaucoup d’espace en mémoire et la plupart des fonctions de l’interface API concernent surtout le sous-système de fichiers. Dans le cas d’un système à interface graphique tel que Mac OS/macOS : il faut prévoir une taille bien supérieure et des centaines de fonctions API.

Un système d’exploitation en mode texte comme MS-DOS se contente de fournir une poignée de fonctions API pour afficher du texte, un peu comme avec un téléscripteur. Dans un système graphique tel que macOS, il faut pouvoir afficher toutes sortes de textes et de graphiques. En théorie, on pourrait se contenter d’une seule fonction permettant à l’application de définir la couleur d’un pixel individuel à une certaine coordonnée écran. Cette approche serait fastidieuse et rendrait les performances inacceptables.

Il est beaucoup plus logique de laisser le système d’exploitation prédéfinir tout un sous-système de programmation graphique, constitué à partir de fonctions pour calculer le tracé des lignes droites, des rectangles et des courbes, et afficher des caractères. Les traits doivent pouvoir être pleins, pointillés ou tiretés. Pour les rectangles et les ellipses, on doit pouvoir choisir un remplissage selon différents motifs. Pour les textes, on doit pouvoir choisir la police et la taille, et ajouter un effet de mise en gras ou de soulignement. Ce sous-système graphique prend en charge tout le travail de rendu des éléments graphiques sous la forme d’une série de pixels sur un écran.

Un logiciel utilisateur qui exploite les possibilités d’un système d’exploitation graphique peut utiliser les mêmes fonctions API pour afficher les graphiques à l’écran et pour les imprimer. Cela permet, par exemple, à un logiciel de traitement de texte de montrer le document sous le même aspect que le résultat qui va être imprimé, caractéristique nommée tel écran-tel écrit6.

Lorsque l’interface utilisateur graphique est intégrée au système d’exploitation, elle constitue une plate-forme commune harmonisant les interactions de l’utilisateur avec ses différentes applications. Pour ce faire, le système d’exploitation doit être doté des fonctions d’API permettant aux logiciels applicatifs de contrôler les composants du dialogue que sont les boutons et les menus déroulants. Pour l’utilisateur final, l’interface graphique est bien plus avenante qu’une interface en mode texte, mais c’est aussi un environnement plus productif pour le programmeur, qui peut ainsi équiper son application d’une interface utilisateur moderne sans avoir à en réinventer tous les rouages.

Avant même le lancement du Macintosh, plusieurs entreprises avaient commencé à travailler sur un système d’exploitation graphique pour l’IBM-PC et les compatibles. D’une certaine façon, les concepteurs chez Apple ont eu leur tâche facilitée parce qu’ils concevaient le matériel en même temps que le logiciel. Le système du Macintosh ne devait supporter qu’un seul modèle de lecteur de disquettes, un seul type d’écran d’affichage et seulement deux imprimantes. Pour concevoir un système d’exploitation à interface graphique pour l’IBM-PC, il fallait dès le départ prendre en charge un nombre énorme de matériels différents.

Par ailleurs, et bien que l’IBM-PC avait été lancé à peine quelques années auparavant en 1980, nombreux sont les utilisateurs qui ont eu le temps de s’habituer à leurs applications MS-DOS favorites en mode texte et n’étaient pas prêts à en changer. On pensait donc qu’il était crucial qu’un futur système d’exploitation à interface graphique pour PC reste capable de faire fonctionner toutes les anciennes applications pour MS-DOS (en plus de celles qui auraient été conçues pour le nouveau système). (Le Macintosh ne savait pas exécuter les logiciels de son prédécesseur Apple II, notamment parce qu’il n’avait pas le même microprocesseur.)

C’est en 1985 que la société Digital Research qui avait conçu CP/M a présenté son système GEM (Graphical Environment Manager). Parallèlement, VisiCorp (qui vendait VisiCalc) avait lancé VisiOn et Microsoft avait de son côté lancé Windows dans sa version 1.0. On avait vite deviné que c’était Windows qui allait gagner cette guerre entre environnements fenêtrés. Ce n’est pourtant qu’en 1990 lors du lancement de sa version 3.0 qu’il a commencé à intéresser un nombre d’utilisateurs conséquent, pour devenir en quelques années le système d’exploitation dominant pour les ordinateurs de bureau et portables. Malgré des similitudes dans l’apparence de leur interface utilisateur, Mac OS et Windows présentaient des interfaces de programmation API radicalement différentes.

L’interface utilisateur des tablettes et des telliphones (« smartphones ») est une autre histoire. Les besoins du dialogue graphique de ces équipements sont proches de ceux des ordinateurs personnels, mais il y a suffisamment de différences pour que l’interface API soit spécifique. De nos jours, le marché des systèmes d’exploitation pour téléphones et tablettes est dominé par les deux ténors que sont Google avec Android et Apple avec iOS.

La plupart des utilisateurs d’ordinateurs ne s’en rendent pas compte, mais l’influence et la contribution du système d’exploitation UNIX restent très importantes. UNIX a été développé au début des années 1970 dans les laboratoires Bell Telephone Laboratories par Ken Thompson (né en 1943) et Dennis Ritchie (1941-2011), entourés de certaines des plus fines lames de la programmation système. Le nom du système est un jeu de mots acronymique : au départ, UNIX devait être une variante simplifiée d’un système existant nommé Multics (Multiplexed Information and Computing Services) créé par les Bell Labs avec le MIT et la société GE.

Les programmeurs chevronnés adorent UNIX. Alors que la plupart des systèmes d’exploitation sont destinés à des machines en particulier, UNIX a dès sa conception été pensé portable, ce qui l’a rendu facile à reformuler pour fonctionner sur des machines très diverses.

Au moment de la création du système UNIX, les laboratoires Bell Labs appartenaient à AT & T (American Telephone & Telegraph). Ils ont de ce fait subi les décisions judiciaires qui visaient à supprimer la situation de monopole de AT & T dans le secteur de la téléphonie. Dans un premier temps, il a été interdit à AT & T de faire la promotion d’UNIX ; elle a été forcée d’autoriser les concurrents à utiliser son système sous licence. C’est ainsi qu’au début de 1973, UNIX s’est trouvé implanté massivement dans les universités, les entreprises et l’administration. Ce n’est qu’en 1983 que AT & T a été autorisée à opérer à nouveau librement et donc à commercialiser sa propre version d’UNIX.

La conséquence est qu’il existe plusieurs variantes d’UNIX portant des noms différents, conçus pour des matériels différents et vendus par des sociétés différentes. Beaucoup de monde est intervenu dans l’évolution de UNIX en laissant des traces. Une certaine philosophie UNIX et quelques lignes de force ont pourtant été préservés par ceux qui ajoutaient des briques au système UNIX.

Un concept fondateur d’UNIX est celui de fichier texte. De nombreux logiciels utilitaires sous UNIX fonctionnent sur ligne de commande : ils lisent le contenu d’un fichier texte, lui appliquent un traitement et stockent le résultat dans un autre fichier texte. Ces outils UNIX peuvent s’alimenter les uns les autres, le tout formant une chaîne de traitement spécifique.

Les dernières évolutions majeures concernant UNIX ont été la création de la fondation du logiciel libre FSF (Free Software Foundation) avec son projet de système GNU, tous deux apparus sous l’impulsion de Richard Stallman (né en 1953). GNU (qui se prononce « gueux-nu ») signifie « GNU’s Not UNIX », ce qui est vrai puisque GNU est censé rester compatible avec UNIX tout en étant diffusé dans des conditions juridiques qui interdisent que quiconque puisse en tirer profit. Le projet GNU a vu éclore de nombreux outils compatibles UNIX, mais surtout tout un noyau de système d’exploitation nommé Linux, qui incarne l’essentiel d’un système compatible UNIX.

Conçu et réalisé principalement par le programmeur finnois Linus Torvalds (né en 1969), Linux est devenu vraiment populaire. C’est ainsi que le système Android qui équipe la plupart des telliphones est basé sur un noyau Linux ; de même, les supercalculateurs actuels fonctionnent tous sous Linux, sans compter une majorité de serveurs Web sur Internet.

Mais Internet mérite d’y consacrer un chapitre entier, et ce sera le dernier du livre.

1 N.d.T. : si nous choisissions de franciser ces commandes, nous aurions A pour affichage, E pour écriture et X pour exécution.

2 N.d.T. : même en français, les mots bootstrap et boot sont devenus courants. Nombreux sont ceux qui « reboutent » leur machine récalcitrante. En ancien français, bouter avait le sens de faire germer ou de démarrer un feu.

3 N.d.T. : le terme invite suggère que le système est en attente et invite l’utilisateur à lui soumettre une commande. Un autre terme candidat en français est incite.

4 N.d.T. : les premiers modèles de PC étaient livrables avec un ou deux lecteurs de disquettes, donc associés aux deux lettres A et B. Lorsqu’un disque dur a été ajouté, il a reçu la lettre C. C’est pour cela que les disques durs de tous les ordinateurs sous Windows de nos jours correspondent à l’unité de disque C.

5 N.d.T. : on conserve souvent le sigle anglais UI (User Interface) et sa variante GUI (Graphical User Interface).

6 N.d.T. : en anglais, WYSIWYG pour What you see is what you get, « ce que vous obtiendrez sera fidèle à ce que vous voyez ». Cette expression a été à l’origine rendue célèbre par le comédien américain Flip Wilson, travesti en Geraldine Jones dans les années 1970.

Chapitre 27  Langages

Tous les ordinateurs fonctionnent en exécutant du code machine, mais écrire un programme en utilisant le code machine équivaut à manger avec un cure-dents. Chaque bouchée est tellement minuscule que le repas en devient interminable. Les octets qui constituent les codes des opérations réalisent les tâches les plus élémentaires qu’il soit : charger un octet d’une valeur numérique depuis la mémoire vers un registre du processeur, faire l’addition avec une valeur et stocker le résultat en mémoire. On a bien du mal à croire que ces micro-actions finiront par produire quoi que ce soit d’intéressant pour un humain.

Nous avons fait quelques progrès par rapport à cet état des choses primitif. Au début du chapitre précédent, nous avions employé des interrupteurs pour saisir les données binaires et les stocker en mémoire. Nous avions ensuite découvert que nous pouvions utiliser un clavier et un écran vidéo pour saisir et visualiser les valeurs hexadécimales des octets de code machine. C’était déjà une amélioration sensible, mais ce n’était qu’une première étape.

Vous savez que les octets des codes machine sont associés à des mnémoniques généralement sur trois lettres, comme MOV, ADD, JMP et HLT. Il devient ainsi possible de décrire un programme en code machine de façon quelque peu symbolique avec ce qui ressemble à de l’anglais abrégé. La plupart des mnémoniques sont accompagnées d’opérandes qui constituent les valeurs sur lesquelles le code doit travailler. Par exemple, sur 8080, le code 46h demande au microprocesseur de stocker dans le registre B l’octet qui se trouve à l’adresse mémoire indiquée par une valeur sur 16 bits qui se trouve dans la paire de registres H et L. Cette longue phrase correspond à cette écriture plus élégante et compacte :

MOV B,M

(Le M signifie « mémoire ».) La collection de mnémoniques avec quelques fonctions complémentaires constitue un langage de programmation particulier : le langage assembleur. Écrire un programme en assembleur est déjà beaucoup plus facile qu’en code machine. Il y a cependant un problème : le processeur ne comprend pas ce langage assembleur.

À l’époque des pionniers de l’informatique, on passait beaucoup de temps à la rédaction des programmes en langage assembleur sur papier. Après une relecture soignée, on pouvait considérer que le programme allait fonctionner, au point de pouvoir passer à la phase d’assemblage manuel. Ce travail de traduction consistait à partir des instructions en assembleur pour trouver les octets des codes machine correspondants en s’appuyant sur un tableau ou une liste de référence. Il n’y avait ensuite plus qu’à entrer ces valeurs dans la mémoire.

Ce qui est le plus difficile dans cette activité d’assemblage manuel est la gestion des sauts et des appels. Pour réussir à traduire manuellement une instruction JMP ou CALL, il faut connaître l’adresse mémoire de la destination, mais vous ne pouvez la connaître qu’une fois toutes les autres instructions en code machine insérées. Il serait bien préférable de laisser l’ordinateur faire la conversion à votre place. Mais comment ?

Le premier outil bienvenu est un éditeur de texte, c’est-à-dire un logiciel dans lequel vous pouvez saisir des lignes de texte, puis les enregistrer dans un fichier. (Malheureusement, il vous faudra assembler ce premier programme d’édition manuellement.). Un éditeur vous permettra de créer les fichiers texte contenant vos instructions en langage assembleur, c’est-à-dire des fichiers de code source. Il vous faudra néanmoins assembler à la main un autre outil qui est le programme assembleur. Ce logiciel lit le contenu d’un fichier texte contenant des instructions en assembleur et convertit ces instructions en codes machine qu’il sauvegarde dans un autre fichier. Le contenu de ce fichier binaire peut ensuite être directement chargé en mémoire ; on peut alors en lancer l’exécution.

Si vous travaillez, par exemple, sur un ordinateur basé sur un 8080 fonctionnant sous le système d’exploitation CP/M, ces outils sont déjà disponibles. L’éditeur de texte porte dans ce cas le nom ED. COM et permet de créer et modifier un fichier texte. (De nos jours, vous disposez de Notepad/Bloc-notes sous Windows ou de TextEdit sous macOS.) Supposons que vous ayez créé un fichier texte portant le nom PROGRAM1. ASM. Les trois lettres ASM forment l’extension de nom de fichier pour du contenu de type code source en assembleur. Voici ce pourrait contenir ce fichier :

       ORG 0100h
       LXI DE,Texte
       MVI C,9
       CALL 5
       RET
Texte: DB ’Salut !$’
       END

Vous avez sans doute détecté quelques mnémoniques inconnues. Tout d’abord, nous trouvons l’instruction ORG qui signifie origine. Ce n’est pas une instruction du processeur, 8080, mais une directive qui demande de positionner l’adresse de la prochaine instruction à 0100h. C’est en effet à cette adresse que le système CP/M implante le programme à exécuter juste avant son démarrage.

La nouvelle instruction LXI (Load eXtended Immediate) effectue le chargement d’une valeur sur 16 bits dans la paire de registres DE. C’est une des instructions 8080 que je n’ai pas reproduites dans mon processeur pédagogique. La valeur sur 16 bits est fournie sous la forme du label Texte qui est défini plus bas dans le code source, juste devant une instruction DB que nous ne connaissons pas encore non plus. Cette instruction peut être suivie de plusieurs octets séparés par des virgules ou bien par une série de lettres ou de chiffres entre deux apostrophes simples. C’est ce second format qui est choisi ici.

Une instruction MVI que nous connaissons déjà stocke la valeur 9 dans le registre C. Nous voyons ensuite un appel CALL 5 en direction du système CP/M. Le système consulte le contenu du registre C et saute vers sa fonction correspondante pour afficher une chaîne de caractères. Le contenu de la chaîne commence à l’adresse spécifiée dans la paire de registres DE. L’affichage se termine lorsque le caractère détecté est un signe dollar $. (Vous pouvez voir que le texte dans l’avant-dernière ligne du programme se termine effectivement par un signe dollar qui marque la fin de chaîne.) Le choix du symbole $ comme marqueur de fin n’est pas très heureux, mais c’est ce qui avait été décidé par les créateurs de CP/M. La dernière instruction END marque la fin du fichier assembleur.

Notre fichier texte est donc constitué de sept lignes qu’il s’agit d’assembler, ce qui se fait dans CP/M au moyen de l’outil ASM. COM. Il s’agit d’un assembleur CP/M. Pour l’utiliser, vous émettez la ligne de commande suivante dans le système CP/M :

ASM PROGRAM1.ASM

L’outil ASM va analyser le contenu du fichier PROGRAM1. ASM et générer s’il ne trouve pas d’erreur dans un fichier portant le nom PROGRAM1. COM. Ce fichier contient la séquence de codes machine correspondant exactement aux instructions en assembleur. (Il y a en réalité une autre étape dans le processus, mais cela n’a pas d’impact dans notre description de l’enchaînement des événements.)

Si on ouvre le fichier PROGRAM1. COM, on y trouve les 16 octets suivants :

11 09 01 0E 09 CD 05 00 C9 53 61 6C 75 74 21 24

Les trois premiers octets correspondent à l’instruction LXI avec ses paramètres. Les deux octets suivants concernent MVI, les trois suivants l’instruction d’appel CALL suivie de l’instruction de retour RET. Les sept derniers octets sont les caractères ASCII des cinq lettres du mot Salut, le signe point d’interrogation et le signe dollar de fin de chaîne. Vous pouvez lancer l’exécution du programme depuis la ligne de commande CP/M ainsi :

PROGRAM1

(Inutile d’ajouter l’extension.) Cette commande demande au système d’exploitation de charger ce programme en mémoire et d’en démarrer l’exécution. Sous très peu de temps, vous voyez apparaître à l’écran le message suivant :

Salut !

L’outil logiciel ASM. COM utilise donc en entrée un texte écrit en langage assembleur que l’on appelle code source et produit en sortie un fichier contenant du code machine, ou code exécutable. Les programmes en langage assembleur sont extrêmement détaillés puisqu’ils se fondent sur une correspondance un à un entre des mnémoniques en assembleur et des codes machine. Le travail de traduction de l’assembleur consiste à analyser chaque ligne de texte pour y repérer le mnémonique et les paramètres, en consultant une liste de tous les mnémoniques et paramètres possibles. Ce processus d’analyse (parsing) utilise un grand nombre d’instructions de comparaison CMP suivies de sauts conditionnels. Ces comparaisons permettent de déterminer quelle instruction en code machine correspond à chaque instruction en assembleur.

La séquence d’octets que contient le fichier PROGRAM1. COM commence par la valeur 11h qui est le code de l’instruction LXI. Les 2 octets suivants valent 09h et 01h, ce qui forme l’adresse 0109h. Le programme assembleur a calculé cette adresse à votre place : puisque l’instruction LXI est positionnée à l’adresse 0100h (la première adresse à laquelle le système CP/M implante un programme en mémoire avant d’exécuter), l’adresse 0109h désigne le début de la chaîne de texte. En général, le programmeur n’a pas besoin de se soucier des adresses associées aux différentes portions de son programme assembleur.

Bien sûr, le concepteur du premier programme assembleur avait été obligé d’assembler ce programme à la main. Lorsqu’il s’agit d’écrire un nouvel outil assembleur pour une machine qui en possède déjà un, on peut se servir de ce premier outil assembleur pour assembler l’autre. Une fois que le nouvel assembleur a été assemblé, il peut s’assembler lui-même !

Lorsqu’un nouveau microprocesseur est commercialisé, il est nécessaire de l’équiper d’un nouveau outil assembleur qui peut être conçu et réalisé sur un ordinateur existant avec l’assembleur de cet autre ordinateur. C’est l’opération de cross-assemblage. L’outil assembleur fonctionne sur l’ordinateur A mais génère du code destiné à fonctionner sur l’ordinateur B.

L’outil assembleur élimine donc les aspects les plus laborieux de la programmation en langage assembleur, celle qui correspond à l’assemblage manuel, mais il occasionne néanmoins de sérieux problèmes. Vous avez certainement deviné que le premier est que la programmation assembleur reste fastidieuse. Vous travaillez en effet au niveau du processeur et vous devez vous charger du moindre détail.

L’autre problème important est que le langage assembleur n’est pas portable d’une machine à une autre. Si vous créez un programme en assembleur pour le processeur Intel 8080, vous ne pouvez pas exécuter ce programme sur un Motorola 6800. Il faudra réécrire tout le programme dans l’assembleur 6800. L’effort sera moindre que celui qu’il a fallu déployer pour concevoir le programme au départ parce que vous aurez déjà résolu les principaux soucis de structuration et d’algorithmique. Cela reste cependant un travail conséquent.

L’essentiel des traitements que réalise un ordinateur relève de l’arithmétique, mais la façon dont ces opérations sont réalisées en assembleur est inefficace au possible. Il serait préférable de pouvoir spécifier les opérations mathématiques en recourant à une notation algébrique éprouvée, comme ceci :

Angle = 27.5
Hypotenuse = 125.2
Hauteur = Hypotenuse × Sinus(Angle)

Si ces trois lignes faisaient partie du code source d’un programme, chacune d’elles correspondrait à une instruction. Comme en algèbre, les noms dans un programme, tels que Angle, Hypotenuse et Hauteur, sont des variables, donc dont la valeur peut changer. Le signe d’égalité correspond à l’opération d’affectation de valeur : dans la première ligne, l’opération est une copie de la valeur 27.5 dans la variable nommée Angle. De même pour la variable Hypotenuse qui reçoit la valeur 125.2 (pas d’accent dans le nom !). Enfin, le mot Sinus est celui d’une fonction. Le programme contient quelque part le bloc de code permettant de calculer le sinus trigonométrique d’un angle et renvoyant la valeur calculée.

Vous aurez remarqué que les deux valeurs numériques citées ne sont pas des valeurs entières, mais des valeurs fractionnaires, utilisant le point décimal1.

Si l’on stocke ces trois instructions dans un fichier texte, on devrait pouvoir concevoir un outil assembleur capable de lire ce fichier et de convertir les expressions algébriques en codes machine pour réaliser les calculs. Qu’est-ce qui nous en empêcherait ?

Répondre à ce besoin revient à découvrir le concept de langage de programmation de haut niveau. Le langage assembleur est de bas niveau, tout simplement parce qu’il reste très proche des détails du matériel de l’ordinateur. On considère comme de haut niveau tous les langages plus symboliques que l’assembleur, mais ils le sont à des degrés divers. Imaginez le président d’une multinationale qui se tourne vers son ordinateur et lui dicte ses volontés dans le style suivant : « Prépare-moi le compte d’exploitation de l’année, rédige le rapport financier annuel, demande une impression en quelques milliers d’exemplaires et envoie une copie à chacun de nos actionnaires. » Si la machine pouvait répondre à ce besoin, ce serait par un langage à vraiment très haut niveau. Dans le monde actuel, les langages de programmation (les infolangages) sont encore loin d’atteindre ce niveau d’abstraction.

Les langues humaines sont le fruit de milliers d’années d’évolution plus ou moins aléatoire et d’adaptation. Même les langues synthétiques telles que l’espéranto s’appuient sur des langues réelles. Les infolangages de haut niveau n’ont pas un héritage aussi important. Concevoir un nouveau langage de programmation intéresse un certain nombre de personnes, parce que le langage détermine la façon dont on exprime des instructions pour une machine. Lors de la première édition de ce livre, j’avais trouvé une estimation datant de 1993 selon laquelle plus de mille langages de haut niveau sont apparus depuis le début des années 1950. À la fin de l’année 2021, un site Web historique (hopl.info) déclare en avoir inventorié 8 945.

Il ne suffit évidemment pas d’en rester à la théorie consistant à définir les concepts d’un nouveau langage en précisant une syntaxe pour exprimer des besoins. Il faut également concevoir concrètement un outil de compilation, c’est-à-dire un logiciel qui convertit les instructions depuis le code source de haut niveau vers du code machine. Comme dans le cas de l’assembleur, le compilateur doit lire le contenu du fichier source un caractère à la fois pour trouver les différents constituants que sont les mots-clés, les symboles et les valeurs numériques. Un compilateur est en revanche beaucoup plus complexe qu’un outil assembleur. Il ne profite plus de cette équivalence 1-vers-1 entre les instructions assembleur et les instructions en code machine. Dans le cas d’un compilateur, une instruction en langage de haut niveau va produire un grand nombre d’instructions en code machine. Cela explique pourquoi créer un compilateur n’est pas facile. Des livres entiers ont été consacrés à la conception et la réalisation des compilateurs.

Les langages de haut niveau ont des avantages et des inconvénients. Le principal avantage est qu’ils sont plus faciles à apprendre et à utiliser que l’assembleur. Un programme écrit dans un langage de haut niveau est normalement plus facile à lire et plus compact. Il est souvent portable, c’est-à-dire qu’il ne dépend pas d’un type de processeur, alors que c’est le cas de l’assembleur. Le programmeur n’a plus à se soucier des détails de la structure matérielle sur laquelle le programme va être exécuté. Bien sûr, pour utiliser le même programme sur des processeurs différents, il faut disposer d’un compilateur par type de machine afin de générer le code machine approprié. Le fichier exécutable reste différent selon le processeur.

En revanche, il est reconnu qu’un bon programmeur en langage assembleur saura produire un programme plus rapide et plus efficace que ce que permet un compilateur. Autrement dit, le fichier exécutable résultant du travail d’un compilateur à partir d’un langage de haut niveau sera plus volumineux et moins performant qu’un programme absolument identique au niveau des fonctions, mais écrit en assembleur. (Au cours des dernières années, cette différence de performances est devenue moins sensible du fait que les microprocesseurs se sont sophistiqués, et les compilateurs ont acquis des capacités d’optimisation du code.)

Bien qu’un langage de haut niveau facilite la programmation, cela ne rend pas le programme plus puissant. D’ailleurs, certains langages de haut niveau ne permettent pas de réaliser certaines opérations habituelles sur les processeurs, et notamment les décalages entre bits et les tests de bits unitaires. Ces tâches sont donc impossibles ou plus difficiles à réaliser avec un langage de haut niveau.

Lors des premières années de l’informatique personnelle, les programmes applicatifs étaient principalement écrits en assembleur. De nos jours, l’assembleur n’est que rarement utilisé, et seulement pour des besoins précis. En effet, dorénavant, les processeurs sont dotés d’un mécanisme de canaux d’exécution pipelines permettant d’exécuter plusieurs instructions simultanément. De ce fait, écrire en assembleur un programme utilisant ces possibilités devient vraiment difficile. Et pendant ce temps, les compilateurs sont devenus de plus en plus sophistiqués. Enfin, les capacités de stockage disque et l’espace mémoire disponible sont devenus tellement importants de nos jours que les programmeurs sont moins poussés à écrire du code compact, occupant un faible espace mémoire. Dans le passé, un défi habituel était de faire tenir le fichier exécutable sur une seule disquette.

Les concepteurs des tout premiers ordinateurs avaient tenté d’exprimer les solutions à leurs problèmes au moyen d’une notation algébrique. Le premier compilateur vraiment opérationnel a été de l’avis général le A-0 (Arithmetic Language version zero). Il avait été créé pour la machine UNIVAC par Grace Murray Hopper (1906-1992) chez Remington-Rand en 1952. C’est elle aussi qui a inventé le terme compilateur. Elle avait fait ses premiers pas avec les ordinateurs en travaillant pour Howard Aiken sur le Mark I en 1944. Elle était toujours active dans le secteur de l’informatique à 80 ans passés, s’occupant des relations publiques pour la société DEC (Digital Equipment Corporation).

© Getty Images

Le plus ancien langage de haut niveau toujours utilisé de nos jours, même s’il a bénéficié de nombreuses évolutions, se nomme FORTRAN. Dans les premiers temps des infolangages, on avait par tradition choisi de les baptiser au moyen de demi-acronymes. Par exemple, FORTRAN est la contraction des trois premières lettres de FORmule et des quatre premières lettres de TRANslation. C’est un translateur de formules mathématiques. Conçu chez IBM pour la série d’ordinateurs 704 au milieu des années 1950, FORTRAN est resté très longtemps le langage de prédilection des scientifiques et des ingénieurs. Il est en effet doté de fonctions de traitement des valeurs flottantes très complètes. Il permet même de travailler avec les nombres complexes (combinant une partie réelle et une partie imaginaire).

Un autre ancien langage de programmation toujours très utilisé de nos jours, mais cette fois-ci plutôt dans le monde de la gestion et des finances, est COBOL, agrégation de COmmon Business Oriented Langage. Il avait été conçu en 1959 par un comité réunissant des délégués de l’industrie américaine et du département de la Défense des États-Unis, et avait bénéficié de l’influence des premiers compilateurs de Grace Hopper. Un des objectifs de COBOL était de permettre aux responsables d’entreprise, sans qu’ils soient capables d’écrire un programme, de comprendre les grandes lignes de ce que faisait un programme en lisant le code source (en réalité, cela n’était que rarement réalisable).

Un autre langage, qui n’est lui plus vraiment utilisé, sauf par quelques passionnés, est ALGOL (ALGorithmic Langage). Il faut savoir qu’ALGOL est également le nom de la seconde étoile la plus brillante dans la constellation de Persée. Conçu au départ par un comité international entre 1957 et 1958, ALGOL est l’ancêtre de nombreux langages actuels à usage général apparus au cours des cinquante dernières années. Il a inauguré un concept fondamental, celui de programmation structurée. Même de nos jours, certains qualifient un certain nombre de langages de programmation comme d’inspiration ALGOL.

ALGOL a établi des façons de rédiger le code source qui sont dorénavant en vigueur dans pratiquement tous les langages de programmation. Il s’agit notamment des mots-clés ou mots réservés, qui sont des mots ayant un seul sens pour construire une tournure opérationnelle. Plusieurs instructions peuvent être regroupées dans des blocs, et un bloc peut être exécuté ou non en fonction d’une condition ou pour un nombre de fois défini.

L’instruction nommée if n’exécute le bloc ou l’instruction qui en dépend que si la condition logique associée est remplie, par exemple, si une variable nommée Hauteur vaut moins de 55. L’instruction for permet d’exécuter une instruction ou un bloc d’instructions de façon répétée, en général en incrémentant une variable à chaque tour. Un tableau (array) réunit plusieurs valeurs du même type, par exemple les noms d’une série de villes. Tout programme est organisé en blocs et en fonctions.

Des versions de FORTRAN, de COBOL et d’ALGOL ont été créés pour les ordinateurs personnels, mais aucun de ces langages n’a eu autant d’impact sur les ordinateurs domestiques que le langage BASIC.

BASIC signifie Beginner’s All-purpose Symbolic Instruction Code, soit « langage symbolique à usage général pour débutants ». Il a été conçu en 1964 par John Kemeny et Thomas Kurtz du département de mathématiques de Dartmouth en utilisant le système informatique en temps partagé de Dartmouth. La plupart des étudiants de cette université n’étaient nullement experts en mathématiques ou en ingénierie. On ne pouvait donc pas espérer qu’ils plongent sans appréhension dans la complexité d’un ordinateur si la syntaxe du langage était obscure. Grâce au langage BASIC, un étudiant assis devant un terminal pouvait saisir des instructions en les faisant commencer par un numéro pour indiquer l’ordre des instructions, comme une sorte de numérotation de lignes. Le premier programme en BASIC qui a été publié dans le premier manuel d’instruction du langage était celui-ci :

 10 LET X = (7 + 8) / 3
 20 PRINT X
 30 END

La plupart des outils de création pour le langage BASIC qui sont apparus sont des interpréteurs et non des compilateurs. Alors qu’un compilateur lit la totalité du contenu du fichier de code source, puis génère la totalité du code machine qu’il stocke dans un fichier exécutable, l’interpréteur lit le code source une ligne à la fois et lance l’exécution du code correspondant sans jamais créer de fichier exécutable. Il est plus facile d’écrire un interpréteur qu’un compilateur, mais les performances de l’exécution d’un programme interprété sont bien plus faibles que celles d’un programme compilé. Sur les ordinateurs personnels, le langage BASIC est devenu prédominant parce que deux collègues, Bill Gates (né en 1955) et Paul Allen (1953-2018), avaient eu la bonne idée de créer un interpréteur BASIC pour la machine Altair 8800 en 1975, ce qui leur avait permis de fonder leur société, Microsoft Corporation, puis d’adapter leur BASIC pour les PC.

Le langage de programmation Pascal a hérité l’essentiel de sa structure du langage ALGOL, en y ajoutant quelques caractéristiques de COBOL. Conçu à la fin des années 1960 par le professeur de sciences informatiques suisse Niklaus Wirth (né en 1934), Pascal a été très populaire chez les premiers programmeurs pour IBM-PC, dans une version tout à fait originale : Turbo Pascal, conçu par la société Borland du Français Philippe Kahn en 1983, était vendu une bouchée de pain (environ 50 dollars). Ce Turbo Pascal avait été écrit par un étudiant danois nommé Anders Hejlsberg (né en 1960) et il était livré avec un environnement de développement intégré composé d’un éditeur de texte pour le code source et d’un compilateur, ce qui permettait de progresser très vite dans la programmation. Ce genre d’environnement de développement existait déjà sur les grands ordinateurs, mais c’est Turbo Pascal qui a été pionnier pour ce genre d’outils sur ordinateur personnel.

Pascal a influencé Ada, un langage très robuste et sérieux puisque conçu à la demande du département de la Défense des États-Unis. Le nom du langage rend hommage à Augusta Ada Byron dont nous avons parlé dans le Chapitre 15, puisque c’est elle qui avait écrit le fameux article décrivant le moteur analytique de Charles Babbage.

Et un jour est apparu le très populaire langage C. Il a été élaboré entre 1969 et 1973 par Dennis M. Ritchie (1941-2011) dans les laboratoires de Bell Telephone. Certains se demandent pourquoi il porte un nom constitué d’une seule lettre. La réponse est très simple : ce nom dérive d’un langage antérieur qui s’appelait B, une version simplifiée de BCPL (Basic CPL), lui-même dérivé de CPL (Combined Programming Language).

La plupart des langages de programmation font tout pour éliminer le moindre indice de langage assembleur, et notamment les instructions pour manipuler les adresses mémoire. Ce n’est pas le cas du langage C. Ce dernier permet d’utiliser des pointeurs, c’est-à-dire des adresses mémoire. Pour un programmeur chevronné qui sait comment bien les utiliser, les pointeurs sont extrêmement pratiques, mais ils sont très dangereux pour tous les autres. Avec un pointeur, vous pouvez écrire dans des zones mémoire de façon imprévue, et les pointeurs constituent une source d’erreurs très commune. Il y a même un programmeur, Alan I. Holub qui avait écrit un livre à propos du langage C en lui donnant un titre évocateur (en français, cela donnerait : « Assez de corde de pendu pour vous tirer vous-même une balle dans le pied »).

Le C est devenu le grand-père de toute une série de langages moins risqués à utiliser, qui lui ont ajouté la possibilité de travailler avec des objets (programmation orientée objets), c’est-à-dire des entités du langage réunissant sous un seul nom un bloc de code et des données de façon très structurée. Le plus connu de ces langages se nomme C++. Il avait été créé par le scientifique danois Bjarne Stroustrup (né en 1950). Dès 1985 est apparu un autre descendant orienté objets : Java, conçu chez Oracle Corporation par James Gosling (né en 1955). Il y eut ensuite le C#, conçu au départ par Anders Hejlsberg chez Microsoft en 2000. Au moment d’écrire ce livre, un des langages de programmation les plus répandus est un autre descendant du C qui porte le nom Python. Il a été conçu en 1994 par le programmeur hollandais Guido van Rossum (né en 1956). Lorsque vous relirez ce livre dans les années 2030 ou 2040, vous aurez sans doute vu apparaître de nouveaux langages !

Les langages de haut niveau incitent parfois le programmeur à adopter un schéma de pensée original. C’est ainsi que certains de ceux apparus récemment mettent l’accent sur la manipulation des fonctions plutôt que sur celle des variables. Il s’agit de langages de programmation fonctionnelle. Un programmeur habitué à l’approche classique de la programmation procédurale peut être perturbé par cette nouvelle approche. Pourtant, ces nouvelles perspectives constituent des opportunités de revoir de fond en comble la façon dont on cherche à résoudre un problème logiciel. Ceci dit, quel que soit le langage, le processeur ne sait toujours exécuter que les bonnes vieilles instructions en code machine qui ont été prévues et « gravées dans le silicium » lors de sa conception.

Il est possible de rendre moins gênantes les différences entre marques de processeurs et, donc, codes machine. Il existe des logiciels pour émuler un processeur avec un autre, ce qui permet notamment de continuer à utiliser d’anciens logiciels et jeux vidéo sur les machines modernes. (Il n’y a rien de neuf là-dedans : Bill Gates et Paul Allen avaient choisi d’écrire leur interpréteur BASIC pour l’Altair 8800 en réalisant les tests sur un émulateur pour Intel 8080 qu’ils avaient eux-mêmes conçu sur un mini-ordinateur PDP-10 de l’université de Harvard.) Java et C# sont deux langages disposant d’un compilateur particulier : il sait produire en sortie un pseudo-code exécutable appelé octocode (bytecode), qui n’est pas exécutable tel quel, mais qui est converti beaucoup plus rapidement et à la volée par un moteur d’exécution. Le projet LLVM, également, procure une liaison virtuelle entre n’importe quel langage de haut niveau et n’importe quel jeu d’instructions de processeur.

Tout cela est le fruit de la magie du logiciel. Lorsque l’on dispose d’assez de mémoire et de bonnes performances, n’importe quel ordinateur peut faire tout ce qu’un autre ordinateur sait faire. C’est une conséquence des recherches d’Alan Turing sur la calculabilité dans les années 1930.

Ce que les travaux de Turing ont également prouvé, c’est que certains problèmes algorithmiques seront à jamais inaccessibles à l’informatique ; un de ces problèmes a des conséquences énormes : vous ne pourrez jamais créer un programme permettant d’en tester un autre au point de garantir que cet autre programme est libre de toute erreur et fonctionne parfaitement ! Autrement dit, nous ne pouvons jamais être certains que nos programmes fonctionnent comme nous l’avions prévu.

Ce constat laisse un sentiment amer, et explique pourquoi il est si important dans tout projet de développement logiciel de prévoir d’importants efforts de test et de mise au point ou débogage.

Un des langages influencés par le C qui a rencontré un grand succès se nomme JavaScript. C’est grâce à JavaScript, créé au départ par Brendan Eich (né en 1961) chez Netscape en 1995, que les pages Web ne sont pas purement statiques et limitées aux affichages de texte et d’éléments graphiques en langage HTML. Au moment où j’écris ceci, 98 % des 10 millions de sites Web les plus visités utilisent du code JavaScript.

Tous les principaux navigateurs Web reconnaissent le langage JavaScript. Cela signifie que vous pouvez créer du code JavaScript sans avoir à installer au préalable un quelconque outil de programmation2.

Pourquoi ne pas expérimenter un peu en écrivant du JavaScript ?

Il suffit en effet de créer un fichier au format HTML qui va contenir un peu de code JavaScript. Pour créer le fichier, on peut utiliser le Bloc-notes de Windows ou l’éditeur de texte TextEdit de macOS. On stocke le code source dans le fichier puis on ouvre le fichier dans un navigateur Web, par exemple Edge, Chrome ou Safari3.

Si votre machine fonctionne sous Windows, utilisez l’utilitaire Bloc-notes. (Si vous ne le trouvez pas dans les menus, servez-vous du programme de données de l’outil de recherche dans le menu Démarrer. Cherchez le nom Bloc-notes ou le nom de fichier Notepad.) Dès que le programme démarre, vous êtes prêt à saisir.

Sous macOS, utilisez le programme TextEdit. Il faudra peut-être chercher avec la fonction de recherche Spotlight. Au démarrage du programme, cliquez si besoin sur le bouton Nouveau document. Il faut savoir que TextEdit produit par défaut un fichier enrichi avec des informations de mise en forme dont vous n’avez pas besoin et qui perturberait le travail des outils d’analyse de code. Il faut produire un fichier texte pur. Accédez aux préférences et vérifiez les réglages. Le format sélectionné doit être Texte. En bas de la boîte, vérifiez que les trois options relatives à l’orthographe et à la grammaire sont inactives. Validez pour refermer la boîte. Ouvrez ensuite le menu Édition et le sous-menu Orthographe et grammaire puis désélectionnez si nécessaire toutes les options servant à corriger l’orthographe.

Vous pouvez ensuite saisir exactement les quelques lignes suivantes :

<html>
    <head>
        <title>Mon JavaScript</title>
    </head>
<body>
    <p id="resultat">Résultats du programme ici !</p>
        <script>
            // Code JavaScript ici
        </script>
    </body>
</html>

Il s’agit d’un fichier au format HTML, utilisant des balises pour délimiter les différentes régions du fichier. Tout au début du fichier, nous trouvons une balise <html> et sa complémentaire </html> tout à la fin. Le premier sous-niveau correspond à la section d’en-tête qui commence par <head> et contient elle-même une balise <title> pour ce qui doit apparaître dans la barre de titre de la fenêtre du navigateur. La section <body> contient une balise de paragraphe <p> qui délimite le texte affiché dans la page, dans notre exemple « Résultats du programme ici ».

La section du corps <body> contient également une section <script>. C’est à cet endroit que vous allez ajouter des instructions en langage JavaScript. Pour l’instant, cette partie ne contient qu’une ligne qui commence par les deux barres obliques //. Elles sont les marqueurs d’un commentaire monoligne. Tout ce qui suit ces deux barres jusqu’à la fin n’a d’intérêt que pour les humains qui vont lire le code source. L’exécution de JavaScript ne tient absolument pas compte de ce qui suit le délimiteur jusqu’à la fin de la ligne.

En théorie, vous pourriez saisir la totalité des instructions sans ajouter de retrait par rapport à la marge gauche ; vous pourriez même tout accumuler sur une seule ligne géante. Dans tous les cas, maintenez au moins les deux balises de début et de fin du bloc JavaScript que sont les deux balises <script> et </script> sur les lignes indépendantes.

Il ne reste plus qu’à enregistrer le fichier quelque part. Aussi bien dans le Bloc-notes que dans TextEdit, choisissez la commande Enregistrer du menu Fichier et choisissez l’emplacement désiré, par exemple le Bureau. Donnez un nom suggestif au fichier, comme MonTestJS.html. Il est essentiel de bien saisir l’extension de nom de fichier, c’est-à-dire les quatre lettres qui suivent le point. Il faut que ce soit html. Dans le cas de TextEdit, un message vous demandera de vérifier que c’est vraiment dans ce format que vous voulez sauvegarder. Confirmez.

Une fois le contenu mis à jour dans le fichier, ne quittez pas l’éditeur. Par allers-retours entre le navigateur et l’éditeur, vous allez enrichir le programme avec de petites retouches successives.

Une fois le code source prêt, il faut localiser le fichier que vous venez de sauvegarder. Cherchez sur le Bureau ou dans l’endroit choisi. Double-cliquez sur le nom ou l’icône. Aussi bien sous Windows que sous macOS, le double clic doit provoquer le chargement du contenu dans votre navigateur Web principal. Vous devriez voir apparaître comme titre la mention « Mon JavaScript » et le coin supérieur gauche du contenu devrait indiquer le message « Résultats du programme ici ! ». Si cela ne se passe pas ainsi, relisez lentement le code source du fichier avant de sauvegarder à nouveau.

Nous sommes maintenant prêts à faire des essais JavaScript. Le principe est de saisir des instructions JavaScript entre les deux balises <script> et </ script> sans oublier de sauvegarder après. En basculant dans le navigateur Web, vous devez demander le rechargement de la page (rafraîchissement). En général c’est une petite icône de flèche circulaire. Dans cette configuration, vous faites évoluer votre programme JavaScript en alternant entre les deux applications : vous retouchez et enregistrez dans une fenêtre d’éditeur puis vous testez dans le navigateur en rafraîchissant la page.

Commençons par un programme minimal que vous allez saisir dans la région du code source réservée à JavaScript, donc entre les deux balises <script> et </script> :

let message = "Salut de mon JavaScript !";
document.getElementById("resultat").innerHTML = message;

Ce premier programme est constitué de deux instructions, chacune sur une ligne différente et chacune se terminant par un signe point-virgule.

La première instruction commence par un mot let qui est un mot-clé du langage JavaScript. Autrement dit, il a un sens particulier. Il est suivi du mot message qui est le nom d’une variable librement baptisée par le programmeur. Avec le mot clé let, vous demandez de stocker une valeur dans la variable mentionnée juste après lui. Vous pourrez plus tard changer cette valeur, d’où le terme de variable. Pour l’identifiant de la variable, vous auriez pu indiquer Msg à la place de message ou encore truc, du moment que votre nom commence par une lettre et ne contient ni espace, ni signe de ponctuation (ni lettre accentuée). Dans l’exemple, cette variable message va contenir une série de caractères (une chaîne, car ces lettres sont délimitées par des guillemets droits). Entre ces guillemets, vous pouvez saisir le message de votre choix.

La seconde instruction est clairement moins évidente car plus complexe. Elle est indispensable pour provoquer une interaction entre JavaScript et HTML. Le mot-clé document désigne la page Web. Dans cette page, le mot getElementById fait chercher un élément de la page HTML portant le nom resultat. Si vous regardez le code du premier exemple, vous voyez que c’est le nom qui avait été attribué au contenu de la balise <p> par le mot-clé id. Enfin, innerHTML demande d’insérer le contenu de la variable message de façon dynamique entre les deux balises <p> et </p>, comme si vous aviez saisi cette phrase dès le départ à cet endroit.

Cette seconde instruction est assez complexe parce qu’il faut que JavaScript puisse avoir accès sans ambiguïté à n’importe lequel des nombreux éléments qui constituent une page Web, et cela oblige la syntaxe à être suffisamment riche et adaptable.

Les compilateurs et les interpréteurs des infolangages sont beaucoup moins compréhensifs que vos professeurs en ce qui concerne l’orthographe. Vérifiez bien qu’il n’y a aucune faute de frappe dans la seconde instruction ! De plus, JavaScript distingue les majuscules des minuscules (il distingue la casse des lettres). Vérifiez notamment la façon dont vous avez saisi le mot innerHTML. Si vous avez écrit InnerHTML ou innerHtml, le programme ne fonctionnera pas. Voilà pourquoi il faut désactiver le correcteur automatique dans TextEdit. Si vous laissez cette fonction active, TextEdit changera let en Let, ce qui rendra le programme inopérant.

Sauvegardez la nouvelle version du fichier et actualisez la page Web dans le navigateur. Vous devez voir apparaître le message en haut à gauche. Si ce n’est pas le cas, relisez votre code source !

Passons à un autre essai dans le même fichier. Pour ne pas supprimer les deux jolies instructions que nous avons eu bien du mal à saisir, nous allons les maintenir, mais en les neutralisant par ajout de deux symboles spéciaux :

/*
let message = "Salut de mon JavaScript !";
document.getElementById("resultat").innerHTML = message;
*/

Le langage JavaScript considère les paires barre oblique-astérisque et astérisque-barre oblique comme des marqueurs de commentaire multilignes. Il est en cela influencé par le langage C. Il y a donc deux sortes de commentaires en JavaScript : les commentaires multilignes que nous venons de voir (/* et */) et les commentaires monolignes qui commencent par //.

Notre prochain programme réalise une multiplication entre valeurs fractionnaires :

let a = 535.43;
let b = 289.771;
let c = a * b;
document.getElementById(«resultat»).innerHTML = c;

Comme dans de nombreux autres infolangages, l’opérateur de multiplication correspond à l’astérisque. En effet, le signe mathématique de la multiplication qui est une croix ne fait pas partie du jeu de caractères ASCII de base.

Vous remarquez immédiatement que la dernière instruction ressemble beaucoup à celle du programme précédent, sauf que nous citons dorénavant la variable c qui contient le résultat de la multiplication. JavaScript est suffisamment prévenant pour vous autoriser à citer directement une variable, qu’elle contienne une chaîne de caractères ou une valeur numérique. Il se charge de formater la valeur pour qu’elle s’affiche correctement.

Une des principales techniques de programmation est la boucle de répétition. Nous avons vu comment en réaliser en assembleur grâce à l’instruction JMP et aux sauts conditionnels. Dans certains infolangages de haut niveau, il existe une instruction portant le nom goto qui ressemble beaucoup à ce saut sans condition JMP. Cependant, l’utilisation de goto est fortement déconseillée, sauf cas extrêmes. En effet, un programme qui réalise de nombreux sauts inconditionnels devient rapidement illisible et impossible à maintenir et faire évoluer. En termes techniques, on parle de code spaghetti parce que les différents sauts s’interpénètrent en formant un réseau inextricable. C’est d’ailleurs pour cette raison qu’il n’existe même pas de mot réservé goto en JavaScript. Toute tentation est ainsi évitée.

Les langages de haut niveau actuels réalisent leurs boucles sans multiplier les sauts directs. Voyons par exemple comment faire la somme de tous les nombres de 1 à 600. Voici une première façon de créer une boucle JavaScript :

let total = 0;
let nombre = 1;

while (nombre <= 100)
{
  total = total + nombre;
  nombre = nombre + 1;
}

document.getElementById("resultat").innerHTML = total;

Ne vous inquiétez pas des lignes vides. Je les ajoute pour mieux montrer les différentes sections du programme : elles n’ont aucun effet. Nous commençons par une section d’initialisation dans laquelle nous définissons la valeur initiale de deux variables. La boucle est constituée du mot-clé while et du bloc de code qu’il contrôle par une paire d’accolades. Le test cherche à savoir si la variable nombre est inférieure ou égale à 100. Si c’est le cas, le bloc conditionnel est exécuté pour ajouter le contenu de nombre à la variable total avant d’augmenter la valeur de nombre. Dès que nombre est supérieur à 100, le programme continue après l’accolade fermante du bloc et arrive ainsi à la dernière instruction qui affiche le résultat.

Si vous connaissez l’algèbre traditionnelle, la syntaxe utilisée pour les deux opérations suivantes peut sembler étonnante :

total = total + nombre;
nombre = nombre + 1;

Comment est-il possible qu’une variable total contienne la même valeur que cette variable total plus une variable nombre ? La variable nombre serait égale à zéro ? Et comment nombre peut-il être égal à nombre plus 1 ?

Tout ceci s’explique lorsque vous apprenez qu’en JavaScript le signe égal isolé n’est pas celui d’égalité. C’est l’opérateur d’affectation. La variable située à gauche de l’opérateur reçoit une copie de la valeur qui se trouve à droite du signe, soit dans la variable citée soit sous forme de valeur littérale. Autrement dit, la valeur à droite du signe égal « va » dans la variable à gauche. Pour tester si deux variables sont égales, aussi bien en JavaScript qu’en langage C, il faut utiliser le double signe égal ==.

Les deux instructions qui nous intéressent ici peuvent être rédigées de façon un peu plus compacte en JavaScript, comme en langage C. On peut en effet écrire les deux instructions ainsi :

total += nombre;
nombre += 1;

La paire d’opérateurs constituée d’un signe + et d’un signe égal demande d’ajouter ce qu’il y a du côté droit à la variable située à gauche.

On a fréquemment besoin d’augmenter de 1 (incrémenter) une variable, comme c’est le cas de nombre ici. L’opération d’incrémentation peut de ce fait être abrégée ainsi :

nombre++;

Il est même possible de combiner les deux instructions en une seule :

total += nombre++;

Ici, la valeur de nombre est ajoutée à la valeur de total, puis la valeur de nombre est augmentée de 14.

Ces techniques peuvent laisser perplexes les programmeurs débutants. Vous les éviterez donc si vous pensez que vos fichiers source devront être repris par des débutants.

Une autre approche pour écrire le même programme consiste à créer une boucle de répétition autour du mot-clé for :

let total = 0;

for (let nombre = 1; nombre <= 100; nombre++)
{
  total += nombre;
}
document.getElementById("resultat").innerHTML = total;

L’instruction for comprend entre parenthèses trois clauses séparées par un signe point-virgule. La première clause détermine la valeur initiale de la variable de contrôle, ici nombre. La deuxième est la condition qui doit être satisfaite pour que le bloc contrôlé soit exécuté. Ici, il faut que nombre soit inférieur ou égal à 100. La troisième clause définit comment doit varier la variable de contrôle. Ici, on augmente de 1 la valeur de nombre après avoir exécuté le bloc délimité par les accolades. Dans cet exemple, en particulier, puisqu’il n’y a qu’une seule instruction dans le bloc de code, les accolades ne sont pas indispensables.

Testons cette boucle en créant un petit programme qui affiche la racine carrée des nombres de 1 à 100 :

for (let nombre = 1; nombre <= 100; nombre++)
{
  document.getElementById("resultat").innerHTML +=
    "La racine carrée de " + nombre + " est " +
      Math.sqrt(nombre) + "<br />";
}

Notez qu’il n’y a qu’une seule instruction dans le bloc de code contrôlé par la boucle. Elle est tellement longue que je l’ai répartie sur trois lignes. Remarquez que la première ligne se termine par la paire += : ce qui suit doit être ajouté au contenu existant de l’élément HTML interne de la balise <p>. Cela crée du texte en plus à chaque tour de boucle. Ce que contient le HTML interne est une combinaison de texte et de chiffres. Vous remarquez l’utilisation d’une fonction JavaScript pour le calcul de la racine carrée nommée Math.sqrt. Elle est définie en standard dans le langage JavaScript. (On parle dans ce cas de fonction prédéfinie ou intégrée.) Notez enfin la balise <br /> qui demande en HTML de passer à la ligne suivante.

Lorsque vous exécutez ce programme en demandant l’affichage de la page, vous voyez apparaître une longue liste de texte. Il faudra sans doute utiliser la barre de défilement ou la molette pour tout voir !

Passons à un dernier essai. Nous allons exploiter un très fameux algorithme appelé Crible d’Ératosthène et permettant de trouver des nombres premiers. Rappelons qu’Ératosthène (176-194 av. J.-C.) était le responsable de la fameuse bibliothèque d’Alexandrie ; c’est aussi lui qui a le premier calculé correctement la circonférence de la terre.

Les nombres premiers ne peuvent pas être divisés que par eux-mêmes ou par 1. Le premier nombre premier est 2 et c’est le seul nombre premier pair. Les suivants sont 3, 5, 7, 11, 13, 17, 19, 23, 29, etc.

La technique d’Ératosthène démarre avec une liste de nombres entiers positifs, commençant à 2. Puisque 2 est premier, nous en éliminons tous les multiples (donc, tous les autres pairs). Puisque 3 est aussi premier, nous pouvons supprimer tous ses multiples. Le nombre 4 a déjà été écarté en tant que multiple de 2. Nous pouvons ensuite supprimer tous les multiples de 5. En continuant ainsi, on aboutit à une liste de nombres premiers.

Le programme JavaScript qui va incarner cet algorithme s’appuie sur un concept de programmation, le tableau (array). Un tableau est une variable composée. Il porte un nom comme une variable simple, mais il regroupe plusieurs valeurs. Pour accéder à chacune d’elles, on utilise un indice entre crochets droits à la suite du nom du tableau.

Dans l’exemple, nous appellerons le tableau premiers. Il contiendra 10 000 valeurs booléennes. En JavaScript, une valeur booléenne peut avoir la valeur true ou la valeur false, qui sont deux mots-clés de JavaScript. (Vous connaissez les valeurs booléennes depuis le Chapitre 6.)

Voici d’abord comment définir le tableau premiers et comment y insérer au départ les valeurs booléennes true :

let premiers = [];
for (let index = 0; index < 10000; index++)
{
  premiers.push(true);
}

Cette initialisation peut être écrite de façon beaucoup plus compacte, mais moins lisible :

let premiers = new Array(10000).fill(true);

La partie principale est constituée de deux boucles for imbriquées. La seconde boucle est la boucle interne. Nous devons donc disposer de deux variables différentes servant d’indice. Nous choisissons les noms i1 et i2. Vous pouvez utiliser des chiffres dans les noms de variables, mais jamais en première position :

for (let i1 = 2; i1 <= 100; i1++)
{
  if (premiers[i1])
  {
    for (let i2 = 2; i2 < 10000 / i1; i2++)
    {
      premiers[i1 * i2] = false;
    }
  }
}

La première boucle for (la boucle externe) incrémente la variable i1 de 2 à 100, cette dernière valeur étant la valeur racine carrée de 10 000. L’instruction if permet de n’exécuter le bloc qu’il contrôle que si l’élément indicé vaut true, ce qui confirme que c’est un nombre premier. Dans la boucle interne, nous commençons par incrémenter la variable i2 à partir de 2. Le produit de i1 par i2 vaut donc 2 fois i1, puis 3 fois i1, puis 4 fois i1, etc. Toutes ces valeurs ne sont pas des nombres premiers et l’élément de tableau correspondant reçoit la valeur false.

Il pourrait sembler étonnant de ne faire croître i1 que jusqu’à 100, et i2 que jusqu’à 10 000 divisé par i1, mais cela suffit à cribler tous les nombres premiers jusqu’à 10 000.

La dernière section du programme se charge de l’affichage des résultats :

for (let index = 2; index < 10000; index++)
{
  if (premiers[index])
  {
    document.getElementById("resultat").innerHTML +=
    index + " ";
  }
}

Si la programmation en JavaScript vous intéresse, veuillez ne pas continuer à utiliser un outil aussi élémentaire que le Bloc-notes ou TextEdit ! Il y a des outils bien meilleurs qui, par exemple, vous préviennent sur-le-champ quand vous commettez une faute de frappe ou de syntaxe quelconque.

Les spécialistes se chamaillent quant à savoir si la programmation est une activité artistique ou une activité scientifique. D’un côté, les formations universitaires portent des titres du style « Sciences informatiques » mais, de l’autre, l’un des livres les plus réputés dans ce domaine, celui de Donald Knuth, est intitulé L’Art de la programmation informatique. Créer un logiciel combine science et art, tout en étant encore autre chose. Selon le grand physicien Richard Feynman, la science informatique s’apparente plutôt à l’ingénierie, puisqu’il s’agit de « faire en sorte que quelque chose sache faire quelque chose ».

Écrire un programme n’est en tout cas que rarement une partie de plaisir. Vous avez peut-être déjà constaté qu’il est facile de se tromper lorsque l’on rédige le code source, ce qui oblige à passer beaucoup de temps à trouver et corriger les erreurs. Le débogage ou mise au point des logiciels est un art (ou bien une science, ou alors une activité d’ingénierie ?).

Les quelques expériences pratiques que nous venons de dérouler dans ce chapitre ne forment que la partie émergée d’un iceberg de savoirs concernant le langage JavaScript. Et l’histoire nous a montré qu’il fallait se méfier des icebergs. Il arrive que l’ordinateur réalise quelque chose de totalement inattendu. Partons du petit programme JavaScript suivant. Il ressemble au deuxième projet, en modifiant seulement les valeurs :

let a = 55.2;
let b = 27.8;
let c = a * b;
document.getElementById("resultat").innerHTML = c;

La valeur que le programme affiche dans le navigateur est 1534,5600000000002, qui ne semble pas être la bonne réponse. En effet, la bonne réponse est tout simplement 1534,56.

Que s’est-il passé ?

Bienvenue dans l’univers des nombres flottants qui sont d’une extrême importance en informatique. Un standard a été défini en 1985 par l’institut IEEE (Institut of Electric and Electronics Engineers), puis reconnu par cet autre institut qu’est l’ANSI (American National Standards Institute). C’est ainsi qu’est apparu ANSI/IEEE Std 754-1985, le standard pour l’arithmétique binaire en virgule flottante. Le texte du standard n’est pas très long, puisqu’il ne fait que 18 pages, mais cela suffit à fournir tous les détails concernant la manière de coder les valeurs flottantes de façon convenable. C’est un des standards les plus importants dans le domaine de l’informatique. Il est utilisé par quasiment tous les ordinateurs et, donc, par tous les logiciels que vous aurez l’occasion de rencontrer.

Ce standard IEEE définit deux formats principaux : le format en simple précision, qui occupe 4 octets par valeur, et le format en double précision, qui en occupe 8. Dans certains langages, vous pouvez choisir entre les deux formats mais, en JavaScript, tout est en double précision.

Le standard IEEE utilise la notation scientifique pour représenter les nombres. Chaque nombre comporte deux parties : une mantisse (certains disent « significande »), qui est multipliée par 10 à une certaine puissance, celle-ci correspondant à l’exposant :

42 705,7846 = 4,27057846 × 10⁴

Cette représentation avec un seul chiffre avant la virgule est dite normalisée5.

Cette notation scientifique est employée par IEEE pour représenter des valeurs binaires. Jusqu’ici, nous n’avons rencontré dans ce livre que des valeurs binaires entières, mais rien n’empêche d’écrire en binaire un nombre fractionnaire. Voici un exemple :

101.1101

Ne dites pas que le point est ici un point décimal ! Nous sommes en numération binaire ; on devrait plutôt parler de point binaire. Les chiffres à gauche du point sont la partie entière et ceux à droite, la partie fractionnaire.

Nous avons vu dans le Chapitre 10 comment convertir une valeur binaire en décimal. Nous savons ainsi que les chiffres correspondent à des puissances de 2. Les chiffres binaires situés à droite du point décimal sont aussi des puissances de 2, mais des puissances négatives. Nous pouvons convertir en décimal le nombre binaire 101.1101 en multipliant les bits par les puissances de 2 positives ou négatives selon la position par rapport au point binaire :

1 × 2² +

0 × 2¹ +

1 × 2⁰ +

1 × 2⁻¹ +

1 × 2⁻² +

0 × 2⁻³ +

1 × 2⁻⁴

Les puissances négatives de 2 sont faciles à calculer en commençant par 1. Il suffit de diviser sans cesse par 2 :

1×4+

0×2+

1×1+

1 × 0,5 +

1 × 0,25 +

0 × 0,125 +

1 × 0,0625

Ce calcul nous permet de savoir que l’équivalent décimal de 101.1101 est 5,8125.

La notation scientifique normalisée pour les valeurs décimales ne laisse qu’un seul chiffre à gauche du point. Il en va de même en notation scientifique binaire. Ainsi, la valeur binaire fractionnaire 101.1101 s’écrit de façon normalisée ainsi :

1.011101 × 2²

Il découle de cette règle qu’un nombre à virgule flottante binaire normalisé comporte toujours un chiffre 1 dans sa partie entière. Nous y reviendrons.

Le standard demande de stocker un nombre flottant en double précision avec 8 octets. Voici comment les 64 bits sont distribués :

s = 1 bit de signe

e = 11 bits d’exposant

f = 52 bits de mantisse

Puisqu’il y a toujours un 1 à gauche du point binaire lorsque la valeur est normalisée, il est inutile de consommer un bit pour ce chiffre systématique dans un flottant normalisé. Seule la partie fractionnaire est stockée sur 58 bits. C’est d’ailleurs pourquoi, même si on ne stocke que 52 bits, on parle de 53 bits de précision. Nous verrons dans un moment à quoi correspond une telle précision.

La partie exposant sur 11 bits peut avoir une valeur entre 0 et 2047. Il s’agit d’un exposant biaisé parce qu’il faut soustraire une valeur appelée le biais de l’exposant pour trouver l’exposant appliqué. En double précision, le biais est égal à 1023.

Le nombre représenté par le bit de signe s, l’exposant e et la partie fractionnaire f est :

( – 1)s × 1.f × 2e – 1023

L’indication – 1 puissance S pour le bit de signe est une façon typique des mathématiciens d’exprimer les conditions suivantes : « Si s vaut 0, c’est que le nombre est positif (n’importe quelle valeur à la puissance 0 vaut 1) ; si s vaut 1, c’est que le nombre est négatif (parce que – 1 à la puissance 1 reste – 1) ».

Dans la suite de l’expression, nous trouvons 1.f, c’est-à-dire la valeur 1 suivie d’un point binaire suivi des 52 bits de la partie fractionnaire. C’est cette valeur qu’il faut multiplier par 2 à une certaine puissance. C’est l’exposant sur 11 bits avec biais qui est stocké en mémoire après soustraction de 1023.

Je vous épargne quelques détails. Il n’y a dans cette description aucune solution pour représenter la valeur 0 ! C’est un cas spécial néanmoins prévu. Le standard gère les zéros négatifs (pour représenter les valeurs négatives très petites), l’infini positif et négatif et, enfin, une valeur que l’on appelle NaN (Not a Number) qui veut dire « non-nombre ». Ces cas spéciaux constituent des éléments importants du standard des nombres à virgule flottante.

Revenons au nombre binaire 101.1101 de notre exemple précédent. Il est stocké avec une mantisse sur 52 bits qui s’écrit ainsi :

0111 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

J’ai ajouté une espace tous les 4 bits pour rendre le tout plus lisible. L’exposant biaisé vaut 1025, ce qui fait que le nombre vaut :

1.011101 × 21025 – 1023 = 1.011101 × 2²

En double précision, et en dehors du 0, la valeur positive ou négative flottante la plus petite est celle-ci :

1.0000000000000000000000000000000000000000000000000000 × 2–1022

Autrement dit, il y a 52 zéros après le point binaire. La plus grande valeur possible est la suivante :

1.1111111111111111111111111111111111111111111111111111 × 2¹⁰²³

La plage de valeurs décimales équivalente va d’environ 2,2250738585072014 × 10–308 à 1,7976931348623158 × 10³⁰⁸. Notez que 10 à la puissance 308 est un très grand nombre. C’est 1 suivi de 308 zéros décimaux !

Les 53 bits de la mantisse (si l’on compte le bit qui n’est pas représenté) donnent une résolution qui équivaut à peu près à 16 chiffres décimaux, mais avec certaines limitations. C’est ainsi que les deux valeurs 140 737 488 355 328,00 et 140 737 488 355 328,01 sont stockées avec exactement la même image binaire et, dans vos programmes, ces deux valeurs sont confondues.

Un problème plus important encore est qu’il est impossible de représenter exactement la plupart des valeurs fractionnaires décimales. Prenons, par exemple, la valeur décimale très simple 1,1. Avec une mantisse sur 52 bits, le résultat est :

0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

Il s’agit là de la partie fractionnaire, donc à droite du point binaire. La représentation complète demande d’ajouter le 1 de la partie entière :

1.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

Pour trouver l’équivalent décimal de cette valeur, on travaille en procédant ainsi :

1 + 2–3 + 2–4 + 2–7 + 2–8 + 2–11 +…

Cela équivaut à faire les additions successives suivantes :

1 + 0,0625 + 0,03125 + 0,00390625 + 0,001953125 + 0,000244140625 +

…

La valeur décimale qui correspond à cette valeur binaire n’est pas celle dont nous étions partis (1,1) mais celle-ci :

1,10000000000000008881…

Évidemment, si vous faites des opérations arithmétiques avec des nombres qui ne sont pas représentés de façon exacte, vous allez produire des résultats qui ne seront pas exacts. Voilà pourquoi, dans notre exemple précédent en JavaScript, le fait de multiplier 55,2 par 25,8 donne un résultat qui n’est pas tout à fait juste : 1 534,5600000000002.

Nous sommes habitués à ce que l’espace des nombres soit infini et qu’il offre une continuité totale. Cependant, dans un ordinateur, il faut stocker des valeurs distinctes les unes des autres, et sans infini. Les personnes intéressées trouveront un outillage théorique pour les mathématiques appliquées aux machines numériques en étudiant les mathématiques discrètes ou finies.

Un autre niveau de complexité concernant l’arithmétique à virgule flottante concerne les calculs de racines, d’exposants, de logarithmes et la trigonométrie. Tous ces traitements peuvent être réalisés avec les quatre opérations de base à virgule flottante : addition, soustraction, multiplication et division.

Voici par exemple comment calculer le sinus par développement d’une série :

L’argument x doit être spécifié en radians (2π radians valent 360o). Le point d’exclamation est une factorielle qui consiste à multiplier entre elles toutes les valeurs entières de 1 jusqu’au nombre concerné. Ainsi, 5 ! est le résultat de 1 × 2 × 3 × 4 × 5, qui ne sont que des multiplications. Les exposants des différents termes sont aussi des multiplications et le reste n’est que divisions, additions et soustractions.

La seule partie un peu délicate correspond aux points de suspension en fin d’équation. Ils signifient qu’il faut continuer les calculs éternellement. En réalité, vous allez vous limiter à la plage qui va de 0 à π/2 (ce qui suffit pour déduire toutes les autres valeurs de sinus). Il n’est absolument pas nécessaire de prévoir de calculer à l’infini. Au bout d’environ 12 termes, vous avez atteint une précision suffisante pour les 53 bits de résolution des nombres en double précision.

Les ordinateurs sont censés simplifier la vie des gens. Devoir concevoir et réaliser toute une famille de routines pour faire de l’arithmétique à virgule flottante semble contraire à cette attente. Mais la magie du logiciel est là ! Dès qu’une personne a créé et mis au point un ensemble de routines pour les calculs flottants sur un modèle de machine, tous les autres programmeurs et utilisateurs peuvent en profiter. L’arithmétique à virgule flottante est tellement cruciale dans les logiciels scientifiques et d’ingénierie qu’elle fait l’objet d’une attention particulière. Dans les premiers temps de l’informatique, une des premières tâches d’écriture de logiciels pour une nouvelle machine consistait justement à créer les routines de traitement des valeurs flottantes. Les langages de programmation possèdent tous des librairies de fonctions mathématiques complètes. Nous en avions aperçu une pour JavaScript en adoptant la fonction Math.sqrt.

C’est une excellente idée de prévoir des composants matériels dédiés aux calculs à virgule flottante. Le premier ordinateur commercialisé à disposer d’un matériel spécialisé pour la virgule flottante était l’IBM 704, dès 1954. Tous les nombres y étaient stockés sous forme de valeurs sur 36 bits. Pour les nombres à virgule flottante, il y avait 27 bits de mantisse, 8 bits d’exposant et 1 bit de signe. Ce matériel de calcul savait faire les additions, soustractions, multiplications et divisions. Les fonctions plus sophistiquées étaient réalisées par logiciel.

Les composants électroniques de calcul à virgule flottante sont apparus pour les ordinateurs personnels dès les années 1980, quand Intel a lancé son coprocesseur numérique 8087. On parlait alors de coprocesseur mathématique ou d’unité à virgule flottante FPU (Floating Point Unit). On parle de coprocesseur parce qu’il ne peut pas fonctionner seul et doit être associé à un processeur principal 8086 ou 8088 (les premiers microprocesseurs Intel 16 bits). Lors de son lancement, le 8087 était considéré comme le circuit intégré le plus sophistiqué qui n’y ait jamais eu. Dorénavant, les coprocesseurs mathématiques ont été réincorporés à l’intérieur du circuit du processeur principal.

De nos jours, les programmeurs se servent de valeurs numériques flottantes comme si elles faisaient partie naturellement de l’ordinateur, ce qui est le cas d’une certaine façon.

1 N.d.T. : en France, bien que l’on utilise la virgule en sciences informatiques, et que l’on parle de nombres à virgule flottante, il faut utiliser dans vos codes source le point comme séparateur et non la virgule.

2 N.d.T. : plus important encore, les visiteurs du site Web n’auront pas besoin de télécharger quoi que ce soit avant de pouvoir profiter du fruit de votre créativité en JavaScript.

3 N.d.T. : en Europe, on utilise également beaucoup Firefox et Opera.

4 N.d.T. : en écrivant ++nombre, l’incrémentation est réalisée avant l’affectation.

5 N.d.T. : nous parlons ici de virgule bien qu’il s’agisse d’un point décimal, et même les programmeurs français doivent utiliser le point.

Chapitre 28  Le cerveau mondial

Entre 1936 et 1937, l’écrivain anglais Herbert George Wells avait donné une série de conférences abordant un sujet assez particulier. Âgé à l’époque de plus de 70 ans, il avait publié dans les années 1890 plusieurs nouvelles de science-fiction qui ont connu un grand succès, et notamment La Machine à explorer le temps, L’Île du docteur Moreau, L’Homme invisible et La Guerre des mondes. Le temps passant, Wells était devenu une sommité intellectuelle connue du grand public, abordant avec sérieux différents sujets sociaux et politiques et partageant ses convictions.

Ses conférences de 1936 et 1937 ont été réunies en 1938 dans un livre intitulé Le cerveau mondial (World Brain)1.

Dans son discours, Wells parlait d’un projet d’encyclopédie, mais pas du genre de celles qui se vendent en porte-à-porte. Son encyclopédie mondiale devait recueillir toutes les connaissances globales d’une façon inédite.

L’Europe vivait alors une période quelque peu trouble. La Grande Guerre s’était achevée deux décennies auparavant et l’Europe semblait se préparer à un nouveau conflit qui promettait de déchirer le continent. Wells était un optimiste et un utopiste ; il était persuadé que la science, la raison et les connaissances constituaient les meilleurs outils pour guider le monde vers un futur meilleur. L’encyclopédie mondiale qu’il proposait devait contenir…

… les concepts structurants de notre ordre social, les grandes lignes et les descriptions principales dans tous les domaines des connaissances, une image exacte et suffisamment détaillée de notre univers, une histoire générale du monde et un système fiable et complet constituant une référence et une source majeure de connaissances.

En bref, il s’agissait de fournir une interprétation partagée de la réalité, une « unification mentale ».

Une telle encyclopédie devrait être mise à jour en permanence, alimentée par les connaissances du monde en croissance permanente. Au long de ce processus de développement, elle aboutirait à…

… une sorte de lieu d’amélioration mentale, un entrepôt dans lequel les connaissances et les idées arrivent, sont triées, résumées, digérées, clarifiées et comparées. Cela constituerait l’embryon matériel d’un véritable Cerveau Mondial.

Il est peu probable que Wells ait pu entendre parler des premiers ordinateurs numériques qui n’étaient dans les années 1930 que des objets de recherche appliquée. C’est pourquoi il n’avait pas d’autre choix que de concevoir son encyclopédie sous forme de livre : « 20, 30 ou 40 volumes », pensait-il. Ceci dit, il était familier avec la toute nouvelle technologie du microfilm :

Il devrait devenir possible dans un futur proche d’exploiter et de créer des bibliothèques microscopiques contenant des photographies de toutes les pages des livres et documents importants, afin de les entreposer pour les rendre accessibles en fonction des demandes des étudiants. Les temps sont proches qui permettront à n’importe quel étudiant, où qu’il se trouve dans le monde, de s’installer avec son projecteur dans sa chambre pour étudier n’importe quel livre, n’importe quel document, reproduit fidèlement.

Quelle vision du futur !

Dix ans plus tard, en 1945, un ingénieur et inventeur nommé Vannevar Bush eut la même vision, de façon un peu plus évoluée.

Bush avait déjà laissé sa trace dans l’histoire de l’informatique. Dès 1927, à l’aide de ses étudiants du département d’ingénierie électrique de l’institut MIT (Massachusetts Institute of Technology), il avait construit un remarquable analyseur différentiel, c’est-à-dire un ordinateur analogique capable de résoudre des équations différentielles. Dès le début des années 1930, il avait été nommé maître de conférences en ingénierie et vice-président du MIT.

Le New York Times avait salué sa disparition en 1974 en le décrivant comme un « paradigme de l’ingénieur, de l’homme qui va au bout des projets », qu’il s’agisse de régler des problèmes techniques ou de réorganiser la bureaucratie gouvernementale. Au cours de la Seconde Guerre mondiale, Bush avait été chargé de coordonner plus de 30 000 scientifiques et ingénieurs pour l’effort de guerre, y compris le fameux projet Manhattan qui a produit la première bombe atomique. Plusieurs décennies durant, Bush s’était fait l’avocat d’un engagement des scientifiques et des ingénieurs dans la vie publique.

Vers la fin de la Seconde Guerre, Bush avait écrit un article devenu célèbre dans le numéro de juillet 1945 de Atlantic Monthly, article qui s’est avéré visionnaire rétrospectivement. Une version abrégée en avait été publiée dans le numéro de septembre du magazine Life avec quelques charmantes illustrations.

Tout comme Wells, Bush se préoccupait des difficultés à rester informé des évolutions techniques :

Les efforts de recherche ne cessent de croître, mais il semble de plus en plus évident que la spécialisation croissante va nous mener à la noyade. Celui qui se lance dans une recherche est vite débordé en découvrant les résultats et conclusions de milliers d’autres chercheurs, éléments qu’il n’a pas eu le temps de bien assimiler et de mémoriser au fur et à mesure de leur apparition. Le problème majeur ne se situe pas au niveau d’un manque éventuel d’adéquation entre ce qui est publié et les besoins actuels, mais bien plutôt au fait que la quantité de travaux publiés dépasse de loin nos capacités d’exploitation efficace de ces travaux. La somme des expériences humaines croît à une vitesse prodigieuse, alors que les moyens techniques dont nous disposons pour tracer nos chemins dans ce labyrinthe afin de découvrir l’élément dont nous avons à chaque moment besoin n’a pas changé depuis l’époque de la fabrication des premiers bateaux à voile.

Bush était bien conscient du rythme soutenu des évolutions technologiques qui allaient pouvoir aider les scientifiques à l’avenir. Il avait d’ailleurs conçu une caméra à fixer sur le front et pouvant être déclenchée dès que quelque chose de notable devait être enregistré. Il parlait déjà des microfilms, de la transmission de documents en fac-similé et de machines capables d’enregistrer la voix humaine et de la convertir directement en texte.

Vers la fin de son article, Bush avait identifié un problème qui restait non résolu : « … nous pouvons multiplier le nombre d’enregistrements sans limite ; mais même avec le volume actuel de connaissances, nous avons bien du mal à les exploiter ». La plupart des informations sont en effet organisées et indexées selon l’ordre alphabétique, ce qui est clairement inapproprié. En effet :

Le cerveau humain ne travaille pas de cette façon, mais par associations. Lorsqu’il est concentré sur un objet, il veut pouvoir passer directement à l’objet associé selon les suggestions de ses associations de pensée, empruntant ainsi un réseau neuronal intriqué constitué de connexions entre cellules du cerveau.

Bush avait alors conçu une machine, « une bibliothèque et une armoire à dossiers personnelle mécanisée », c’est-à-dire une sorte de bureau sophistiqué stockant des microfilms et permettant d’y accéder facilement. Il l’avait appelée « memex ».

L’essentiel du contenu d’un memex est acheté sous forme de microfilms prêts à être insérés. Cela permet de collecter et de mettre en place toutes sortes de livres, d’illustrations, de périodiques, de journaux. La correspondance d’entreprise est collectée également et la saisie directe reste possible. Le haut du memex comporte une plaque transparente sur laquelle on dépose une note, une photographie, un compte-rendu, bref, toutes sortes d’objets de savoir. Une fois l’objet en place, la manipulation d’un levier fait prendre une photographie qui est stockée dans le prochain espace vide d’une section adéquate du memex…

Mieux encore, le memex permettrait d’ajouter des notes de marge et des commentaires et de les relier par un système d’indexation associative.

Il s’agit là d’une des caractéristiques majeures du memex. Le point important est cette capacité à mettre deux éléments en relation. De plus, une fois qu’un certain nombre d’éléments ont été interconnectés pour former un cheminement, il devient possible de les passer en revue rapidement ou lentement, en utilisant un levier dans un geste ressemblant à celui de tourner les pages d’un livre. C’est exactement comme si les objets physiques avaient été réunis à partir de sources disparates pour former un autre livre sous une nouvelle reliure. Cela permet l’émergence de nouvelles formes d’encyclopédies, prêtes à l’emploi car dotées d’un réseau de cheminement associatif qui les traverse en tous sens.

Bush avait même anticipé que par paresse et par désir de confort, on s’épargnerait de mémoriser tout ce que l’on avait appris. L’utilisateur de cette machine « pouvait se réapproprier le privilège d’oublier les connexions entre choses dont il n’avait pas besoin immédiatement, car il était assuré de pouvoir les retrouver dès qu’il en aurait besoin ».

En 1965, donc deux décennies après la création du concept de memex par Bush, il devenait envisageable de concevoir un ordinateur conforme à ce concept. Le visionnaire de l’informatique Ted Nelson (né en 1937) décida de moderniser l’exposé du concept de memex dans un article publié en 1965 parmi les comptes-rendus de conférence de l’association ACM (Association for Computing Machinery). Son article évoquait le traitement d’informations complexes grâce à une structure de fichiers conçue pour gérer le complexe, l’évolutif et l’indéterminé. Voici comment s’ouvrait son introduction :

Si nous voulons nous servir d’un ordinateur pour nos fichiers personnels et comme support à la créativité, il nous faut des structures de fichiers de données radicalement différentes dans leur caractère des structures utilisées pour le traitement des données d’entreprise et scientifiques. Ces structures doivent en effet permettre d’établir des arrangements intriqués et idiosyncrétiques, des modifications sans limite, des alternatives non figées et posséder une documentation interne complète.

Tout en faisant référence à l’article de Bush à propos des memex, Nelson affirma que le matériel était prêt pour construire cet équipement. La structure de fichiers qu’il proposait était ambitieuse et attrayante à la fois. Il lui a fallu inventer un nouveau mot pour bien la décrire :

Permettez-moi d’introduire un nouveau terme, « hypertexte », pour désigner un ensemble de matériaux textuels ou illustratifs interconnectés d’une façon tellement complexe qu’il n’est plus possible d’en produire une représentation sur papier. Cet ensemble pourrait contenir des sommaires ou des cartes du contenu avec toutes leurs relations ; il serait doté d’annotations, d’ajout et de notes de bas de page produites par des experts à l’occasion de leur étude du contenu. Je considère qu’un tel objet et un tel système, s’il est correctement conçu puis administré, offrirait de grandes potentialités dans les secteurs de l’éducation, en augmentant la palette de choix offerte aux étudiants, en renforçant leur sentiment de liberté, en les motivant et en leur laissant garder le contrôle intellectuel. Un tel système pourrait croître sans limite, en englobant de plus en plus des connaissances écrites du monde. Mais la structure de fichiers interne devra avoir été conçue pour permettre cette croissance, ces changements et ces arrangements d’informations complexes.

Les articles de HG Wells, de Vannevar Bush et de Ted Nelson montrent que certaines personnes pressentaient déjà ce qu’est devenu Internet bien avant que l’on puisse croire en sa faisabilité.

C’est un important défi que de réussir à faire communiquer des ordinateurs sur de longues distances. Le réseau Internet tire son origine des recherches du département de la Défense des États-Unis, dès les années 1960. Sa première mouture nommée ARPANET (Advanced Research Projects Agency Network) est opérationnelle dès 1971. Les principaux concepts fondateurs d’Internet étaient déjà en place. Le plus important était sans doute le concept de commutation de paquets qui consiste à distribuer les flux de données en petits paquets recevant des informations de gestion dans un en-tête.

Supposons, par exemple, un ordinateur sur lequel se trouve un fichier texte pesant 30 000 octets. Un ordinateur B est connecté à A d’une certaine façon et émet une demande pour obtenir ce fichier texte en envoyant des signaux sur la connexion. L’ordinateur a réagi en commençant par répartir le contenu du fichier en 20 blocs de 1500 octets pièce. Chaque paquet comporte une section d’en-tête dans laquelle est indiquée la source (ordinateur A), la destination (ordinateur B), le nom du fichier, un nombre identifiant le numéro du paquet et le nombre total de paquets, par exemple, partie 7/20. L’ordinateur B accuse réception de chacun des paquets au fur et à mesure puis reconstruit le fichier. S’il constate qu’un paquet manque, parce qu’il a été perdu lors de la transmission, le destinataire en demande une nouvelle copie.

La partie en-tête peut contenir une somme de contrôle (checksum). C’est un grand nombre qui est produit selon une règle particulière à partir de la valeur de tous les octets du fichier. Lorsque l’ordinateur B reçoit un paquet, il effectue ce calcul et compare le résultat à la somme de contrôle calculée par A et transférée. S’il n’y a pas égalité, on considère que le paquet a été altéré pendant son transfert et une nouvelle copie est demandée.

La commutation de paquets offre plusieurs avantages par rapport à l’envoi d’un fichier en une seule étape. Tout d’abord, la connexion entre les deux ordinateurs peut ainsi être utilisée en même temps avec d’autres partenaires pour échanger d’autres paquets de données. Aucun ordinateur en particulier ne bloque l’accès du système pour lui pendant qu’il transmet un fichier volumineux. En outre, les paquets détériorés sont les seuls à devoir être transmis, au lieu de retransmettre la totalité du fichier.

Tout au long de ce livre, nous avons vu comment un fil conducteur permettait de transmettre de l’information numérique. Lorsque du courant circule et qu’il y a une tension, cela correspond à la valeur binaire 1. Lorsqu’il n’y a pas de courant, c’est un 0 binaire. Mais tous les fils et câbles des circuits vus dans le livre étaient assez courts. Pour transmettre de l’information numérique sur grande distance, il faut adopter une autre stratégie.

La première forme de communication numérique à longue distance correspond au téléphone filaire traditionnel. En effet, le téléphone analogique existait depuis un siècle et était donc largement répandu. Au départ, il avait été conçu pour transmettre de la voix. En termes techniques, la plage de fréquences qui devaient pouvoir être transmises sans trop de distorsion (pour conserver l’intelligibilité) allait de 300 Hz à 3 400 Hz.

Pour convertir des 0 et des 1 binaires afin de transmettre des données sur une ligne téléphonique classique, donc sous forme audio, on utilise une technique de modulation qui consiste à faire varier le signal analogique audio pour que de l’information numérique y soit codée.

Un des équipements de modulation les plus anciens, le Bell 103, avait été lancé en 1962 par la société AT&T. Son influence a continué à se faire sentir largement jusque dans les années 1990. Cet appareil sait fonctionner en mode Full Duplex, ce qui signifie qu’il peut recevoir et émettre des données simultanément. À une extrémité de la ligne téléphonique, il y a une station émettrice et, à l’autre extrémité, une station réceptrice. Les deux communiquent à la vitesse de 300 bits par seconde.

Le Bell 103 s’appuie sur la technique de modulation par déplacement de fréquences FSK (Frequency Shift Keying). Le 0 binaire est encodé par la station émettrice avec une fréquence de 1 070 Hz et le 1 binaire, avec une fréquence de 1 270 Hz. Voici par exemple l’encodage fréquentiel du code ASCII sur 8 bits de la lettre W :

La figure ne permet pas bien de le distinguer, mais les espaces entre les différents cycles des bits à 0 sont un peu plus larges que pour les bits à 1, parce que la fréquence est inférieure pour les 0. La station réceptrice utilise deux autres fréquences, 2 025 et 2 225 Hz. Pour le contrôle des erreurs de transmission, on employait souvent un unique bit de parité.

L’équipement qui réalise la modulation pour encoder les données binaires est tout à fait capable de l’opération inverse : la démodulation d’un signal audio pour en extraire des 0 et des 1. C’est pourquoi on appelle ce genre d’équipement un modulateur-démodulateur, c’est-à-dire un modem.

Le modèle Bell 103 savait transmettre à 300 bits par seconde ; on parlait aussi d’équipement 300 bauds. Le baud est une unité de mesure qui rend hommage à Émile Baudot (voir Chapitre 13). Le taux exprimé en bauds correspond à une quantité de symboles transmis, ce qui correspond parfois au nombre de bits transmis par seconde, mais pas toujours. Supposez, par exemple, que vous ayez défini une stratégie de modulation FSK qui exploite quatre fréquences différentes pour coder quatre couples de bits 00, 01, 10 et 11. Si la forme d’onde qui porte ces codes travaille à 1 000 périodes par seconde, on parlerait d’une transmission à 1 000 bauds, mais l’opération transmettrait 2 000 bits par seconde.

Un modem 300 bauds fonctionnant avec des ondes acoustiques, il émet des bruits caractéristiques, notamment au moment de la prise de connexion avec le partenaire. Ces grésillements sont souvent utilisés dans les séries télévisées pour évoquer l’informatique des années 1980 et 1990.

Les modems utilisant les lignes téléphoniques classiques ont plafonné à la vitesse de 56 kbit/s et on les appelle des modem 56 K. Ils restent incontournables dans certaines régions isolées. Pour une meilleure vitesse de connexion, on est vite passé à la technique ADSL, au câble coaxial et à la liaison satellite. Ces techniques utilisent une modulation à fréquence radio bien plus élevée. Des techniques de modulation de plus en plus sophistiquées ont permis d’inclure de plus en plus d’informations numériques dans les formes d’onde, ce qui a permis d’augmenter encore les performances des transmissions numériques.

L’essentiel des connexions Internet entre continents ainsi qu’entre grandes métropoles utilise un support plus récent qui est la fibre optique. Le fond des océans est traversé par de tels câbles en fibre optique. Il s’agit de très fins fils de verre ou de plastique dans lesquels circule un rayon de lumière infrarouge. Normalement, la lumière progresse toujours en ligne droite, mais la surface interne des fibres optiques est conçue pour forcer le rayon de lumière à rebondir ; c’est ce qui permet de courber la fibre (avec précautions) sans empêcher la progression du signal.

En général, un câble réunit plusieurs centaines de fibres, chacune transmettant un signal indépendant. Il est même possible de faire porter plusieurs signaux sur une même fibre. Le codage de l’information sur la fibre utilise des impulsions de lumière : la lumière est effectivement allumée pour 1 et éteinte pour 0. C’est ainsi que fonctionnent les échanges à haute vitesse de l’Internet moderne.

La topologie joue un rôle important : en théorie, il aurait été possible de créer Internet autour d’un unique ordinateur gigantesque situé quelque part, puis en connectant tous les autres ordinateurs à celui-ci. Cela aurait rendu Internet beaucoup plus simple, mais aurait créé des inconvénients : les personnes situées loin de l’ordinateur central ne jouiraient pas des mêmes performances que celles situées très près. De plus, si cet ordinateur gigantesque tombait en panne, la totalité d’Internet du monde entier s’arrêterait.

Il a été décidé de décentraliser Internet pour offrir beaucoup de redondance. Il n’existe pas un point faible en particulier. Un grand nombre de gros ordinateurs stockent d’énormes volume de données : ce sont des serveurs. Les plus petits ordinateurs qui exploitent ces données en interrogeant les serveurs sont des clients. En général, les ordinateurs clients dont nous nous servons ne sont pas directement connectés à ces serveurs, mais ils passent par un intermédiaire, le fournisseur d’accès Internet FAI. Vous savez sans doute comment s’appelle le vôtre, parce que vous lui versez un abonnement mensuel. Lorsque vous accédez à Internet depuis votre telliphone, c’est votre opérateur téléphonique qui joue le rôle de FAI.

En plus des câbles et des ondes radio, tout ce qui se trouve sur Internet est interconnecté grâce à des routeurs, des équipements qui calculent des routes pour connecter chaque client au serveur demandé. Vous disposez peut-être aussi d’un routeur à votre domicile, car il fait souvent partie du modem d’accès à Internet ou d’une station Wi-Fi. Les routeurs sont dotés à l’arrière de prises Ethernet pour y connecter physiquement un ordinateur et éventuellement une imprimante.

Les routeurs qui gèrent les communication Internet sont beaucoup plus complexes que les routeurs domestiques et la plupart sont interconnectés. Ces routeurs transmettent des paquets de données à d’autres routeurs qui sont connectés à leur tour à d’autres routeurs, et on obtient au final un réseau maillé complexe. Un routeur est doté de son propre processeur parce qu’il contient et met à jour des tables de routage et applique des règles de routage algorithmiques pour trouver à chaque instant la meilleure route transférant chaque paquet à la destination.

L’autre composant matériel essentiel est la carte adaptateur réseau NIC (Network Interface Controller). Chaque carte réseau reçoit en usine un identificateur unique et permanent. C’est son adresse MAC (Media Access Control). Une adresse MAC est constituée de douze chiffres hexadécimaux, souvent présentés sous forme de six groupes de deux.

Tout matériel connecté à Internet possède une adresse MAC unique. Les ordinateurs de bureau et les portables disposent en général de plusieurs adresses pour leur connexion à Internet, pour le Wi-Fi et pour le Bluetooth (cette technique permettant de se connecter aux périphériques proches grâce à des ondes radio). Vous pouvez connaître les adresses MAC en vigueur en accédant aux paramètres de votre ordinateur. Le modem et le boîtier Wi-Fi possèdent bien sûr une adresse MAC, généralement imprimée sur une étiquette. Votre telliphone utilise des adresses MAC pour le Wi-Fi et le Bluetooth. Les douze chiffres hexadécimaux d’une adresse MAC garantissent que l’épuisement du nombre d’adresses ne menace pas. Ces douze chiffres autorisent plus de 30 000 adresses MAC pour chaque être humain de la planète.

Le réseau Internet offre tout un ensemble de services, notamment la messagerie et le partage de fichiers, mais la plupart des gens exploitent surtout Internet pour son réseau Web (le World Wide Web) qui a principalement été défini en 1989 par le chercheur anglais Tim Berners-Lee (né en 1955). Lorsqu’il a créé le Web, il a choisi de réutiliser le mot « hypertexte » qui avait été inventé par Ted Nelson alors que Berners-Lee n’avait que 10 ans.

Le principal type de document sur le Web est la page Web ; son contenu est organisé grâce au langage HTML (Hypertext Markup Language). Nous en avons vu un peu dans les exercices du chapitre précédent. Un document HTML est bâti sur une succession de balises (tags) de marquage : <p> délimite un paragraphe de texte, <h1> un titre de premier niveau, et <img> une image bitmap.

Une des balises de marquage HTML essentielles s’écrit <a>, initiale du mot ancrage. La balise d’ancrage sert à définir un hyperlien, qui prend en général l’aspect d’un mot ou groupe de mots s’affichant différemment du reste du texte, par exemple avec un effet de soulignement. Lorsque la souris clique dans ce lien, cela provoque l’affichage d’une autre page web. C’est le mécanisme essentiel d’interconnexion des pages web. On peut aussi définir des liens internes, pour aller vite d’une section à une autre d’un document long ou ajouter des liens de référence permettant d’aller plus en profondeur dans un des sujets abordés.

Le Web s’est énormément enrichi en l’espace de deux décennies.

Ni H. G. Wells, ni Vannevar Bush n’auraient pu prévoir cet énorme potentiel de recherche en ligne, d’activités commerciales, de divertissements (les vidéos de chat surtout) et l’attrait pervers qui pousse des personnes à s’affronter avec de parfaits inconnus à propos de politique. Rétrospectivement, la révolution informatique aurait été incomplète sans Internet. Ce réseau est devenu le point culminant de la révolution numérique ; on pourrait vouloir mesurer le bénéfice de cette révolution en vérifiant si Internet a fait du monde un endroit meilleur. Mais c’est un sujet que je laisse discourir à des esprits plus affûtés que le mien.

Chaque page Web est localisée grâce à son localisateur de ressources uniforme URL (Uniform Resource Locator). Voici, par exemple, l’adresse URL d’une des pages de la version anglaise du site Web compagnon du livre :

https://www.CodeHiddenLanguage.com/Chapter24/index.html

Cette URL débute par un nom de domaine (www.CodeHiddenLanguage.com), suivi d’un nom de dossier (Chapter24), suivi d’un nom de fichier (index. html) avec des barres obliques en guise de séparateurs. Le début de l’URL est le nom d’un protocole, ici https qui est la variante sécurisée de http (Hypertext Transfer Protocol). Ces protocoles stipulent les modalités de transfert du contenu de la page depuis le site vers le navigateur.

Ne confondez pas cette URL avec l’identifiant de ressources uniforme URI. Le format est le même que pour une URL, mais le but est d’identifier un objet ou un élément, pas un lieu (tel qu’une page Web) où se trouve cet élément.

Sur un ordinateur moderne, une application peut émettre une requête HTTP en faisant un appel destiné au système d’exploitation. Cela se résume à spécifier une chaîne de texte contenant l’adresse URL comme dans https://www.CodeHiddenLanguage.com. Dans un délai de réaction raisonnable, l’application reçoit une réponse HTTP contenant la page demandée. Si la requête ne peut pas aboutir, la réponse est un code d’erreur. Si le client demande un fichier inexistant sur le serveur (par exemple, https:// www.CodeHiddenLanguage.com/FakePage.html), la réponse sera celle du code standard 404 de page Web introuvable.

Entre la requête initiale et la réponse finale vont se succéder entre le client et le serveur différentes étapes plus ou moins sophistiquées.

L’adresse URL du site Web est relativement digeste pour un humain, mais c’est un pseudonyme de l’adresse numérique qui est le vrai identifiant de la page demandée, c’est-à-dire l’adresse IP (Internet Protocol) du style 50.87.147.75. Il s’agit ici d’une adresse au standard IP version 4, avec des nombres sur 32 bits. La nouvelle norme IP 6 dispose de 128 bits. Pour récupérer l’adresse IP à partir de l’adresse URL, le navigateur du client (ou une autre application) interroge un serveur spécial nommé DNS (Domain Name System), qui joue le rôle d’annuaire en gérant les correspondances entre URL et adresses IP.

L’adresse IP d’un site est fixe ; même l’ordinateur du visiteur (le client) possède nécessairement une adresse IP, sauf qu’elle lui est en général attribuée parmi celles disponibles localement par le fournisseur d’accès. De nombreux équipements viennent s’ajouter aux ordinateurs dans les domiciles, en particulier les appareils de domotique et de l’Internet des objets IoT. Ils obtiennent une adresse IP accessible localement.

Dans tous les cas, le dialogue entre client et serveur pour visiter une page Web en émettant une requête HTTP se fonde sur un jeu de protocoles nommé TCP/IP (Transmission Control Protocol/Internet Protocol). Ces protocoles stipulent comment distribuer chaque fichier en paquets et quelles informations ajouter dans l’en-tête, notamment les adresses IP de la source et de la destination, adresses qui ne changent pas pendant les transits d’un routeur à un autre du chemin connectant client et serveur. L’en-tête contient aussi les adresses MAC source et destination qui, elles, changent chaque fois que les données passent par un nouveau routeur.

La plupart des routeurs contiennent une table de routage ou une règle de routage permettant de trouver le chemin le plus efficient pour progresser vers le serveur pour la requête et vers le client pour la réponse. Le routage des paquets de routeur en routeur est sans doute l’aspect le plus complexe d’Internet.

Lors du premier accès à un site tel que MonSiteDemo.com, vous allez sans doute saisir l’adresse suivante dans le navigateur :

MonSiteDemo.com

Notez que les adresses Web ne distinguent pas les majuscules des minuscules. Saisir monsitedemo.com revient au même, mais c’est moins lisible.

Le navigateur ajoute d’office en préfixe la mention https s’il s’agit d’une requête HTTP. Cette adresse de base ne précise aucun nom de fichier ou de page. Ce cas est prévu : la configuration du serveur de MonSiteDemo.com stipule le nom du fichier à renvoyer quand rien n’est précisé. Pour la plupart des sites, c’est le fichier default.html. Le résultat est identique à l’adresse complète suivante :

MonSiteDemo.com/default.html

C’est la page d’accueil du site. Notez que vous pouvez à tout moment voir le code source HTML de la page affichée au moyen d’une commande du navigateur (par exemple, Code source de la page dans le menu des outils).

Dès que le navigateur récupère le contenu du fichier demandé (default. html, par exemple), il le passe au peigne fin ; c’est la phase d’analyse évoquée dans le Chapitre 27. Il s’agit d’avancer caractère par caractère dans le code HTML en détectant chaque balise (traitement constitué d’une batterie de tests et de sauts) pour mettre en forme l’aspect visuel, travail pris en charge par un moteur de rendu HTML généralement écrit en langage C++. Enfin, pour afficher la page, le navigateur profite des fonctions graphiques dont est capable le système d’exploitation.

Pendant l’analyse du fichier .html, le navigateur peut détecter une directive demandant d’utiliser les règles de formatage d’un fichier lié nommé, par exemple, style.css. Il s’agit d’un fichier texte conforme à la syntaxe CSS (Cascading Style Sheet) qui décrit avec précision l’aspect visuel choisi pour la page affichée. Le navigateur émet une nouvelle requête HTTP pour que ce fichier lui soit envoyé. Un peu plus loin dans le fichier .html, le navigateur peut détecter une référence à un fichier image au format JPEG nommé Code2Cover.jpg, qui contiendrait une image de la couverture du livre. Une troisième requête HTTP permettrait de le rapatrier.

Ainsi, la page d’accueil du site compagnon contient ensuite la liste des chapitres avec les exemples. Le navigateur ne charge pas le contenu de toutes ces autres pages, mais propose des hyperliens sur lesquels il suffit de cliquer.

Si vous cliquez, par exemple, sur le lien du Chapitre 6 de la version anglaise, la requête va demander l’URL https://www.codehiddenlanguage.com/Chapter06. Ce n’est pas un nom de fichier. Le serveur consulte sa liste et trouve un dossier nommé Chapter06, mais il ne contient pas de fichier default.html à afficher. En revanche, il contient un fichier index.html qui est donc renvoyé.

Le navigateur analyse le fichier reçu qui fait référence au même fichier de styles style.css que la page d’accueil générale. Mais le navigateur a fort à propos conservé le fichier dans un cache local en supposant qu’il en aurait encore besoin. Cela lui évite d’en redemander le transfert. Le fichier index. html contient plusieurs balises <iframe> qui référencent d’autres fichiers HTML qui seront téléchargés à la demande. Ils comportent des balises de blocs de code JavaScript entre un jeu de balises <script>. Ces fichiers JavaScript sont alors téléchargés, puis le code JavaScript est analyse et exécuté.

Dans les premiers temps, le code JavaScript était interprété par le navigateur Web au fur et à mesure. De nos jours, les navigateurs Web sont dotés d’un moteur de compilation JavaScript qui travaille sur le modèle du juste-à-temps : l’outil ne compile pas la totalité du programme, mais seulement la partie qu’il a besoin d’exécuter. On parle de compilateur JiT (Just in Time).

Le site Web d’accompagnement de ce livre propose des images interactives, mais le fond de page HTML est statique. Il est pourtant possible de fournir du contenu dynamique, préparé à la volée par le serveur avant d’être envoyé. En effet, lorsqu’un serveur reçoit une adresse URL, il peut en faire ce qu’il veut, et peut donc créer du contenu HTML à la demande qu’il transmet ensuite au client.

Il arrive que l’adresse URL soit complétée par une ou plusieurs chaînes de requête. En général, c’est la partie qui suit le signe ? séparateur, les chaînes étant séparées par des signes &. Ces chaînes sont évidemment analysées et interprétées par le serveur. Les serveurs supportent aussi le style URL nommé REST (REpresentational State Transfer) dans lequel des données sont transmises du serveur vers le client sous forme de fichiers de données. Ces différents mécanismes sont dits du côté serveur parce que les programmes qui les réalisent se situent de son côté. En revanche, JavaScript est un langage de programmation du côté client. Un programme JavaScript du côté client peut ainsi interagir avec un programme du côté serveur.

L’extrême diversité des sites Web témoigne du choix énorme en matière de techniques de programmation Web. L’augmentation régulière des volumes de stockage et des traitements sur les serveurs ont fait apparaître progressivement une sorte d’entité à distance pouvant se comparer à un nuage et que l’on appelle le cloud. Plus les données personnelles des utilisateurs sont implantées dans cet infonuage, moins les utilisateurs dépendent du contenu de leur ordinateur personnel. Grâce au nuage, l’expérience informatique bascule de centrée matériel vers centrée utilisateur.

Que croyez-vous que Wells ou Bush auraient pensé d’Internet ?

Tous deux étaient convaincus qu’il était indispensable pour les humains d’améliorer l’accès aux connaissances et aux savoirs du monde. Qui pourrait ne pas en convenir ? Pourtant, fournir cet accès ne fait pas automatiquement entrer une civilisation dans son âge d’or. De nos jours, les gens sont plus souvent saturés par la quantité d’informations disponibles que confiants dans leur capacité à gérer ce flot.

Si l’on considère qu’Internet représente une sélection d’un très grand nombre de types de gens, de personnalités, de croyances et de centres d’intérêt, on peut convenir que c’est bien en quelque sorte devenu un cerveau mondial. Pourtant, ce n’est pas l’interprétation partagée de la réalité que désirait Wells. Les connaissances authentiques sont à peine plus accessibles que les nombreuses fausses sciences, infoxes et théories du complot.

Je pense que Wells aurait adoré l’idée de Google Books (books.google.com) qui résulte de l’effort de numérisation de nombreux livres et magazines des plus prestigieuses bibliothèques. Tous les livres tombés dans le domaine public sont de ce fait entièrement accessibles. Malheureusement, les créateurs de Google Books semblent avoir négligé l’intérêt des cartes de catalogue ; ils obligent ainsi l’utilisateur à dépendre totalement de mécanismes de recherche qui sont en eux-mêmes sérieusement biaisés. Ce problème crucial rend la recherche d’éléments spécifiques dans Google Books follement difficile.

Diamétralement opposé à Google Books, nous trouvons JSTOR (www.jstor.org). Abréviation de Journal Stockage, c’est une collection de journaux académiques organisée de façon très méticuleuse et efficace. Au départ, l’accès à JSTOR était limité, mais le site est devenu beaucoup plus accessible au grand public à la suite d’un tragique événement : le suicide d’un programmeur, persécuté alors qu’il cherchait à rendre le contenu de JSTOR librement accessible.

Si vous savez lire une partition de musique occidentale, vous trouverez votre bonheur sur le site du projet de librairie de partition internationale IMSLP (International Music Score Library Project). C’est l’équivalent de Google Books pour les partitions musicales. IMSLP est un énorme référentiel de partitions numérisées, toutes libres de droits et organisées et indexées de façon très efficace.

Si l’on songe aux idées émises par Vannevar Bush et Ted Nelson à propos de la possibilité de créer chacun notre propre réseau de documents, il semble que nous n’y soyons pas encore. Les sites tels que Google Books, JSTOR et LMSLP n’acceptent pas les liens spécifiques qui auraient permis de construire ces Web personnels. Les traitements de texte et tableurs actuels permettent d’ajouter des liens vers des sources d’information externes, mais ce n’est pas très souple.

Le site Web le plus proche du concept d’encyclopédie mondiale de Wells est évidemment Wikipédia (wikipedia.org). Son concept fondateur de Wikipédia est d’être une encyclopédie pouvant être enrichie par ses utilisateurs. Cette idée aurait pu facilement dégénérer au point de rendre le site chaotique et inutilisable, mais grâce à la supervision studieuse et consciencieuse de Jimmy Wales (né en 1966), Wikipédia est devenu le site unique le plus précieux d’Internet.

Dans son article sur le cerveau mondial, H.G. Wells avait écrit ceci :

Une Encyclopédie destinée à l’humanité entière ne doit accepter aucun dogme réducteur si elle n’accepte pas simultanément les critiques correctives. Elle devra être protégée de façon éditoriale avec le plus grand soin de l’invasion continuelle par la propagande réductrice. Elle devra adopter une posture générale que la plupart des gens vont considérer comme du scepticisme. Quelle que soit la force avec laquelle ils sont vénérés, les mythes doivent être considérés comme des mythes et non comme des interprétations symboliques d’une vérité supérieure ou de tout autre diversion similaire. Les visions, projets et théories doivent pouvoir être clairement distingués des faits concrets. L’Encyclopédie devra, par nécessité, s’opposer avec force aux grandiloquents récits nationalistes comme aux hypothèses sectaires. Elle devra par nécessité être en faveur et non indifférente à la communauté mondiale dont elle doit pouvoir devenir un élément essentiel. Si ces critères constituent selon vous des biais et des présupposés, l’Encyclopédie mondiale devra certainement être biaisée. Elle sera biaisée car elle ne pourra faire autrement que privilégier l’organisation, la comparaison, la construction et la création. Ce sera essentiellement un projet créatif. Elle devra constituer la contribution majeure à la bonne organisation de la croissance vers un Monde Nouveau.

Voilà des objectifs ambitieux ; il est remarquable de voir à quel point Wikipédia est proche de ses attentes.

Je suppose que la plupart d’entre nous n’ont pas le même optimisme que Wales qui est persuadé que mettre à disposition un énorme corpus de connaissances peut aider le monde à se forger un meilleur futur. On dit parfois que si nous le construisons, cela se produira, mais quelle que soit la force de nos convictions, rien n’est garanti. La nature humaine déjoue souvent les espérances.

Nous devons néanmoins faire tout ce qui est en notre pouvoir pour y parvenir.

1 N.d.T. : seul le chapitre 3 en a été traduit en français.

Code – Le langage caché du matériel et des logiciels de nos ordinateurs

Authorized translation from the English language edition, entitled Code : The Hidden Language of Computer Hardware and Software 2nd Edition by Charles Petzold, published by Pearson Education, Inc, publishing as Microsoft Press, Copyright © 2023 Charles Petzold.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

French language edition published by Éditions First, un département d’Édi8, Copyright © 2023.

Traduction autorisée de l’américain de Code : The Hidden Language of Computer Hardware and Software 2nd Edition de Charles Petzold, publié par Pearson Education, Inc. sous la marque Microsoft Press © 2023 Charles Petzold.

Édition française © 2023 Éditions First, un département d’Édi8

92 avenue de France 75013 Paris – France

Tél. : 01 44 16 09 00 Fax : 01 44 16 09 01

Courriel : firstinfo@editionsfirst.fr

Site Internet : www.editionsfirst.fr

ISBN : 978-2-412-08746-6

Dépôt légal : novembre 2023

Traduction de l’américain : Olivier Engler

Mise en page : Enredos e Legendas Unip. Lda

Corrections : Stéphanie Durteste

Édition : Julien Templier

Tous droits réservés. Toute reproduction, même partielle, du contenu, de la couverture ou des icônes, par quelque procédé que ce soit (électronique, photocopie, bande magnétique ou autre) est interdite sans autorisation par écrit d’Édi8 et de Pearson Education.

Limites de responsabilité et de garantie. L’auteur et l’éditeur de cet ouvrage ont consacré tous leurs efforts à préparer ce livre. Pearson Education, Édi8 et les auteurs déclinent toute responsabilité concernant la fiabilité ou l’exhaustivité du contenu de cet ouvrage. Ils n’assument pas de responsabilité pour ses qualités d’adaptation à quelque objectif que ce soit, et ne pourront être en aucun cas tenus responsables pour quelque perte, profit ou autre dommage commercial que ce soit, notamment mais pas exclusivement particulier, accessoire, conséquent, ou autre.

Marques déposées. Toutes les informations connues ont été communiquées sur les marques déposées pour les produits, services et sociétés mentionnés dans cet ouvrage. Pearson Education et Édi8 déclinent toute responsabilité quant à l’exhaustivité et à l’interprétation des informations. Tous les autres noms de marques et de produits utilisés dans cet ouvrage sont des marques déposées ou des appellations commerciales de leur propriétaire respectif. Pearson Education et Édi8 ne sont liés à aucun produit ou vendeur mentionné dans ce livre.

Le format ePub a été préparé par Isako www.isako.com à partir de l’édition papier du même ouvrage.