Aller au contenu principal

Des bits aux caractères

Ce que ce chapitre apporte

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

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.

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.

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.

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…

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 nous 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") nous 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é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.

Vérification

Vérification rapideon peut se reprendre

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

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

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

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

Synthèse

  • Une ligne électrique ne transporte que deux états : tout se ramène à des bits.
  • Un octet = 8 bits = un nombre de 0 à 255 = un caractère.
  • bin(n) et int(texte, 2) font l'aller-retour avec le binaire ; hex(n) et int(texte, 16) avec l'hexadécimal, qui écrit un octet en 2 caractères.
  • ord() donne le code ASCII d'un caractère, chr() fait l'inverse. Attention : ord("0") vaut 48.
  • format(n, "08b") et format(n, "02X") affichent proprement, avec un nombre de colonnes fixe.
  • (n >> d) & 1 isole le bit numéro d : on le fait glisser à droite, puis on regarde par le trou du masque.
  • ^ (OU exclusif) sert à calculer une somme de contrôle : même calcul des deux côtés, comparaison, et on jette si ça ne colle pas.

Mettre en pratique