Consignes
Le rendu de TP se fera uniquement à travers l'interface web TPLab. Vous devrez fournir un fichier .c contenant le code des fonctions demandées et éventuellement des fichiers annexes pour la question 12.
Vous pouvez utiliser le fichier Makefile fournit pour faciliter la compilation.
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 qui sont définies dans le fichier Makefile. (Vous pouvez également ajouter les options -Wno-unused-parameter -Wno-unused-function.)
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
Suite aux demandes répétées de nombreux étudiants, voici un petit tutoriel sur les fichiers Makefile (lien local : Makefile).
Je vous conseille de lire le fichier et de suivre les étapes décrites à l'intérieur. (À un autre moment que pendant le TP !)
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.
Utilisation du code
-
Le code fourni contient une fonction
main
dans un fichier main.c que vous ne devez pas modifier. Cette fonction permet de lancer le programme compilé depuis la ligne de commande. -
Votre code devra être écrit dans le fichier harry-potter.c. (Sauf les tests, qui seront dans tests-harry-potter.c.)
-
Une des options disponibles est -T qui permet d'exécuter la fonction
perform_tests
du fichier test-harry-potter.c. La fonction fournie gère un ou deux tests. À vous de l'étendre de manière pertinente. -
Le code définit 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
).
Voila un exemple d'exécution :
$ make clean
rm -f *.o a.out
$ make
gcc -Wall -std=c99 -Wextra -pedantic -Werror -Wno-unused-parameter -Wno-unused-function -c -o main.o main.c
gcc -Wall -std=c99 -Wextra -pedantic -Werror -Wno-unused-parameter -Wno-unused-function -c -o correction.o correction.c
gcc -Wall -std=c99 -Wextra -pedantic -Werror -Wno-unused-parameter -Wno-unused-function -c -o tests-correction.o tests-correction.c
gcc -Wall -std=c99 -Wextra -pedantic -Werror -Wno-unused-parameter -Wno-unused-function main.o correction.o tests-correction.o -o golay
$
$ ./golay -h
Utilisation :
./golay options
actions sur un mot :
-c MOT code le mot de 12 bits sur 23 bits
-C MOT code le mot de 12 bits sur 24 bits
-s MOT calcule le syndrome du mot 23 bits
-d MOT décode (avec correction d'erreurs) le mot de 23 bits sur 12 bits
-D MOT décode (avec correction d'erreurs) le mot de 24 bits sur 12 bits
-e MOT décode (avec correction d'erreurs exhaustive) le mot de 23 bits sur 12 bits
-E MOT décode (avec correction d'erreurs exhaustive) le mot de 24 bits sur 12 bits
tests aléatoires :
-n NB nombre de mots à tester
-p X probabilité d'erreur sur chaque bit
divers :
-x utilise la base hexadécimal plutôt que la base 2
-v augmente le niveau des affichages de débogage
-T ARG lance la fonction de test
-h ce message
$
$ ./golay -T binaire 12648430
en binaire, le mot 0x00c0ffee s'écrit 0b00000000110000001111111111101110
$
1.1. Opérations bit à bit
Nous utiliserons le type uint32_t
(entiers 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 (ou 3339 en décimal).
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.
Il est possible de manipuler directement les bits des entiers du langage C grâce aux opérations bit-à-bit.
Les opérations sont le « et », le « ou» et le « ou-exclusif » :
-
le « et », donné par l'opérateur binaire
&
:&
0 1 0 0 0 1 0 1 -
le « ou », donné par l'opérateur binaire
|
:|
-1 1 0 0 1 1 1 1 -
le « ou exclusif » (aussi appelé XOR), donné par l'opérateur binaire
^
:^
0 1 0 0 1 1 1 0 -
la négation est donnée par l'opérateur unaire
~
:~
0 1 1 0
Appliqués à des entiers (ou des char
), les opérateurs
&
, |
, ^
et ~
agissent
sur chaque bit indépendamment.
attention, les opérateurs &&
et ||
ne sont
pas des opérateurs bit à bit !
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.
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. 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:
$ ./golay -T binaire 12648430
en binaire, le mot 0x00c0ffee s'écrit 0b00000000110000001111111111101110
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 !
La lecture suivantes est réservée aux hackers qui se demandent comment
minimiser le nombre d'opérations de la fonction poids
: https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation.
Aucun point supplémentaire ne sera donné si vous choisissez une des versions de Wikipedia, mais vous risquez de perdre un point si vous n'êtes pas capable d'expliquer le fonctionnement de votre code !
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 (12+11) bits. Comme la partie gauche de la matrice est la matrice identité, cela signifie que le codage de w commence par w lui même et est suivi de 11 bits de redondance. Le décodage est 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, les bits de redondance de 011000000000 sont obtenus avec 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) ;
Privilégiez les opérations bit à bit dans votre fonction...
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. Il faudra donc coller ces bits de redondance derrière le mot initial pour obtenir le codage d'un mot.
Vous pouvez tester la fonction code_golay depuis la ligne de commande :
$ ./golay -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) ;
Cette fonction est très simple car il suffit de supprimer les bits de redondance avec un décalage !
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 :
$ ./golay -C 110100110
code_golay_etendu: 000110100110 => 000110100110101011001011
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 utilisé 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 initial est correct. Si le syndrome est différent de 000000000, alors le mot initial 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 :
$ ./golay -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) et permet donc de corriger 3 erreurs parmi les 23 bits reçus.
Nous allons implémenter une méthode de correction 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 :
-
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.
Cette propriété vient du fait que la matrice génératrice utilisée est sous forme canonique (avec la matrice identité à gauche).
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.
On peut donc corriger w' en 00011000100100001101010.
Si on décale ce mot vers la gauche (pour annuler le décalage vers la droite précédent), en remettant le bit de poids fort à droite, on obtient 00110001001000011010100 qui est la correction du mot w initial.
Cette propriété vient du fait que le code de Golay est cyclique.
É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.
Attention, pour éviter des problèmes dans la suite, faites en sorte de
toujours renvoyer des mots de n
bits : les bits de poids fort en
dehors du résultat doivent être égaux a 0
.
Programmez la fonction
/*
* correction d'erreur pour le code de Golay en utilisant la méthode "error
* trapping" + cyclicité
*/
mot correction_golay(mot m) ;
Attention, cette fonction doit renvoyer un mot de Golay correct sur 23
bits. (C'est la fonction main
qui décode le résultat en enlevant
les 11 bits de redondance.)
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 :
$ ./golay -d 01110110101110100000010
correction_golay: 01110110101110100000010 => 111101101011
Note : la fonction appelée grâce à l'option -d fait la correction d'erreur puis un décalage pour supprimer les bits de redondance.
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
Attention, la fonction correction_exhaustive_golay
doit
renvoyer un mot du code de Golay, c'est à dire un mot de 23 bits.
2.6. Statistiques sur la correction
Le code de Golay a une capacité de 12/23 ≈ 0.522 : il envoie en moyenne 52.2% d'information et 47.8% de redondance.
On peut démontrer qu'avec une telle capacité, le meilleur taux d'erreur possible en sortie (en fonction du taux d'erreur en entrée) suit la courbe suivante :
Les options -p et -n de l'exécutable golay permettent de simuler des transmissions sur un canal binaire symétrique bruité.
$ ./golay -n 10000 -p 3
Tests sur un canal symétrique binaire avec taux d'erreur de 3.00%.
10000 mots ont été codés et envoyés
5202 mots ont étés modifiés (52.0200%)
7216 bits ont étés modifiés (3.1374%)
39 mots n'ont pas étés corrigés correctement (0.3900%)
132 bits n'ont pas étés corrigés correctement (0.1100%)
Utilisez ces options pour ajoutez des points sur les graphiques ci dessus
et interprétez le résultat attentivement. (Vous pouvez aussi utiliser
la fonction statistiques
directement dans votre fichier de
tests.)
N'oubliez pas d'ajouter les fichiers pertinents lors de votre soumission.