Le langage
Ce que ce chapitre apporte
- Choisir le type d'une variable d'après la plage de valeurs qu'elle doit contenir.
- Prévoir le résultat d'une division entière et d'un dépassement de capacité.
- Écrire une condition et une boucle, en évitant les pièges de ponctuation propres au C.
- Compter exactement les tours d'une boucle for.
- Distinguer une variable locale d'une variable globale, et savoir laquelle survit d'un passage dans loop() au suivant.
Le langage d'Arduino est du C++, et il est bavard là où le pseudo-code était souple : chaque variable a un type, chaque instruction finit par un point-virgule, chaque bloc s'ouvre et se ferme par une accolade. La logique des conditions et des boucles ne change pas. Ce qui change, ce sont deux conséquences d'une mémoire minuscule : un nombre a une taille, et un calcul peut en sortir sans prévenir.
Déclarer avant d'utiliser
Une variable se déclare avec son type, et le type ne change plus ensuite.
Trois règles de ponctuation, dont l'oubli produit l'essentiel des erreurs de compilation des premières semaines : chaque instruction se termine par ;, chaque bloc est encadré par { et }, et les noms distinguent majuscules et minuscules, led et LED étant deux variables différentes.
BROCHE_LED au lieu de 13 dans tout le programme ne coûte rien à l'exécution. Le jour où la LED change de broche, une seule ligne change, et le programme se lit sans avoir à deviner ce que signifie ce 13.
Les types ont une taille
Sur un ordinateur, la taille d'un entier ne se remarque presque jamais. Sur l'Uno, avec 2 Ko de mémoire vive, elle se remarque tout le temps.
| Type | Octets | Plage de valeurs | Emploi typique |
|---|---|---|---|
bool | 1 | true ou false | un état |
byte | 1 | 0 à 255 | une valeur de PWM, un octet reçu |
char | 1 | -128 à 127 | un caractère |
int | 2 | -32 768 à 32 767 | un compteur, une lecture analogique |
unsigned int | 2 | 0 à 65 535 | un compteur qui ne descend jamais |
long | 4 | environ ±2,1 milliards | un grand entier |
unsigned long | 4 | 0 à environ 4,3 milliards | le temps, retourné par millis() |
float | 4 | 6 à 7 chiffres significatifs | une mesure physique |
int occupe généralement 4 octets. Sur l'Uno, il en occupe 2 et plafonne à 32 767. Un programme recopié depuis un exemple pour ordinateur peut donc dépasser cette limite sans que rien ne le signale.
Deux calculs qui surprennent
La division entière
Entre deux entiers, / rend un entier : la partie décimale est tronquée, sans arrondi. 7 / 2 vaut 3.
Le piège tient dans la première ligne. Déclarer le résultat en float ne change rien : le calcul (a + b) / 2 est entièrement fait entre entiers, et ce n'est qu'ensuite que 7 est rangé dans un float.
Le dépassement de capacité
Quand un calcul sort de la plage de son type, le résultat n'est plus le bon, et aucune erreur ne se produit.
Le cas le plus traître vient des constantes. 60 * 1000 est calculé entre deux int, et 60 000 ne tient pas dans un int de l'Uno : le résultat vaut -5 536. Passé à delay, qui attend un unsigned long, ce nombre négatif devient 4 294 961 760 millisecondes, soit 49,7 jours d'attente.
millis() rend le nombre de millisecondes écoulées depuis le démarrage. Rangé dans un int, il dépasse 32 767 au bout de 32,7 secondes, et toute comparaison de durées devient fausse. Une variable qui contient un instant se déclare toujours unsigned long.
Les conditions
La structure est celle du pseudo-code, la ponctuation en plus.
| Opérateur | Sens | Opérateur | Sens |
|---|---|---|---|
== | égal | && | et |
!= | différent | || | ou |
<, >, <=, >= | comparaisons | ! | non |
if (mode = 2) ne compare pas : il affecte 2 à mode, et la condition vaut vrai puisque 2 n'est pas nul. Le programme compile, le test est toujours vrai, et mode a été modifié au passage. La comparaison s'écrit ==.
Quand une même variable est comparée à plusieurs valeurs, switch est plus lisible qu'une suite de if. Chaque cas se termine par break, sans quoi l'exécution continue dans le cas suivant.
Les boucles
loop() répète déjà tout le programme. Une boucle for ou while sert à répéter à l'intérieur d'un passage.
L'en-tête du for contient trois parties séparées par des points-virgules : l'initialisation, faite une fois, la condition, testée avant chaque tour, et l'incrément, fait après chaque tour.
for (int i = 0; i < n; i++) fait n tours. for (int i = 0; i <= n; i++) en fait n + 1, parce que la valeur n est elle aussi traitée. La règle pratique : partir de 0 et s'arrêter strictement avant n, ce qui est aussi la forme qui convient aux tableaux du chapitre suivant.
while répète tant qu'une condition est vraie, et convient quand le nombre de tours n'est pas connu d'avance.
Locale ou globale
Une variable locale est déclarée dans une fonction : elle naît à chaque exécution de cette fonction et disparaît à la fin.
Une variable globale est déclarée hors de toute fonction : elle existe pendant toute la vie du programme et garde sa valeur.
La distinction a une conséquence directe sur loop(). Un compteur déclaré dans loop() est recréé à zéro à chaque passage : il ne compte jamais au-delà de 1.
Exercices type
Avec a = 7 et b = 8, que vaut float moyenne = (a + b) / 2; et comment obtenir 7,5 ?
moyenne vaut 7.0. Le calcul (a + b) / 2 est fait entre deux entiers : 15 divisé par 2 donne 7, et ce 7 est ensuite rangé dans le float.
Il suffit qu'un des deux opérandes soit décimal pour que la division le devienne : (a + b) / 2.0 donne 7.5. On peut aussi convertir explicitement : (float)(a + b) / 2.
Un programme doit faire clignoter une LED 5 fois. Il contient for (int i = 0; i <= 5; i++). Combien d'éclairs produit-il ?
Six. i prend les valeurs 0, 1, 2, 3, 4 et 5, soit six tours.
La correction consiste à s'arrêter strictement avant 5 : for (int i = 0; i < 5; i++). Compter à la main les valeurs prises par i sur un petit cas reste la vérification la plus sûre.
Un compteur d'appuis déclaré par int appuis = 0; au début de loop() affiche toujours 1. Pourquoi ?
La déclaration est exécutée à chaque passage dans loop(). Le compteur est donc remis à zéro juste avant chaque test, et vaut 1 au mieux après un appui.
Il faut le déclarer hors de loop(), en variable globale. Il est alors initialisé une seule fois, au démarrage, et conserve sa valeur d'un passage à l'autre.
delay(60 * 1000) devait attendre une minute. Le programme semble bloqué. Pourquoi ?
60 * 1000 est calculé entre deux int. Le résultat, 60 000, dépasse 32 767 et vaut en pratique -5 536 sur l'Uno. Converti en unsigned long pour delay, il devient 4 294 961 760 millisecondes, près de 50 jours.
L'écriture correcte force un calcul sur quatre octets : delay(60000UL), ou delay(60UL * 1000).
Vérification
1.Que vaut 7 / 2 entre deux entiers ?
2.Sur l'Uno, quelle est la plus grande valeur d'un int ?
3.Combien de tours fait for (int i = 0; i <= 5; i++) ?
4.Que fait if (mode = 2) ?
5.Dans quel type ranger la valeur rendue par millis() ?
6.Un compteur déclaré au début de loop() reste bloqué à 1. Pourquoi ?
La méthode
- Choisir le type d'après la plus grande valeur possible, et non d'après la valeur courante. Le temps va dans un
unsigned long. - Repérer chaque division et se demander si ses deux opérandes sont entiers.
- Calculer les constantes qui dépassent 32 767 avec un suffixe
UL. - Compter les tours d'une boucle en listant les valeurs du compteur sur un petit cas.
- Placer chaque variable : locale si elle ne sert qu'à un passage, globale si elle doit durer.
- Relire chaque condition à la recherche d'un
=qui devrait être==.
Synthèse
- Toute variable a un type, qui fixe sa taille et sa plage de valeurs.
- Sur l'Uno, un
intoccupe 2 octets et plafonne à 32 767. - La division entière tronque :
7 / 2vaut 3, et déclarer le résultat enfloatn'y change rien. - Un dépassement de capacité ne produit aucune erreur, seulement un résultat faux.
60 * 1000en est l'exemple le plus courant. - Le temps se range dans un
unsigned long. for (int i = 0; i < n; i++)fait exactement n tours.=affecte,==compare ; chaquecased'unswitchse termine parbreak.- Une variable locale à
loop()est recréée à chaque passage ; une globale conserve sa valeur.