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.
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>.
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.
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.
À 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.
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.
64 + 8 = 72
- décimal
- 72
- hexadécimal
- 0x48
- caractère
- Hchr(72)
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 :
| Position | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
|---|---|---|---|---|---|---|---|---|
| Bit | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
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
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.
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écimal | Binaire | Hexadécimal |
|---|---|---|
| 10 | 00001010 | 0A |
| 72 | 01001000 | 48 |
| 122 | 01111010 | 7A |
| 255 | 11111111 | FF |
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.
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.
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 :
Voici les caractères qu'on va manipuler dans le protocole :
| Caractère | Code | Binaire |
|---|---|---|
< | 60 | 00111100 |
> | 62 | 00111110 |
: | 58 | 00111010 |
0 | 48 | 00110000 |
A | 65 | 01000001 |
H | 72 | 01001000 |
K | 75 | 01001011 |
O | 79 | 01001111 |
S | 83 | 01010011 |
i | 105 | 01101001 |
Deux pièges classiques, qui coûtent en général une heure de débogage :
- Le caractère
0n'est pas le nombre 0.ord("0")vaut 48, pas 0. Si on transmet le caractère0, 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 envoieoket une station qui attendOKne se comprendront pas.
Pour coder un mot entier, on parcourt ses caractères :
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 bits | Ce 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
0x43
ASCII, un seul octet
apoint 97· 1 octet
0x61
ASCII, un seul octet
fpoint 102· 1 octet
0x66
ASCII, un seul octet
époint 233· 2 octets
0xC3
ouvre une séquence de 2
0xA9
continuation
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 :
é, 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.
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.
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 :
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).
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.
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.
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 ».
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 :
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.
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.
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é ^.
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.
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 :
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
Bases, octets, encodage et trames.
- Prédire les bits d'un octet3 · Confirmé
- Écrire un octet en binaire2 · Fonctions
- Relire un octet reçu2 · Fonctions
- Le code ASCII d'un caractère2 · Fonctions
- Retrouver le caractère reçu2 · Fonctions
- Coder un mot en ASCII3 · Confirmé
- Isoler les quatre bits de poids faible3 · Confirmé
- Calculer une somme de contrôle3 · Confirmé
Coder un point de code en UTF-8, reconnaître un octet de continuation, et compter des octets plutôt que des caractères.
- Débogage : la longueur qui compte les octets3 · Confirmé
- Coder un caractère en UTF-84 · Avancé
- Reconnaître un octet de continuation3 · Confirmé