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 :

Le seul fait que votre programme fonctionne ne suffit pas pour avoir une bonne note. Les points suivants seront pris en compte :

  1. l'architecture de votre programme (code découpé en fonctions etc.),

  2. la lisibilité de votre programme (choix pertinent pour les noms de variables etc.),

  3. la présence de commentaires aux endroits appropriés,

  4. 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

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 :

Appliqués à des entiers (ou des char), les opérateurs &, |, ^ et ~ agissent sur chaque bit indépendamment.

Deux autres opérations sont disponibles :

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.
  1. 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 fonction perform_tests. Vous devrez donc utiliser cette fonction pour effectuer vos tests... (Note : cette option prend un argument de type char*, qui est passé à la fonction perform_tests.)

  2. Le fichier contient également une fonction DEBUG qui s'utilise comme la fonction printf. 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.

  3. 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 ]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.rr 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 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, ... };
  1. Complétez la définition des dernières lignes de cette matrice (variable globale) par les constantes appropriées.

  2. 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 :

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 :

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:

  1. choisissez un mot sur 12 bits,

  2. codez le avec la fonction code_golay,

  3. ajoutez (avec un XOR) une erreur choisie, comme par exemple 00000000000001100001000,

  4. essayez de la corriger avec la fonction correction_golay,

  5. 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 !

La méthode pour corriger toutes les erreurs de 2 ou 3 bits est donc un peu plus compliquée :

  1. on essaie la méthode précédente

  2. 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.

  3. 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.

  4. ...

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.

  1. Programmez la fonction correction_exhaustive_golay,

  2. et comparez les temps d'exécution de correction_exhaustive_golay et correction_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.