Cours-Programmation C pour systèmes embarqués
Transcription
Cours-Programmation C pour systèmes embarqués
Programmation C pour systèmes embarqués Sylvain MONTAGNY [email protected] Bâtiment chablais, bureau 13 04 79 75 86 86 Retrouver tous les documents de Cours/TP sur le site www.master-electronique.com Présentation des cours : Sommaire Cours : 10.5 h en 7 séances 1ère partie : Rappel sur le langage C (Exercices de base) 2ème partie : La programmation en langage C avancée 3ème partie : Présentation du TP : Réalisation d’un algorithme de compression de données. Université de Savoie 2 Présentation TP et examen TP : 20 h en 5 séances Le but de ce projet est d'écrire un programme de compression de fichiers textes, sans perte d'information. On demande également d'écrire un décompresseur qui devra restaurer le fichier original. L’algorithme proposé ici est l'algorithme de Huffman. Une note : 18 points : Examen de TP avec une partie théorique. 2 points proviennent du travail fait en TP Université de Savoie 3 1ère partie : Rappel sur le langage C (exercices de base) Donner l’exécution du code suivant : #include <stdio.h> #include <stdlib.h> int main(void){ unsigned char i; unsigned char tab[5]={1,2,4,8,16}; for (i=0;i<5;i++) { printf("Le %d element est %d\n",i+1,tab[i]); } return EXIT_SUCCESS; } Université de Savoie 4 1ère partie : Rappel sur le langage C (exercices de base) Donner l’exécution du code suivant : #include <stdio.h> int main() { int i,j; for(i=0;i<5;i++){ for(j=5-i;j<5;j++){ printf("++"); } printf("\n"); } return EXIT_SUCCESS; } Université de Savoie 5 1ère partie : Rappel sur le langage C (exercices de base) Écrire une fonction C calculant la longueur d'une chaîne de caractères, donnée en argument. Le prototype de la fonction est le suivant : int longueur(char *s) Université de Savoie 6 1ère partie : Rappel sur le langage C (exercices de base) Écrire une fonction C calculant la longueur d'une chaîne de caractères, donnée en argument. int longueur(char *s) { int n = 0; while (s[n] != '\0') { n++; } return n; } Université de Savoie 7 1ère partie : Rappel sur le langage C (exercices de base) Soit un texte donné par une chaîne de caractères. Le but est de compter le nombre d'occurrences de chaque lettre minuscule. Question 1 : Réaliser les déclarations suivantes : Le texte (chaîne de caractère constante en lettre minuscule) sera déclaré dans un tableau nommé « ch ». Vous afficherez la chaîne de caractère à l’écran. Un tableau d'entiers nommé « occ » permet de compter les occurrences de chaque lettre de l’alphabet. La taille du tableau est fixée par une constante (nombre de lettre de l’alphabet). Un pointeur nommé « p » pour parcourir le texte. Université de Savoie 8 1ère partie : Rappel sur le langage C (exercices de base) #include <stdio.h> #define NB_LETTRE 26; void main(void) { /* déclaration d'une chaîne <=> tableau de caractères. */ char ch[]="ceci est une chaîne de test"; /* déclaration d'un pointeur sur une chaîne de caracteres. */ char *p = ch; /* déclaration d'un tableau de 26 cases */ int occ[NB_LETTRE]; printf("Chaîne en mémoire : %s\n",ch); } Université de Savoie 1ère partie : Rappel sur le langage C (exercices de base) Question 2 : Initialiser le tableau d’occurrence à zéro : int i=0; for (i=0; i<NB_LETTRE;i++) occ[i]=0; Question 3 : Compter les occurrences jusqu’à la fin de la chaîne de caractère : while (*p != '\0') { if ( (*p >= 'a’) && (*p <= 'z’) ) { occ[*p-'a'] = occ[*p-'a'] + 1; } p++; } Question 4 : Afficher le contenu du tableau occ en précisant la lettre : le nombre de a est … le nombre de b est …. for (i=0; i<nb_lettres; i++) printf("Nombre de %c : %d\n", 'a'+i,occ[i]); Université de Savoie 10 2ème partie : La programmation en langage C avancée Lisibilité du code, les types de variables, les typedef, occupation mémoire, porté des variables, les opérateurs, manipulation de registre, les structures, les pointeurs, le main(), pointeurs, la pile, type de fonction, allocation dynamique, les options d’optimisations à la compilation, les erreurs classiques du C… 11 Lisibilité du code C (1) Exercice : Réaliser un code qui imprime les N premiers éléments d'un tableau d’entier A[ ] en insérant un espace entre les éléments et en commençant une nouvelle ligne après chaque dixième chiffre. void main(void){ int A[80],N,i; scanf("%d",&N); for (i=0; i<N; i=i+1){ printf("%d", A[i]); if ((i%10) == 9) printf("\n"); else printf(" "); } } //Saisie de N //Boucle N fois //Affichage contenu tableau //Test du 10ème chiffre 12 Lisibilité du code C (2) Règles à respecter : Mettre des commentaires explicites Ne pas trop compresser le code Respecter une homogénéité dans votre façon de coder. Déclarer des variables explicites Organiser la mise en page de votre code, ou respecter celle déjà existante. Université de Savoie 13 Lisibilité du code C (3) Voici une deuxième façon de coder, très pratique mais beaucoup moins lisible. void main(void){ int A[80],N,i; scanf("%d",&N); for (i=0; i<n; i++) printf("%d%c", a[i],(i%10==9)? ‘\n‘ : ' '); } Université de Savoie 14 Lisibilité du code C (4) Quelques équivalences… parfois à éviter Op. Fonction += Addition et affectation -= Soustraction et affectation *= Multiplication et affection /= Division et affectation %= Modulo et affectation ++ Incrémentation Exemple nombre += 5; Equivalence nombre=nombre+5 nombre -= 6; nombre *= 3; nombre /= 2; nombre %= 4; nombre = nombre % 4 nombre++; nombre = nombre + 1; -- Décrémentation nombre--; nombre = nombre - 1; y = x++ y = ++x y x x y = = = = Université de Savoie x x + 1 x + 1 x 15 Lisibilité du code C (5) #include <stdio.h> main(t,_,a)char*a;{return!0<t?t<3?main(-79,-13,a+main(-87,1_,main(-86,0,a+1)+a)):1,t<_?main( t+1, _, a ):3,main( -94, 27+t, a )&&t == 2 ?_ <13 ?main ( 2, _+1, "%s %d %d\n" ):9:16:t<0?t<-72?main( _, t,"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+ ,/#{l,+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/ +#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/"):t<-50?_==*a ?putchar(31[a]):main(-65,_,a+1):main((*a == '/') + t, _, a + 1 ):0<t?main ( 2, 2 , "%s"):*a=='/'||main(0,main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);} The International Obfuscated C Code Contest >> http://www.ioccc.org/ Les types de variables (1) char = 1 octet (signed ou unsigned) Int, long, double, float dépendent de la cible processeur utilisée. Afin de connaître la taille (en octet) d’une variable, on utilise la fonction sizeof() : printf("int=%d octets", sizeof(int)); Note : Il n’existe pas de type Booléen en C Université de Savoie 17 Les types de variables (2) Cas d’une compilation pour processeur 32 bits Voir le cas d’un compilateur C pour PIC16F ou PIC18F (microchip) Voir le cas d’un compilateur C pour DSP TMS320 (Texas Instruments) (page suivante) Université de Savoie 18 Les types de variables (3) Université de Savoie 19 Les types de variables (4) Donner la représentation binaire des nombres suivants : char a=64; unsigned char b=64; char c=128; unsigned char d=128; Quel est l’affichage des fonctions suivantes? printf("%d",a); printf("%d",c); printf("%u",a); printf("%u",c+a); Note : Une déclaration est signed par défaut généralement. Note : %d -> entier signed / %u -> entier unsigned Université de Savoie 20 Les types de variables (5) Conversion explicite : La conversion de type consiste à transformer un type de valeur en un autre, en respectant la syntaxe suivante (<type>) <expression> Conversion implicite : Lorsque les deux opérandes sont de type différent, le compilateur prévoit une conversion implicite suivant l'ordre : { char -> int -> long -> float -> double } et { signed -> unsigned } Université de Savoie 21 Les types de variables (6) Décrivez le conversion du type, ainsi que les affectations du code suivant : int main(void){ int n, m, l; double d; d = 5; n = (int) 3.4; n = 1; m = 5; d = 2; l = (int) (m / d); d = n / m; d = n / ((double) m); d = 'A' * 6.0 – m + 0xA2CL; return 0; } 22 Portée des variables Globale / Locale Les variables locales sont déclarées dans la fonction les utilisant. les variables globales sont déclarées en début de programme. Le linker attribue une adresse fixe et définitive à ces dernières (variable globale) pendant toute la durée du programme. L'utilisation de variables locales améliore considérablement la lisibilité et la sécurité des données dans un programme en C. Une variable locale existe exclusivement pendant l’exécution de la fonction où est déclarer la variable. Université de Savoie 23 Classes de stockage et qualificateurs Classes de stockage auto : Définition automatique pas utilisée car implicite. register : Demande au compilateur d’utiliser un registre (plus utile) static : La variable locale conserve sa valeur extern : La variable est déclarée dans un autre fichier Qualificateurs const : Spécifie une « variable » non modifiable volatile : Limite les effets de l’optimisation sur la variable Université de Savoie 24 Classe de stockage et qualificateur classe auto Cette classe n’est pas utilisé car elle est implicite pour les variables locales. En effet, Les variables locales sont par défaut "automatiques" , crées à l'entrée de la fonction qui les déclare et détruites à la sortie. Pour cela elles sont rangées dans la « pile». void f(void) { auto int j = 0; printf("j vaut %d",j); } void f(void) { int j = 0; printf("j vaut %d",j); } void main(void) { f(); } Mais jamais utilisée . . . 25 Classe de stockage et qualificateur classe static La classe static permet à une variable locale d’être persistante et donc de conserver sa valeur pendant les appels successifs de la fonction. void f(void) { static int i = 0; /* i ne sera initialisé qu’une fois*/ int j = 0; /* j sera initialisé à chaque fois */; i++; j++; printf("i vaut %d et j vaut %d.\n", i, j); } void main(void) { f(); f(); } Note : La classe « static » sur une variable globale ou une fonction aura pour objectif de privatiser l’objet au fichier où elle est déclarée. C’est-àdire qu’elle ne pourra pas être utilisée depuis un autre fichier. Classe de stockage et qualificateur classe extern (1) extern : permet de spécifier que la variable a été déclaré dans un autre fichier. /* File : ext.c */ void next(void); void next1(void); int a1=1; /* definition of external (non static)*/ void main(void){ a1=2; printf("a1=%d\n",a1); next(); next1(); printf("a1=%d\n”,a1); } 27 Classe de stockage et qualificateur classe extern (2) /* File file1.c */ /* File file2.c */ int b1=0; extern int a1; void next(void){ char a1; a1='a'; b1=77; } void next1(void){ float b1; b1=19.2; a1=13; } Si on omet le terme extern, une nouvelle variable est créer avec une nouvelle allocation mémoire. 28 Classe de stockage et qualificateur classe register On peut demander au compilateur de ranger une variable très utilisée dans un registre, à l’aide de la classe register. Le nombre de registres étant limité, cette requête ne sera satisfaite que s’il reste des registres disponibles. Cette technique permettant d’accélérer les programmes a aujourd’hui perdu tout son intérêt grâce aux performances des optimiseurs de code intégrés au compilateur. Documentation du compilateur MikroC : Université de Savoie 29 Classe de stockage et qualificateur classe register Le C définit des qualificateurs pouvant influer sur une variable : const : pour définir une variable dont la valeur ne doit jamais changer ; volatile : désigne une variable dont les accès ne doivent pas être optimiser par le compilateur. Cette variable sera relue depuis son emplacement d’origine à chaque accès. En effet, cela est important lorsque d’autre sources (périphérique matériel, processus, etc…) accède à la variable en même temps que notre programme. Une variable peut avoir plusieurs qualificateurs Université de Savoie 30 Classe de stockage et qualificateur Qualificateur const Le qualificateur const indique au compilateur que la valeur de la variable ne doit pas changer. Il est donc impératif d'assigner une valeur à la déclaration de la variable, sans quoi toute tentative de modification ultérieure entraînera une erreur de la part du compilateur : Étudier les codes suivants : const int i = 0; i = 1; void fonction( const char * pointeur ) { pointeur[0] = 0; pointeur = "Nouvelle chaîne de caractères"; } char * const pointeur = "Salut tout le monde !"; pointeur = "Hello world !"; const char * const pointeur = "Salut tout le monde !"; pointeur = "Hello world !"; pointeur[0] = 0; Université de Savoie 31 Classe de stockage et qualificateur Qualificateur volatile int * pReg = (int *) 0x 1234; while (* pReg == 0) { … } Le compilateur va optimiser la boucle while en considérant que la lecture de *pReg n’est jamais modifiée. En réalité, elle peut être modifiée par : Un périphérique d’entrée/sortie mappé en mémoire Une interruption Une autre tâche Université de Savoie 32 Évaluation des expressions booléennes (1) Le C ne possède pas de type booléen dédié. Dans ce langage, n'importe quelle valeur différente de zéro est considérée vraie, zéro étant considéré comme faux. Ce qui veut dire que n'importe quelle expression peut être utilisée à l'intérieur des tests (entier, réels, pointeurs, tableaux, etc.). Cela peut conduire à des expressions pas toujours très claires, comme : int a; a = une_fonction(); if (a) { /* ... */ } On préférera : int a; a = une_fonction(); if (a != 0) { /* ... */ } Attention : int a = 0; b = 2; if (a = b) { /* Le code qui suit sera toujours exécuté ... */ } Université de Savoie 33 Évaluation des expressions booléennes (2) Les opérateurs logiques de comparaisons (&& et ||, similaires sémantiquement à leur équivalent binaire & et |) ont une exécution totalement différente. Dans le cas du ET logique (&&), si l'opérande gauche s'évalue à faux (valeur zéro), on sait déjà que le résultat du ET sera faux et donc ce n'est pas la peine d'évaluer l'opérande droite. De la même manière si l'opérande gauche d'un OU logique (||) est évalué à vrai, le résultat sera aussi vrai (valeur !=0) et donc l'évaluation de l'opérande droite est inutile. if (z != 0 && a / z < 10) { printf("Tout va bien\n"); } Université de Savoie 34 Les manipulations de bits (1) Les manipulations de bits sont beaucoup utilisées dans l’embarqué. Pour contrôler un périphérique matériel, on retrouve des registres de 8, 16 ou 32 bits qu’il faut modifier. Mettre à 1 le bit 4 de a : unsigned int a = 0x000F; /* 0000 0000 0000 1111 */ Mettre à zéro le bit 3 de a : unsigned int a = 0x000F; /* 0000 0000 0000 1111 */ Faire une fonction int set_bit(int mot, int nbr) qui retourne le mot modifié à l’emplacement nbr. 35 Les manipulations de bits (1) Les manipulations de bits sont beaucoup utilisées dans l’embarqué. Pour contrôler un périphérique matériel, on retrouve des registres de 8, 16 ou 32 bits qu’il faut modifier. Mettre à 1 le bit 4 de a : unsigned a = 0x000F; /* 0000 0000 0000 1111 */ unsigned b = 0x0010; /* 0000 0000 0001 0000 */ unsigned c = a | b; /* 0000 0000 0001 1111 soit 0x001F */ Mettre à zéro le bit 3 de a : unsigned a = 0x000F; /* 0000 0000 0000 1111 */ unsigned b = 0xFFF7; /* 1111 1111 1111 0111 */ unsigned c = a & b; /* 0000 0000 0000 0111 soit 0x0007 */ Université de Savoie 36 Les manipulations de bits (2) Tester si le bit 2 de a est à 1 : unsigned a = 0x000F; /* 0000 0000 0000 1111 */ Tester si le bit 3 de a est à 1 et si le bit 15 est à 0 : unsigned a = 0x000F; /* 0000 0000 0000 1111 */ 37 Les manipulations de bits (2) Tester si le bit 2 de a est à 1 : unsigned a = 0x000F; /* 0000 0000 0000 1111 */ if (a & (1 << 2)) { printf("bit 2 = 1"); } else { printf("bit 2 = 0"); } Tester si le bit 3 de a est à 1 et si le bit 15 est à 0 : unsigned a = 0x000F; /* 0000 0000 0000 1111 */ if ( a & (1 << 3)!=0 && a&(1<<15)==0 ) { printf("bit 2 = 1 et bit 15=0"); } else { printf("bit 2 = 1 et bit 15=0 n’est pas vérifié"); } 38 Le main() Le main() est le point d’entrée d’une application. Dans un système embarquée sans système d’exploitation, le point d’entrée du programme sera précisé dans la phase d’édition de liens par l’initialisation du vecteur d’interruption nommé RESET. Université de Savoie 39 A quoi sert un pointeur ? Université de Savoie 40 Les tableaux (1) Un tableau est un regroupement consécutif de donnée de même type et de taille fixe. // Tableau à 1 dimension // Tableau à 2 dimensions void main(void){ void main(void){ int tableau[4]; int i; for(i=0;i<4;i++){ tableau[i]=0; int tableau[4][3]; int i,j; for(i=0;i<4;i++){ for(j=0;j<3;j++){ tableau[i][j]=0; } } } } } Université de Savoie 41 Les tableaux (2) tableau[0] tableau[1] tableau[2] tableau[3] tableau[0][0] tableau[0][1] tableau[0][2] tableau[1][0] tableau[1][1] tableau[1][2] tableau[2][0] tableau[2][1] tableau[2][2] tableau[3][0] tableau[3][1] tableau[3][2] Université de Savoie 42 Les tableaux (3) Passage de tableaux en paramètre des fonctions. Un tableau n’est jamais passé en paramètre, c’est son adresse qui est fournie. Université de Savoie 43 Organisation logicielle (1) 0xFFFFFFFF stack SP (Stack Pointer) address space heap (dynamically allocated) data segment Data : (static and global) 0x00000000 code segment (=program) Université de Savoie PC (Program Counter) 44 Organisation logicielle (2) Code segment : Emplacement ou se trouve le code compilé (programme) Data segment : Data : contient toutes les variables « static » et « globales » (initialisées ou non) Heap : Le tas est géré dynamiquement. C’est une zone de donnée qui grossi à la réservation de zone mémoire (malloc) et qui se réduit lors de la libération (free). Stack : C’est une zone de stockage de type LIFO. Elle contient les adresses de retour des fonctions et les variables locales. Université de Savoie 45 Organisation logicielle (3) Imposer une adresse physique pour une variable ou une constante Les registres des microcontrôleurs sont à des adresses imposées par le concepteur du µC, or le linker d'un compilateur C a le contrôle total des adresses, il choisit où sont rangées variables et constantes. On peut aussi imposer une adresse à une variable. Attention, il faut être certain que vous soyez le seul à utiliser cette adresse pour ne pas créer d’erreur de segmentation. char c; void main (void){ *(unsigned char *)0x80 = 0xAA; c= *(unsigned char *)0x80; } 46 Organisation logicielle (4) Directive du linker Il est possible de préciser à l’éditeur de lien l’endroit exact ou nous souhaitons que le code, les variables ou les constantes seront positionnées. Exemple : Pour le linker MikroC, on utilise les directives suivantes : Directive absolute : Directive absolute specifies the starting address in RAM for a variable or a starting address in ROM for a constant.. Directive org : Directive org specifies a starting address of a routine in ROM. Directive org is appended to the function definition. Directive orgall : If the user wants to place his routines, constants, etc, above a specified address in ROM, #pragma orgall directive should be used. Directive funcorg : You can use the #pragma funcorg directive to specify the starting address of a routine in ROM using routine name only. 47 Optimisation du code (1) Dans le contexte de l’embarqué, il est important de connaître les options de compilation d’optimisation. Il y a en effet un compromis à trouver entre la taille de l’exécutable produit et le temps d’exécution. Université de Savoie 48 Optimisation du code (2) Option -O0 (niveau 0) Option -O1 (niveau 1) Allocates variables to registers Performs loop rotation Eliminates unused code Simplifies expressions and statements Performs all -O0 optimizations, and: Removes unused assignments Eliminates local common expressions Option -O2 (niveau 2) (default optimization level) Performs all -O1 optimizations, and: Performs loop optimizations Université de Savoie 49