Aller au contenu principal

Binaire, hexadécimal et code ASCII

Ce que ce chapitre apporte 6 points
  • Expliquer ce qu'est un bit, un octet, et pourquoi une machine ne manipule que des 0 et des 1.
  • Convertir un nombre entre décimal, binaire et hexadécimal, à la main et avec Python.
  • Coder un caractère en ASCII avec ord() et le décoder avec chr().
  • Afficher un nombre proprement avec format(), sur 8 bits ou sur 2 chiffres hexadécimaux.
  • Extraire les bits d'un octet un par un avec le décalage >> et le masque & 1.
  • Calculer une somme de contrôle avec le OU exclusif^ et dire à quoi elle sert.
Tout est octets : le binaire compte, l'hexadécimal abrège, et le code ASCII associe un nombre à chaque caractère. On va bientôt faire dialoguer deux cartes électroniques sur un simple câble à deux fils. Sur ce fil, il n'y a pas de mots, pas de lettres, pas de nombres : il y a une tension qui monte et qui descend. Pour transmettre le message OK, il faut d'abord savoir le transformer en une suite de 0 et de 1. Ce chapitre donne les briques : compter en binaire, coder un caractère, lire les bits un par un, et vérifier qu'un message n'a pas été abîmé en route. Le parcours d'architecture reprend les mêmes notations du côté du processeur, l'hexadécimal des adresses dans Registres et mémoire et les opérations bit à bit dans Masques binaires et décalages, et le chapitre de réseau sur l'adressage IPv4 les emploie sur un masque de sous-réseau, qui n'est rien d'autre qu'un ET bit à bit.

Pourquoi seulement des 0 et des 1

Dans un circuit, l'information n'est pas un chiffre : c'est une tension électrique. Un composant sait faire deux choses de façon fiable et rapide : laisser passer le courant, ou ne pas le laisser passer. On appelle ça un état haut et un état bas.

On pourrait imaginer une machine qui distingue dix niveaux de tension pour représenter les chiffres de 0 à 9. Le problème, c'est le bruit : un fil de trois mètres, un moteur qui démarre à côté, et 4,2 volts deviennent 3,9 volts. Avec dix niveaux, on ne sait plus lire. Avec deux niveaux bien séparés, il reste une marge énorme : « en dessous de la moitié, c'est un 0 ; au-dessus, c'est un 1 ». C'est pour cette robustesse que tout est codé en 0 et en 1 : les nombres, les lettres, les images, le message <S01:OK>.

Définition

