Consignes
Le rendu de TP se fera uniquement à travers l'interface web TPLab. Vous devrez fournir un unique fichier .c contenant le code des fonctions demandées.
Vous pouvez utiliser le fichier Makefile fournit pour faciliter la compilation.
Certaines questions appellent à une réponse que vous pouvez mettre en commentaire dans votre fichier principal.
Attention : les points suivants ne rapportent rien, mais ne pas les respecter pourra retrancher jusqu'à 10 points sur la note finale :
-
votre fichier devra s'appeler "nom.c",
-
chaque fichier devra comporter un commentaire au début avec vos nom, prénom, filière et numéro du TP,
-
votre code doit compiler avec les options -Wall -Werror -Wextra -pedantic -std=c99, -
Le seul fait que votre programme fonctionne ne suffit pas pour avoir une bonne note. Les points suivants seront pris en compte :
-
l'architecture de votre programme (code découpé en fonctions etc.),
-
la lisibilité de votre programme (choix pertinent pour les noms de variables etc.),
-
la présence de commentaires aux endroits appropriés,
-
la présence de documentation pour vos fonctions.
Liens utiles
1. Préliminaires
Le but de ce TP est de comprendre le fonctionnement du code de Golay et d'en implémenter une version en C...
Nous utiliserons le type uint32_t
(mots non signés, sur 4
octets, défini dans stdint.h) pour représenter en interne les suites
de 12 bits et 24 bits :
typedef uint32_t mot ;
(Cette définition se trouve déjà dans le fichier tp1.h.)
Par exemple, la suite de 12 bits 110100001011 sera en fait représentée par 00000000000000000000110100001011, c'est à dire 0xd0b.
La norme du C ne spécifie pas la taille des entiers. Les int
(ou
unsigned
) peuvent être stockés sur 2 octets (16 bits). Les
long
ou unsigned long
sont toujours stockés sur 4
octets (32 bits) ou plus.
En pratique, les compilateurs modernes utilisent
-
1 octet (8 bits) pour les
char
et leurs variantes, -
2 octets (16 bits) pour les
short
et leurs variantes, -
4 octets (32 bits) pour les
int
et leurs variantes, -
8 octets (64 bits) pour les
long
et leurs variantes.
Le type uint32_t
est apparu dans la norme C99 et représente les
entiers non-signés sur 32 bits. On pourrait également utiliser les types
uint_least32_t
ou uint_fast32_t
qui ont au
moins 32 bits.
1.1. Opérations bit à bit
Il est possible de manipuler directement les bits des types du langage C grâce aux opérations bit-à-bit.
Les opérations sont :
-
le « et » :
-
0 & 0 = 0
-
0 & 1 = 1 & 0 = 0
-
1 & 1 = 1
-
-
le « ou » :
-
0 | 0 = 0
-
0 | 1 = 1 | 0 = 1
-
1 | 1 = 1
-
-
le « ou exclusif » (XOR) :
-
0 ^ 0 = 0
-
0 ^ 1 = 1 ^ 0 = 1
-
1 ^ 1 = 0
-
-
la négation :
-
~0 = 1
-
~1 = 0
-
Appliqués à des entiers (ou des char
), les opérateurs
&
, |
, ^
et ~
agissent
sur chaque bit indépendamment.
Deux autres opérations sont disponibles :
-
le décalage à droite, qui supprime des bits de poids faible et mets des
0
dans les bits de poids forts. Sur des entiers non signés, ça correspond à une division entière par une puissance de 2 :-
en binaire :
00010000 >> 2 = 00000100
, c'est à dire16>>2 = 4
en décimal, ou0x10>>2 = 0x04
en hexadécimal, -
en binaire :
01101100 >> 3 = 00001101
, c'est à dire0x6a>>3 = 0x0d
en hexadécimal,
-
-
le décalage à gauche, qui ... décale vers la gauche. Sur des entiers non signés, ça correspond à une multiplication par une puissance de 2, en tronquant le résultat s'il y a dépassement de capacité :
-
en binaire :
00010000 << 2 = 01000000
, c'est à dire16<<2 = 64
en décimal, ou0x10<<2 = 0x40
en hexadécimal, -
en binaire (sur des
char
) :01101100 << 3 = 01100000
, c'est à dire0x6a<<3 = 0x60
en hexadécimal.
-
Sur les entiers signés, les décalages agissent exactement comme des multiplications / divisions par des puissances de deux. Cela signifie que le bit de poids fort (le bit de signe) est conservé par ces opérations et que le décalage ne concerne que les autres bits. On parle de décalage arithmétique par opposition au décalage logique des entiers non signés.
Téléchargez l'archive contenant les fichiers du TP. Vous ne devrez modifier que le fichier harry-potter.c.
Définissez la fonction
int bit(int i, mot m);
qui donne le bit (0 ou 1) numéro i
du mot m
. Le
bit de poids faible aura le numéro 0 et le bit
de poids fort aura le numéro 31. Par exemple :
// test pour la question 1
mot m = 0x555; /* càd 00...000010101010101 en binaire */
int n = 3;
printf("Le bit numéro %d de %x est %u et le bit %d est %u.\n", n, m, bit(n,m), n+3, bit(n+3,m));
devra afficher
$ ./harry-potter -T 1
Le bit numéro 3 de 555 est 0 et le bit 6 est 1
Servez-vous de cette fonction pour écrire une fonction
void print_bin(mot u, int n) ;
qui affichera les n bit de poids faible d'un mot sans ajouter de saut de ligne en fin d'affichage. Par exemple:
m = 0xace;
printf("le mot '%x' est en fait ",m); print_bin(m,12); printf(".\n");
devra afficher
le mot 'ace' est en fait 101011001110.
-
Le fichier contient une fonction
main
que vous ne devez pas modifier. Cette fonction permet de lancer le programme compilé depuis la ligne de commande. Une des options disponibles est -T qui permet d'exécuter la fonctionperform_tests
. Vous devrez donc utiliser cette fonction pour effectuer vos tests... (Note : cette option prend un argument de typechar*
, qui est passé à la fonctionperform_tests
.) -
Le fichier contient également une fonction
DEBUG
qui s'utilise comme la fonctionprintf
. Elle a simplement un premier argument supplémentaire qui permet de contrôler l'affichage :-
si cet argument est à 0, le message est toujours affiché,
-
si cet argument est à 1, le message est seulement affiché si le programme est lancé avec l'option -v,
-
si cet argument est à 2, le message est seulement affiché si le programme est lancé avec l'option -vv,
-
etc.
-
-
L'exécutable propose d'autres options qui permettent de tester directement certaines fonctions (code_golay, code_golay_etendu, correction_golay+decode_golay).
1.2. Poids d'un mot
Écrivez la fonction
int poids(mot m);
qui renvoie le nombre de 1 dans un mots.
N'oubliez pas de tester votre fonction !
2. Le code de Golay
2.1. Codage
Une des matrices génératrices du code de Golay est [ I | A ] où I est la matrice identité sur 12 lignes / 12 colonnes et A est la matrice sur 12 lignes / 11 colonnes suivante :
1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 |
0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 |
0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |
0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0 |
0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 |
1 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 |
On code donc un mot de 12 bits en un mot de 23 bits. Comme la partie gauche de la matrice est la matrice identité, cela signifie que le codage de w est w.r où r contient les 11 bits de redondances ajoutés par le code. Le décodage est donc trivial: il s'agit d'un décalage à droite.
Cette matrice signifie que :
-
les bits de redondance du mot 100000000000 sont 10101110001 (première ligne),
-
les bits de redondance du mot 010000000000 sont 11111001001 (deuxième ligne),
-
... Les bits de redondance des autres mots sont codés par linéarité. Par exemple, pour 011000000000 on fait le XOR de la troisième et deuxième lignes de la matrice A.
Les matrices habituellement données pour le code de Golay sont différentes et plus "symétriques". La matrice donnée ici correspond à une présentation du code de Golay comme code cyclique engendré par le polynôme X11 + X9 + X7 + X6 + X5 + X + 1 dans F2[X] / (X23-1).
Comme les mots de bits sont représentée par des éléments du type mot (synonyme du type uint32_t) la matrice A doit être représentée par un tableau d'éléments du type mot:
mot golay_A[12] = { 0x571, ... };
-
Complétez la définition des dernières lignes de cette matrice (variable globale) par les constantes appropriées.
-
Vous pouvez maintenant écrire la fonction de codage, qui multiplie un mot par la matrice définie précédemment:
/* * code une suite de 12 bits (bits de poids faible d'un mot) en ajoutant * la redondance donnée par la matrice A * Le résultat (23 bits) est stocké dans les 23 bits de poids faible du * mot renvoyé. */ mot code_golay(mot entree) ;
Attention : c'est le bit de poids fort qui correspond à la ligne 0 de la matrice, et le bit de poids faible qui correspond à la ligne 11 de la matrice...
Note: par rapport à ce qui a été vu en cours, seuls les bits de redondance sont obtenus par multiplication de matrice. Le mot initial reste inchangé...
Privilégiez les opérations bit à bit dans votre fonction...
Vous pouvez tester la fonction code_golay depuis la ligne de commande :
$ ./harry-potter -c 110100111
code_golay: 000110100111 => 00011010011111110000110
Écrivez la fonction
/*
* décode un mot du code de golay un supprimant les bits de redondance
*/
mot decode_golay(mot m) ;
Le code de Golay étendu code une suite de 12 bits en une suite de 24 bits : il s'agit du code de Golay avec un bit de parité supplémentaire ajouté à droite.
Écrivez la fonction correspondante :
/*
* code de Golay étendu: 23 bit du code de Golay, avec un bit de parité
*/
mot code_golay_etendu(mot entree) ;
Vous pouvez tester la fonction code_golay depuis la ligne de commande :
$ ./harry-potter -C 110100111
code_golay_etendu: 000110100111 => 000110100111111100001100
2.2. Vérification
Pour vérifier si un mot de 23 bits est correct, il faut multiplier ce mot par une matrice de parité. La matrice de parité correspondant à la matrice génératrice est obtenue en collant une matrice identité 11 lignes / 11 colonnes sous la matrice A précédente :
1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 |
0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 |
0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |
0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0 |
0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 |
1 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 |
- | - | - | - | - | - | - | - | - | - | - |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Le résultat de cette multiplication est appelé le syndrome d'un mot. Si le syndrome (sur 11 bits) est 00000000000, alors le mot est un mot correct. Si le syndrome est différent de 000000000, alors le mot n'est pas correct.
Définissez la matrice de parité
/*
* matrice de parité pour le code de Golay
*/
unsigned golay_H[23] = {
...,
...,
};
puis écrivez la fonction qui calcule le syndrome (11 bits) d'un mot
/*
* syndrome d'un mot du code de Golay (sans bit de parité)
*/
mot syndrome_golay(mot m) ;
Par exemple :
$ ./harry-potter -s 11010111010101111000110
syndrome_golay: 11010111010101111000110 => 01111011000
2.3. Correction des erreur -- 1
Le code de Golay permet de détecter 6 erreurs (7 pour la version étendue). Il permet donc de corriger 3 erreurs parmi les 23 bits reçus. La méthode de correction est basée sur les propriétés suivantes du code :
-
la matrice génératrice est donnée sous forme standard (avec une matrice identité à gauche),
-
la matrice génératrice donne un code cyclique : si w est un mot correct, alors toutes les permutations circulaires de w sont également correctes.
Propriété 1: si le syndrome a un poids de 3 ou moins (càd a moins de trois 1) alors le syndrome contient exactement l'erreur.
Par exemple, w = 00100100000010110000001 a pour syndrome 00000100010.
Comme le syndrome ne contient que deux 1, cela signifie que le mot w a deux erreurs :
-
le deuxième bit en partant de la droite,
-
le sixième bit en partant de la droite.
La correction de w est donc 00100100000010110100011.
Ce type de correction n'est pas très intéressant car seuls les bits de redondance (les 11 bits de poids faible) peuvent être corrigés de cette manière.
Propriété 2: pour corriger un mot, il suffit de corriger une permutation circulaire (puis de la permuter circulairement à l'envers).
Par exemple, le mot w = 00110001001101011011100 a pour syndrome 00011101011.
La propriété précédente ne permet donc pas de le corriger.
Si on décale le mot w vers la droite, en remettant le bit de poids faible à gauche, on obtient w' = 00011000100110101101110.
Le syndrome de ce mot est 10100000100 qui ne contient que trois 1.
La propriété précédente permet de corriger w' en 00011000100100001101010.
Si on décale ce mot vers la gauche, en remettant le bit de poids fort à droite, on obtient 00110001001000011010100.
Écrivez la fonction qui prend un mot m de n bits en argument et fait le décalage de p bits vers la droite, en remettant les p bits de poids faible à gauche.
/*
* renvoie le décalage circulaire (à droite) de p bits sur un mot m de n bits
*/
mot decalage_circulaire(mot m, int n, int p) ;
Pour notre application, la valeur de n sera toujours 23.
Privilégiez les opérations bit à bit pour cette fonction.
Par exemple, sur le mot m = 11010101110 avec n=11 bits, le décalage vers la droite de 3 bits donne 11011010101.
Programmez la fonction
/*
* correction d'erreur pour le code de Golay en utilisant la méthode "error
* trapping" + cyclicité
*/
mot correction_golay(mot m) ;
Testez votre fonction sur des exemples:
-
choisissez un mot sur 12 bits,
-
codez le avec la fonction code_golay,
-
ajoutez (avec un XOR) une erreur choisie, comme par exemple 00000000000001100001000,
-
essayez de la corriger avec la fonction correction_golay,
-
comparer avec le mot du code sans erreur.
Par exemple :
$ ./harry-potter -d 01110110101110100000010
correction_golay: 01110110101110100000010 => 111101101011
Note : la fonction appelée grace à l'option -d fait la correction d'erreur puis un décalage afin de ne donner que les bits de données corrigés.
2.4. Correction des erreur -- 2
Lorsqu'il n'y a qu'un bit d'erreur, la méthode précédente fonctionne toujours !
-
Donnez un exemple d'erreur de 3 bits que la fonction précédente ne peut pas corriger,
-
donnez un exemple d'erreur de 2 bits que la fonction précédente ne peut pas corriger.
La méthode pour corriger toutes les erreurs de 2 ou 3 bits est donc un peu plus compliquée :
-
on essaie la méthode précédente
-
si ça ne fonctionne pas, on reprend le mot initial (incorrect) et on suppose que le premier bit est faux. On le change et on essaie de corriger en utilisant la méthode précédente.
-
si ça ne fonctionne pas, on reprend le mot initial (incorrect) et on suppose que le deuxième bit est faux. On le change et on essaie de corriger en utilisant la méthode précédente.
-
...
Attention, dans les étapes 2, 3, 4, ... on essaie de corriger 2 bits. Il faut vérifier que le poids du syndrome est inférieur à 2 au lieu de 3.
Parce que le code de Golay est parfait, cette méthode trouvera toujours une correction pour le mot.
Modifiez votre fonction correction_golay pour pouvoir corriger toutes les erreurs de 2 ou 3 bits.
./correction -d 01110110101110100010010
correction_golay: 01110110101110100010010 => 010100101011
2.5. Correction d'erreurs : recherche exhaustive
Une autre méthode de correction d'erreurs est la recherche exhaustive : on parcourt tous les mots corrects en les comparant au mot à corriger. Le résultat est le mot correct le plus proche (au sens de la distance de Hamming) du mot à corriger.
-
Programmez la fonction
correction_exhaustive_golay
, -
et comparez les temps d'exécution de
correction_exhaustive_golay
etcorrection_golay
en détaillant votre méthodologie.
Vous pouvez tester cette fonction avec l'option `-e` :
./correction -e 01110110101110100010010
correction_golay_exhaustive: 01110110101110100010010 => 010100101011
2.6. Pour aller plus loin
Écrivez une fonction qui lit des octets dans un fichier (fonction fread), les code avec le code Golay étendu, et écrit le résultat dans un autre fichier (fwrite).
Écrivez la fonction réciproque qui lit un fichier et corrige les erreurs avant d'écrire un fichier contenant le décodage correspondant.
Attention: un octet ne contient pas assez de bits pour être codé avec le code de Golay, mais deux octets contiennent trop de bits. Il faudra donc lire trois octets à la suite (24 bits) et les coder sur six octets (48 bits). (Et vice versa pour la fonction réciproque.)
Si le fichier en entrée ne contient pas assez d'octets, il faudra ajouter un "padding", qu'il ne faudra pas oublier d'enlever dans la fonction qui décode...
Lorsqu'un octet d'un fichier codé est modifié, il y a potentiellement 8 bits qui peuvent changer. Le code de Golay ne parviendra donc pas toujours à corriger un octet erroné.
Proposez une méthode qui permet de corriger plus facilement ce types d'erreurs (sur un octet entier), sans ajouter de bits de redondances supplémentaires.
Implémentez ce protocole dans vos fonctions de la question précédente.