Aller au contenu principal

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.

Arduino
int nombreDePassages = 0; // entier
float temperature = 21.5; // nombre à virgule
bool porteOuverte = false; // vrai ou faux
char lettre = 'A'; // un caractère
const int BROCHE_LED = 13; // constante : ne peut plus être modifiée
void setup() {}
void loop() {}

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.

Une constante plutôt qu'un nombre magique
Écrire 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.

TypeOctetsPlage de valeursEmploi typique
bool1true ou falseun état
byte10 à 255une valeur de PWM, un octet reçu
char1-128 à 127un caractère
int2-32 768 à 32 767un compteur, une lecture analogique
unsigned int20 à 65 535un compteur qui ne descend jamais
long4environ ±2,1 milliardsun grand entier
unsigned long40 à environ 4,3 milliardsle temps, retourné par millis()
float46 à 7 chiffres significatifsune mesure physique
Un int de l'Uno n'est pas un int d'ordinateur
Sur un ordinateur, un 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.

Arduino
int a = 7, b = 8;
float faux = (a + b) / 2; // 7.0 : la division entière est faite AVANT la conversion
float juste = (a + b) / 2.0; // 7.5 : 2.0 est un float, la division devient décimale

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.

Arduino
byte niveau = 255;
void setup() {
niveau = niveau + 1; // niveau vaut 0 : un byte repart à zéro après 255
}
void loop() {}

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.

Arduino
void setup() {
delay(60000UL); // une minute : le suffixe UL fait un calcul en unsigned long
}
void loop() {}
Le temps ne se range jamais dans un int
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.

Arduino
int temperature = 32;
void setup() {
pinMode(8, OUTPUT);
}
void loop() {
if (temperature > 30) {
digitalWrite(8, HIGH); // ventilateur en marche
} else {
digitalWrite(8, LOW);
}
}
OpérateurSensOpérateurSens
==égal&&et
!=différent||ou
<, >, <=, >=comparaisons!non
= n'est pas ==
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.

Arduino
int mode = 2;
void setup() {
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
}
void loop() {
switch (mode) {
case 1:
digitalWrite(9, HIGH);
break;
case 2:
digitalWrite(10, HIGH);
break;
default: // aucun cas ne correspond
digitalWrite(9, LOW);
digitalWrite(10, LOW);
break;
}
}

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.

Arduino
void setup() {
pinMode(13, OUTPUT);
for (int i = 0; i < 5; i++) { // i prend les valeurs 0, 1, 2, 3, 4 : cinq tours
digitalWrite(13, HIGH);
delay(300);
digitalWrite(13, LOW);
delay(300);
}
}
void loop() {}

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.

Compter les tours, pas les deviner
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.

Arduino
void setup() {
pinMode(2, INPUT);
pinMode(13, OUTPUT);
}
void loop() {
while (digitalRead(2) == HIGH) { // tant que le bouton est enfoncé
digitalWrite(13, HIGH);
}
digitalWrite(13, LOW);
}

Locale ou globale

Définition

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.

Arduino
int appuis = 0; // globale : conserve sa valeur d'un passage à l'autre
void setup() {
Serial.begin(9600);
pinMode(2, INPUT);
}
void loop() {
if (digitalRead(2) == HIGH) {
appuis++;
Serial.println(appuis); // affiche 1, 2, 3 au fil des appuis
delay(300);
}
}

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

Vérification rapideon peut se reprendre

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

  1. 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.
  2. Repérer chaque division et se demander si ses deux opérandes sont entiers.
  3. Calculer les constantes qui dépassent 32 767 avec un suffixe UL.
  4. Compter les tours d'une boucle en listant les valeurs du compteur sur un petit cas.
  5. Placer chaque variable : locale si elle ne sert qu'à un passage, globale si elle doit durer.
  6. 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 int occupe 2 octets et plafonne à 32 767.
  • La division entière tronque : 7 / 2 vaut 3, et déclarer le résultat en float n'y change rien.
  • Un dépassement de capacité ne produit aucune erreur, seulement un résultat faux. 60 * 1000 en 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 ; chaque case d'un switch se termine par break.
  • Une variable locale à loop() est recréée à chaque passage ; une globale conserve sa valeur.