Apprendre à programmer l'Arduino en langage C,
Un tutoriel de Francesco Balducci, traduit par F-leb
Le 2016-09-27 19:06:55, par f-leb, Responsable Arduino et Systèmes Embarqués
Si vous êtes férus d'Arduino et que vous souhaitez vous mettre au langage C, ce tutoriel est peut-être fait pour vous :
ou comment apprendre la programmation du microcontrôleur Atmel AVR de l'Arduino en véritable langage C, au cœur des registres de la puce, sans passer par l'EDI standard, et sans utiliser le fameux « langage Arduino ».
J'adore jouer avec ma carte Arduino UNO et son environnement de développement graphique. C'est un outil qui facilite grandement la programmation en dissimulant une foule de détails. D'un autre côté, je ne peux m'empêcher d'observer dans les coulisses de l'EDI, et j'ai senti le besoin de me rapprocher des aspects matériels, de m'écarter des bibliothèques fournies par défaut et de l'EDI Java pour compiler directement mes programmes à partir de la ligne de commandes.
Francesco Balducci
Francesco Balducci
Du code source en langage C jusqu'au téléversement dans la carte Arduino avec la chaîne de compilation avr-gcc.
Bonne lecture, et bon développement, en vrai langage C...
-
Vincent PETITModérateurSalut Gérard,
Alors justement non, la variable i n'appartient pas à la fonction func, elle est globale car elle est déclarer et créer en dehors de toutes fonctions.
Ci dessous la variable i est globale et sa portée atteint tout le fichier .c dans le quel elle est. Cette variable sera créée dans ce qu'on appelle le "tas" dans la RAM.
Code C : 1
2
3
4
5
6
7int i; void func(void) // fonction attendre { i = 0; while (i == 0); }
Ci dessous la variable i est locale et sa portée est limitée à la fonction func. Cette variable sera créée dans la "pile" qui se trouve dans la RAM, au moment où on entre dans la fonction. Puis elle est détruite lorsqu'on sort de la fonction.
Code C : 1
2
3
4
5void func(void) // fonction attendre { int i = 0; while (i == 0); }
Ce qui se passe dans le code ci dessous est très subtile et on comprend le problème en regardant l'assembleur généré par le compilateur.
Code C : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int i; //variable globale interrupt (TIMERA0_VECTOR) irq_routine(void) // a chaque débordement du TIMERA, disons toutes les secondes { i = 42; } void func(void) // fonction attendre { i = 0; while (i == 0); // tant que i == 0 alors on tourne en rond } int main(void) // programme principal qui ne fait pas grand chose { func(); printf("si j'arrive ici, c'est que i ne vaut plus 0 et que je suis sortie de la function func()\n"); return 0; }
Si on le déroule on voit que le main lance la fonction func qui initialise la variable i à 0 puis tant que cette dernière est à 0 on tourne en rond. Sans routine d'interruption on a simplement réussi a planter le micro ou plutôt à le faire entrer une boucle infinie. Pour être plus précis la variable i va d'abord être mis dans un registre de travail pour être mis à jour avec la valeur 0 dans la RAM puis pour manipulation, donc ce registre sera mis à 0 (valeur de la variable i) puis testé en boucle avec la valeur 0 dans une boucle via l'ALU (l'unité arithmétique et logique).
Ensuite arrive l'interruption du TIMERA, une seconde après le démarrage du micro. Le micro va donc empiler (stocker dans la pile qui se trouve dans la RAM) les registres de travail, le status register et le pointeur de pile, pour retrouver ces petits et recommencer à travailler là où il était une fois que le routine d'interruption sera terminée.
Le programme d'interruption va affecter la valeur 42 à la variable i qui se trouve en RAM
Sortie de la fonction d'interruption, le micro va dépiler tout ce qu'il avait empiler avant d'être interrompu et le CPU va être rechargé avec les valeurs d'avant l'interruption pour reprendre où il était.
Malheureusement il était dans une boucle infinie qui ne fait que comparer un registre qui vaut 0 avec la valeur 0. Malgré que la variable i vaut bien 42 en RAM pourquoi diable est ce que le CPU irait relire la variable i en RAM ? Il ne fait que reprendre ce qu'il faisait. Et ce qu'il faisait c'est juste une comparaison dans le while (i == 0);. D'ailleurs il n'y a pas d'affectation dans cette instruction donc pas d'excuse pour aller causer avec la RAM ou recharger un registre de travail.
Note qu'avec des variables locales, ce phénomène bien tordu n'arrive pas. Si on a besoin impérativement d'une variable globale, il faut spécifier avec GCC (compilateur de Arduino) : volatile int i; pour forcer le compilateur a ajouter des instructions qui consistent justement à aller recharger le registre de travail avec la valeur dans la variable globale i, à chaque manipilation de celle ci, moyennant une petite perte de temps d'accès à la RAM à chaque fois.
Je ne sais pas si c'était plus clair comme ça ?
A+le 21/07/2018 à 20:51 -
Vincent PETITModérateurNon non pas du tout !
C'est que j'ai pensé a un problème très sournois du genre :
Code C : 1
2
3
4
5
6
7
8
9
10
11
12int i; interrupt (TIMERA0_VECTOR) irq_routine(void) // a chaque débordement du TIMERA { i = 42; } void func(void) // fonction attendre { i = 0; while (i == 0); }
- Lorsque la fonction "func" sera appelée, "i" va être mis dans un registre de travail pour manipulation, donc ce registre sera mis à 0 puis testé avec la valeur 0 dans une boucle.
- Puis le TIMERA va déborder et la routine d'interruption va s'exécuter en stoppant tout (on empile), l'emplacement mémoire de "i" va valoir 42.
- Enfin, on sort de l'interruption, on retourne dans le code de la fonction "func" (on dépile) et le registre qui valait 0, avant, et qui est testé avec la valeur 0 n'a aucune raison de se mettre à jour avec la nouvelle valeur de "i", soit 42 !
Et c'est la boucle infinie.
Pire encore, si jamais les options d'optimisation sont activées alors GCC va voir tout de suite une comparaison avec la valeur 0 et un registre qui est mis a 0.... et il risque de retirer la condition qui pour lui ne sert strictement a rien et c'est vrai puisqu'on teste 0 avec 0. Ce problème se règle facilement avec le mot clé volatile qui va forcer le compilateur a aller voir l'emplacement mémoire de la variable "i" à chaque vérification ou manipulation.
Code C : volatile int i;
D'où la frousse que j'ai eu en voyant ce _BV(bit) que je ne connaissais pas et la phrase :
En C, on a l'habitude d'utiliser [...], mais le compilateur reconnaît ce genre d'accès et produit un code d'assemblage optimisé dans le cas d'opérations bit par bit, sans opération de lecture supplémentaire.
C'est rien Vincent, ce n'est qu'une macro, ça va aller le 04/10/2016 à 15:25 -
DeliasModérateurBonsoir
Pour le coup j'ai lu ce tuto à la sortie du suivant.
Juste pour "optimisation", en fait c'est bien de le connaître, une écriture sur le registre PINx inverse les sorties dont la valeur écrite est à 1.
Donc la boucle
Code : 1
2
3
4
5
6
7
8
9while(1) { /* set pin 5 high to turn led on */ PORTB |= _BV(PORTB5); _delay_ms(BLINK_DELAY_MS); /* set pin 5 low to turn led off */ PORTB &= ~_BV(PORTB5); _delay_ms(BLINK_DELAY_MS); };
Code : 1
2
3
4
5while(1) { /* toggle pin 5 to blink LED */ PINB = _BV(PORTB5); _delay_ms(BLINK_DELAY_MS); };
Je manque d’expérience en C, les AVR je les travaille en ASM. Je ne peux pas apporter de réponse à vos questions sur le volatile.
Bonne soirée.
Deliasle 15/11/2016 à 22:36 -
Vincent PETITModérateurMerci pour ce tuto Fabien !
Voilà qui pourrait intéresser les copains du forum C qui n'ont qu'un petit pas à franchir pour jouer sur un microcontrôleur.
Sur le rôle de la macro _BV
En C, on a l'habitude d'utiliser les opérateurs d'affectation bit à bit |= et &= pour lire ou écrire dans une variable, mais le compilateur reconnaît ce genre d'accès et produit un code d'assemblage optimisé dans le cas d'opérations bit par bit, sans opération de lecture supplémentaire.
Tu es entrain de me faire peur car je n'avais jamais constaté de différence entre :
Code C : DDRB |= _BV(DDB5);
Code C : DDRB |= 0x20;
Code C : DDRB = DDRB | 0x20;
A bientôt,le 27/09/2016 à 23:56 -
f-lebResponsable Arduino et Systèmes EmbarquésSalut Vincent
Je ne te suis pas, finalement cela revient au même, non ?
http://www.nongnu.org/avr-libc/user-...5a93800b3d9546
#define _BV(bit ) (1 << (bit))
#include <avr/io.h>
Converts a bit number into a byte value.
Note
The bit shift is performed by the compiler which then inserts the result into the code. Thus, there is no run-time overhead when using _BV().
le 30/09/2016 à 18:30 -
Vincent PETITModérateurOui oui mais lorsque j'ai lu cette phrase, qui introduit _BV dans le tuto :
En C, on a l'habitude d'utiliser les opérateurs d'affectation bit à bit |= et &= pour lire ou écrire dans une variable,
Mais a partir du mais justement :
mais le compilateur reconnaît ce genre d'accès et produit un code d'assemblage optimisé dans le cas d'opérations bit par bit, sans opération de lecture supplémentaire.et que _BV est né de ça !
Je me suis donc interrogé sur ce point.
Je sais que sur GCC, et même d'autre compilateur, les options de compilations peuvent avoir un effet désastreux en programmation embarqué car des variables en attentes du matériel ou d'un registre peuvent être éliminées du source car le compilateur pense qu'elles ne servent a rien. Certain flag dans des registres se mettent a 0 après une simple lecture et là aussi certain compilateur, voyant dans le code un simple lecture, vont essayer d'optimiser et malheureusement ça fini en "comme la variable ne fait que lire une adresse et ne fait rien de cette valeur, pas de calcul ni d'opération" bah c'est qu'elle ne sert pas au final !
A+le 01/10/2016 à 22:06 -
f-lebResponsable Arduino et Systèmes Embarquésbon, je ne suis pas assez calé
je n'ai pas compris ce "mais" comme ca, mais c'est peut-être un souci dans la traduction:
In C we use the bitwise “|=” and “&=” assignment operators, which usually read and write a variable, but the compiler recognizes those kind of accesses generating optimized assembly in case of bit-wise operations, and there is no read operation involved.le 02/10/2016 à 17:07 -
neuneutrinosMembre actifDans ma tête quand je vois ça :
une voix me dit "mais pourquoi il n'utilise pas un xor ?"
A part ce détails des plus utile...Code : _BV(bit) 1<<(bit)
1<<(DDB5) peut être déterminé lors de la compilation comme une constante, et donc ne pas d'opération superflue lors de l’exécution.( pas d'opération de décalage inutile lors de l’exécution )
Je l'ai compris comme ça.le 04/10/2016 à 9:24 -
Vincent PETITModérateurJe suis d'accord et pour tout dire, je cherchais une raison technique à cette macro _BV(bit) mais c'est simplement pour des raisons de lisibilité/compréhension du programme qu'elle est défini.
Je dois probablement avoir un raisonnement beaucoup trop électronique (binaire/assembleur/registre) car effectivement, faire :
Code C : DDRB |= (_BV(DDB0) | _BV(DDB1) | _BV(DDB2)); // mettre a en sortie PB0, PB1 et PB2
Code C : DDRB |= 0x03; // mettre a en sortie PB0, PB1 et PB2
le 04/10/2016 à 15:17 -
F6EEQMembre régulierBonjour à tous,
J'ai repris le tuto sur "ARDUINO en C".
Cette discussion est vieille, mais toujours d'actualité, et je me pose une question:Enfin, on sort de l'interruption, on retourne dans le code de la fonction "func" (on dépile) et le registre qui valait 0, avant, et qui est testé avec la valeur 0 n'a aucune raison de se mettre à jour avec la nouvelle valeur de "i", soit 42 !
Pour moi le "int i " du début vaut pour la "boucle principale" donc le programme d'interruption, mais pas pour la fonction "func"... ou me trompe-je (!).
Bon, je ne suis pas un grand spécialiste du C , mais j'ai au moins compris la portée des variable, ou du moins je pense
Gérard.le 21/07/2018 à 14:49