Un bit (de l'anglais binary digit) est la plus petite information possible : un 0 ou un 1. C'est l'unité de base de tout ce qui circule sur le fil.

Le bit et l'octet

Un seul bit ne raconte pas grand-chose : allumé ou éteint. On les regroupe donc par paquets de huit.

Définition

Un octet est un paquet de 8 bits (byte en anglais). Avec 8 bits, on peut représenter 28 = 256 valeurs différentes, soit les nombres de 0 à 255.

Retenons l'ordre de grandeur : un caractère du message tient dans un octet. Le message OK fait 2 octets, donc 16 bits à faire passer sur le fil. La trame <S01:OK> fait 8 caractères, donc 64 bits.

main.py
Sortie
>_ Prêt à exécuter…

À retenir. 1 bit = 0 ou 1. 1 octet = 8 bits = un nombre de 0 à 255 = un caractère. Un message de N caractères, c'est 8 × N bits à faire passer sur le fil.

Vérification rapideon peut se reprendre

Combien de valeurs différentes un octet peut-il représenter ?

Compter en binaire

On compte en base 10 sans y penser. Dans le nombre 205, le 2 ne vaut pas 2 : il vaut 200, parce qu'il est à la place des centaines. Chaque position vaut dix fois plus que sa voisine de droite : 1, 10, 100, 1000…

En binaire, c'est la même mécanique, sauf que chaque position vaut deux fois plus que sa voisine de droite : 1, 2, 4, 8, 16, 32, 64, 128.

Huit cases, huit poids. La figure ci-dessous les rend manipulables : basculer une case change le total du poids écrit au-dessus, et la somme se réécrit à chaque clic.

128
64
32
16
8
4
2
1

64 + 8 = 72

décimal
72
hexadécimal
0x48
caractère
Hchr(72)
Un octet, poids par poids. La case de gauche vaut 128, celle de droite 1, et le nombre n'est rien d'autre que la somme de celles qui valent 1.
À manipuler
Basculer les cases une à une en partant de zéro et regarder la ligne de somme : chaque case ajoute exactement son poids, jamais autre chose. Allumer ensuite toutes les cases pour trouver la plus grande valeur qu'un octet peut porter, puis n'allumer que la case de gauche : le total obtenu est le poids de cette seule case, et c'est pourquoi un bit de plus double la portée. Chercher enfin la configuration qui affiche la lettre A, puis celle qui affiche a : un seul bit les sépare.

Lire un nombre binaire

Pour lire 01001000, on écrit la valeur de chaque position au-dessus des bits, puis on additionne les positions où il y a un 1 :

Position1286432168421
Bit01001000

Les 1 sont aux positions 64 et 8, donc 01001000 vaut 64 + 8 = 72. Et 72, on va le voir dans un instant, c'est le code de la lettre H.

Le bit tout à gauche (celui qui vaut 128) est le bit de poids fort : c'est celui qui pèse le plus lourd. Celui tout à droite (qui vaut 1) est le bit de poids faible. Cette distinction compte pour le projet : quand on envoie un caractère sur le fil, on doit décider dans quel ordre on envoie les bits, et l'autre équipe doit lire dans le même ordre, sinon elle reçoit n'importe quoi.

Dans l'autre sens : décimal vers binaire

Pour convertir 72 en binaire à la main, on descend la liste des poids, du plus grand au plus petit, et l'on se demande à chaque fois « est-ce que ça rentre ? » :

  • 128 rentre-t-il dans 72 ? Non → bit 0, il reste 72.
  • 64 rentre-t-il dans 72 ? Oui → bit 1, il reste 8.
  • 32 dans 8 ? Non → 0. 16 dans 8 ? Non → 0.
  • 8 dans 8 ? Oui → bit 1, il reste 0.
  • 4, 2, 1 dans 0 ? Non → 0, 0, 0.

Résultat : 01001000.

Python le fait pour soi

main.py
Sortie
>_ Prêt à exécuter…

Le préfixe 0b de bin() n'est pas un bit, c'est juste une étiquette qui dit « ce qui suit est du binaire ». On s'en débarrasse plus loin avec format().

L'hexadécimal, une écriture compacte

11111111 est pénible à lire, à écrire et à recopier sans se tromper. Les électroniciens utilisent donc une troisième écriture : l'hexadécimal, en base 16.

Définition

En hexadécimal, chaque position vaut seize fois sa voisine de droite, et on a besoin de seize symboles : 0 à 9, puis A, B, C, D, E, F pour 10, 11, 12, 13, 14, 15.

L'intérêt est purement pratique : un octet s'écrit toujours avec exactement deux caractères hexadécimaux, parce que 4 bits font exactement un caractère hexa. 11111111 s'écrit FF. 01001000 s'écrit 48.

DécimalBinaireHexadécimal
10000010100A
720100100048
122011110107A
25511111111FF
main.py
Sortie
>_ Prêt à exécuter…

C'est cette écriture qu'on retrouvera dans la somme de contrôle *7A à la fin d'un datagramme : 7A n'est pas « sept-A », c'est le nombre 122 écrit de façon compacte.

Vérification rapideon peut se reprendre

Pourquoi écrit-on les octets en hexadécimal plutôt qu'en binaire ?

Le code ASCII : des nombres pour les caractères

Une ligne électrique ne sait transporter que des nombres. Pour transmettre la lettre H, il faut donc que tout le monde soit d'accord sur le nombre qui représente H. Cet accord existe depuis 1963 et s'appelle l'ASCII.

Définition

Le code ASCII est une table qui associe un nombre à chaque caractère : A vaut 65, a vaut 97, 0 (le caractère zéro) vaut 48, l'espace vaut 32. L'ASCII d'origine tient sur 7 bits (0 à 127) ; on le range en pratique dans un octet.

En Python, deux fonctions font l'aller-retour :

main.py
Sortie
>_ Prêt à exécuter…

Voici les caractères qu'on va manipuler dans le protocole :

CaractèreCodeBinaire
<6000111100
>6200111110
:5800111010
04800110000
A6501000001
H7201001000
K7501001011
O7901001111
S8301010011
i10501101001

Deux pièges classiques, qui coûtent en général une heure de débogage :

  • Le caractère 0 n'est pas le nombre 0. ord("0") vaut 48, pas 0. Si on transmet le caractère 0, on transmet l'octet 48.
  • Majuscules et minuscules sont deux caractères différents. ord("A") vaut 65, ord("a") vaut 97. Une station qui envoie ok et une station qui attend OK ne se comprendront pas.

Pour coder un mot entier, on parcourt ses caractères :

main.py
Sortie
>_ Prêt à exécuter…
Vérification rapideon peut se reprendre

Le code ASCII de 'A' vaut 65. Que vaut celui de 'a' ?

Au-delà de l'ASCII : UTF-8

L'ASCII tient sur 7 bits, soit 128 caractères. C'est assez pour l'anglais, et manifestement pas pour le français, encore moins pour le grec, le japonais ou un symbole monétaire. Il a donc fallu un autre accord.

Unicode attribue un numéro, appelé point de code, à chaque caractère de chaque écriture : é porte le numéro 233, € le 8364, et il y en a aujourd'hui plus de cent cinquante mille. Unicode dit quel numéro, il ne dit pas comment l'écrire en octets. C'est le rôle d'un codage, et celui qui s'est imposé est UTF-8.

UTF-8 repose sur une idée simple : le nombre d'octets varie selon le caractère, et les premiers bits de chaque octet annoncent son rôle.

Premiers bitsCe que l'octet annonce
0…un caractère ASCII, tenant seul sur cet octet
110…l'ouverture d'une séquence de deux octets
1110…l'ouverture d'une séquence de trois
10…une continuation, jamais un début

Cpoint 67· 1 octet

01000011

0x43

ASCII, un seul octet

apoint 97· 1 octet

01100001

0x61

ASCII, un seul octet

fpoint 102· 1 octet

01100110

0x66

ASCII, un seul octet

époint 233· 2 octets

11000011

0xC3

ouvre une séquence de 2

10101001

0xA9

continuation

Les quatre caractères de « Café » codés en UTF-8. Les trois premiers tiennent sur un octet, le é en occupe deux, et les bits d'en-tête, en couleur, disent lequel ouvre et lequel continue.

Deux conséquences suivent de cette figure, et elles expliquent presque tout ce qu'on rencontre ensuite.

Un texte ASCII est déjà un texte UTF-8, octet pour octet. C'est ce qui a permis à UTF-8 de remplacer l'ASCII sans rien casser, et pourquoi un fichier anglais s'ouvre correctement quel que soit le réglage.

Une continuation ne peut pas être prise pour un début. Une lecture qui démarre au milieu d'une séquence s'en aperçoit et se resynchronise, ce qu'aucun codage à longueur fixe ne permet.

En Python, encode et decode font l'aller-retour entre texte et octets :

main.py
Sortie
>_ Prêt à exécuter…
Le mojibake, ou pourquoi « Café » devient « Café »
Latin-1 est un codage où chaque octet vaut un caractère. Relire de l'UTF-8 avec cette règle ne provoque aucune erreur : les deux octets du é, 0xC3 et 0xA9, sont simplement lus comme deux caractères distincts, Ã et ©.
C'est pourquoi le dégât est silencieux et ne touche que les lettres accentuées : la partie ASCII, codée à l'identique dans les deux, traverse intacte. Un texte à demi lisible est la signature de ce défaut.
main.py
Sortie
>_ Prêt à exécuter…

La règle qui évite tout le chapitre. Un flux d'octets ne porte pas son codage : rien dans le fichier ne dit en quoi il est écrit. Le codage est une information qui voyage à côté, dans un en-tête, une convention ou un paramètre.
D'où la seule discipline qui tienne : préciser encoding="utf-8" à l'ouverture d'un fichier, en lecture comme en écriture, sans compter sur le réglage de la machine. C'est ce que fait le chapitre sur les fichiers, et c'est pour cette raison.

Vérification rapideon peut se reprendre

Combien d'octets occupe la lettre « é » en UTF-8, et combien la lettre « e » ?

Afficher proprement avec format()

bin(72) affiche 0b1001000 : sept caractères, un préfixe parasite, et surtout pas les 8 bits qu'on veut envoyer sur le fil. Pour un chronogramme ou un tableau de trame, il faut toujours le même nombre de colonnes.

La fonction format() prend un nombre et une consigne d'affichage :

main.py
Sortie
>_ Prêt à exécuter…

Décode la consigne "08b" de gauche à droite : 0 = complète avec des zéros, 8 = sur 8 caractères au total, b = en binaire. De même, "02X" veut dire : complète avec des zéros, sur 2 caractères, en hexadécimal majuscule (x minuscule donnerait 0a au lieu de 0A).

main.py
Sortie
>_ Prêt à exécuter…

Lire les bits un par un

On sait transformer H en 72, et 72 en 01001000. Mais pour piloter un fil, il faut les bits un par un, dans l'ordre, parce qu'on ne peut poser qu'une seule valeur à la fois sur la ligne. Deux petits outils suffisent.

Le décalage >>

>> fait glisser tous les bits d'un nombre vers la droite. Les bits qui dépassent à droite tombent dans le vide.

main.py
Sortie
>_ Prêt à exécuter…

L'image mentale : les bits sont écrits sur un tapis roulant, et >> 3 fait avancer le tapis de trois crans vers la droite.

Le masque & 1

L'opérateur & compare deux nombres bit à bit et ne garde un 1 que là où les deux ont un 1. Le nombre 1 s'écrit 00000001 : il n'a un 1 qu'à la position du bit de poids faible.

L'image mentale : & 1, c'est poser sur l'octet un cache en carton percé d'un seul trou, tout à droite. On ne vois plus qu'un seul bit, celui du bas ; tout le reste est masqué. C'est d'ailleurs de là que vient le mot masque.

main.py
Sortie
>_ Prêt à exécuter…

Les deux ensemble

Combine les deux et on peut aller chercher n'importe quel bit : « fais glisser le bit qui m'intéresse tout à droite, puis regarde par le trou ».

main.py
Sortie
>_ Prêt à exécuter…

Pour envoyer l'octet sur le fil dans l'ordre naturel de lecture (poids fort d'abord), on parcourt les décalages de 7 jusqu'à 0 :

main.py
Sortie
>_ Prêt à exécuter…

range(7, -1, -1) compte à l'envers : 7, 6, 5, 4, 3, 2, 1, 0. La borne d'arrivée -1 n'est jamais atteinte, comme toujours avec range.

Astuce

format(n, "08b") donne le même résultat sous forme de texte, en une ligne. Alors pourquoi apprendre >> et & 1 ? Parce que sur la carte, on ne veut pas afficher les bits : on veut les poser un par un sur une sortie, et une sortie prend un nombre (0 ou 1), pas un caractère.

Vérification rapideon peut se reprendre

Un texte enregistré en UTF-8 et relu comme du Latin-1 donne quoi ?

Vérifier qu'un message est arrivé intact

Le message traverse un fil de plusieurs mètres, à côté d'une alimentation et d'autres cartes. Un bit peut basculer. La station qui reçoit <S03:T=20;V=80;MSG=OK> doit pouvoir se demander : « est-ce bien ce qui a été envoyé ? »

L'idée d'une somme de contrôle est simple : l'émetteur calcule un petit nombre à partir du message, et l'envoie à la suite. Le récepteur refait le même calcul sur ce qu'il a reçu. Si les deux nombres diffèrent, le message est abîmé et on le jette.

Le calcul le plus courant, parce qu'il est minuscule à écrire et rapide même sur un microcontrôleur, utilise le OU exclusif, noté ^.

Définition

Le OU exclusif (^) compare deux nombres bit à bit et met un 1 là où les deux bits sont différents. 1 ^ 1 donne 0, 0 ^ 0 donne 0, 1 ^ 0 donne 1.

main.py
Sortie
>_ Prêt à exécuter…

Deux propriétés le rendent parfait pour un contrôle d'erreur : n ^ n vaut toujours 0, et n ^ 0 vaut toujours n. On part donc de 0 et on combine tous les caractères du message :

main.py
Sortie
>_ Prêt à exécuter…

Le récepteur refait exactement le même calcul. Si un seul caractère a changé en route, la valeur ne tombe plus juste.

À retenir. Une somme de contrôle ne répare rien : elle dit seulement « quelque chose ne va pas ». Et elle ne vaut que si les deux équipes calculent exactement la même chose : sur quels caractères ? le < est-il compté ? et le > ? C'est précisément pour ça que le protocole doit être écrit noir sur blanc. Le *7A de l'énoncé du projet est un exemple : la valeur obtenue dépendra de la règle qu'on aura choisie.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Mettre en pratique