Aller au contenu principal

Le langage Arduino : setup et loop

Ce que ce chapitre apporte 7 points
  • 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, d'un reste, et d'un dépassement de capacité.
  • Expliquer un dépassement de capacité à partir des bits de la variable.
  • É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.
  • Lire un message d'erreur du compilateur et retrouver la ligne fautive.

Un programme Arduino tient en deux fonctions : setup, exécutée une fois, et loop, répétée sans fin. Le langage d'Arduino est du C++, proche parent du C dont le parcours dédié détaille les types et leur taille, 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.

La broche 13 est celle de la LED soudée sur la carte, LED_BUILTIN, présentée au chapitre précédent : les programmes de ce chapitre qui la pilotent se testent sans aucun montage.

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
double4identique à float sur l'Unoaucun gain de précision ici

Ces plages ne sont pas arbitraires : elles se déduisent du nombre de bits. Un octet contient 8 bits, qui prennent 28=2562^8 = 256 combinaisons différentes. Un type non signé les emploie toutes pour les nombres positifs, de 0 à 255. Un type signé en consacre la moitié aux négatifs, de −128 à 127. Sur 16 bits, cela donne 216=65 5362^{16} = 65\,536 combinaisons : de 0 à 65 535 sans signe, de −32 768 à 32 767 avec.

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.
Ce n'est pas une particularité d'Arduino mais une règle du C : la norme ne fixe pas la taille d'un int, seulement un minimum, et c'est chaque machine qui décide. Le chapitre Les types et leur taille du parcours C la fait mesurer par sizeof, et c'est le même programme qui rend 4 sur un ordinateur et 2 sur l'Uno.

Deux calculs qui surprennent

La division entière, et son reste

Entre deux entiers, / rend un entier : la partie décimale est tronquée, sans arrondi. 7 / 2 vaut 3. L'opérateur % rend ce qui reste : 7 % 2 vaut 1, parce que 7=3×2+17 = 3 \times 2 + 1.

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
void setup() {}
void loop() {}

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 reste a un usage qui revient dans presque tous les montages à plusieurs états : faire tourner un compteur en rond. (etape + 1) % 4 vaut successivement 1, 2, 3, puis 0, et ainsi de suite, sans jamais atteindre 4.

Arduino
int etape = 0;
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println(etape); // 0, 1, 2, 3, 0, 1, 2, 3...
etape = (etape + 1) % 4; // après 3 vient 0 : le compteur tourne en rond
delay(500);
}

C'est la manière la plus sûre d'enchaîner les quatre phases d'un feu tricolore, ou les huit LEDs d'un chenillard : l'indice ne peut jamais sortir de la plage, quel que soit le nombre de passages.

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() {}

La raison se voit sur les bits. 255 s'écrit 11111111 : les huit bits sont à 1. Ajouter 1 produit une retenue qui se propage jusqu'au bout, et donnerait 100000000, un nombre de neuf bits. La variable n'en a que huit : la retenue de gauche n'a nulle part où aller, elle se perd, et il reste 00000000.

byte : 1 octet, de 0 à 255128643216842111111010byte x = 250

Les cases pleines sont les bits à 1. La valeur est la somme de leurs poids.

Un byte, bit par bit. Au-dessus de chaque case, le poids du bit : la valeur est la somme des poids des bits à 1. Au-delà de 255, la retenue sort à gauche et se perd.
À manipuler
Ajouter 1 jusqu'à 255 en regardant les bits se remplir par la droite, puis ajouter 1 encore une fois. Revenir à 250 et ajouter 10 : le résultat, 4, se retrouve en posant l'addition, 260 − 256 = 4. Enfin, partir de 0 et retirer 1 : un byte ne descend pas sous zéro, il repart à 255.

Un type signé déborde de la même façon, avec une conséquence encore plus déroutante : son bit de gauche compte négativement. Sur un int de l'Uno, ce bit vaut −32 768. Le plus grand int, 32 767, s'écrit 0111111111111111 ; lui ajouter 1 donne 1000000000000000, c'est-à-dire −32 768. Le compteur ne s'arrête pas au plafond : il saute au plancher.

int : 2 octets, de −32 768 à 32 767−32 76816 3848 1924 0962 0481 02451225612864321684210000000000000000int x = 0

Les cases pleines sont les bits à 1. La valeur est la somme de leurs poids, et le bit de gauche, encadré en rouge, compte négativement.

Un int de l'Uno, sur deux octets. Le bit de gauche, encadré en rouge, vaut −32 768. Ajouter 10 000 six fois de suite, comme le fait le calcul 60 × 1000, et lire ce que la carte obtient.
À manipuler
Partir de 0 et ajouter 10 000 six fois. Le quatrième ajout fait déjà basculer le bit de gauche : 30 000 + 10 000 donne −25 536. Au sixième, la figure affiche −5 536, la valeur exacte que la carte calcule pour 60 × 1000. Cliquer ensuite « x = max » puis « x + 1 » : le plus grand int devient le plus petit.

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.
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.Un byte vaut 200. Que vaut-il après avoir reçu 100 de plus ?

4.Que vaut 17 % 5 ?

5.Dans quel type ranger la valeur rendue par millis() ?

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 ==.
Le point-virgule de trop, l'accolade qui manque
if (temperature > 30); compile : le point-virgule termine le if sur une instruction vide, et le bloc qui suit s'exécute toujours, quelle que soit la température. De même, for (int i = 0; i < 5; i++); fait cinq tours à vide, puis exécute le corps une seule fois.
Sans accolades, un if ne commande que la ligne qui le suit. Écrire if (temperature > 30) puis deux lignes indentées, digitalWrite(8, HIGH); et alarme = true;, ne soumet que la première à la condition : la seconde s'exécute à chaque passage, et l'indentation n'y change rien. Mettre des accolades à chaque if et à chaque boucle évite les deux pièges.

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;
}
}
Vérification rapideon peut se reprendre

Que fait if (mode = 2) ?

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, et chacune s'exécute à un moment précis. L'organigramme les remet dans l'ordre où la carte les exécute réellement.

Organigramme
VraiFauxDébuti prend la valeur 0, une seule foisi est inférieur à 5allumer puis éteindre la LEDi augmente de 1, à la fin du tourFin

Ce que fait for (int i = 0; i < 5; i++). L'initialisation n'a lieu qu'une fois ; la condition est testée AVANT chaque tour, y compris le premier ; l'incrément a lieu APRÈS chaque tour. Quand i atteint 5, la condition est fausse et la boucle s'arrête sans exécuter le corps.

La boucle for n'est donc qu'une écriture compacte de cette boucle while, qui dit exactement la même chose :

Arduino
void setup() {
pinMode(13, OUTPUT);
int i = 0; // initialisation
while (i < 5) { // condition
digitalWrite(13, HIGH);
delay(300);
digitalWrite(13, LOW);
delay(300);
i++; // incrément
}
}
void loop() {}
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.

L'incrément n'avance pas forcément d'une unité : i += 2 est l'abréviation de i = i + 2, et fait avancer le compteur de deux en deux.

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);
}

Le bouton est câblé entre la broche 2 et le 5 V, avec une résistance de 10 kΩ entre la broche 2 et GND : sans cette résistance, l'entrée relâchée flotte et digitalRead rend n'importe quoi. Le chapitre sur les boutons et les capteurs explique pourquoi, et comment s'en passer.

Vérification rapideon peut se reprendre

1.Combien de tours fait for (int i = 0; i <= 5; i++) ?

2.Dans for (int i = 0; i < 5; i++), quand l'incrément i++ est-il exécuté ?

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);
}
}

Le bouton est câblé comme dans la section sur les boucles, vers le 5 V avec sa résistance de 10 kΩ vers GND. Le delay(300) n'est qu'un expédient : le programme compte un état, pas un appui, si bien qu'un bouton maintenu plus de 300 ms compte plusieurs fois. La détection du front, au chapitre sur les boutons et les capteurs, règle la question.

Vérification rapideon peut se reprendre

Un compteur déclaré au début de loop() reste bloqué à 1. Pourquoi ?

Lire un message d'erreur

Le compilateur refuse un programme mal écrit, et il le dit en anglais, avec un numéro de ligne. Cinq messages couvrent l'essentiel des premières semaines ; les reconnaître fait gagner des heures.

MessageCe qu'il veut direOù chercher
expected ';' before '}' tokenun point-virgule manqueà la fin de la ligne précédente
'ledPin' was not declared in this scopece nom n'existe pas icifaute de frappe, majuscule, ou variable locale d'une autre fonction
expected '}' at end of inputune accolade ouvrante n'a jamais été ferméecompter les { et les } du fichier
expected unqualified-id before 'if'une instruction est hors de toute fonctionun if ou un digitalWrite écrit hors de setup() et loop()
redefinition of 'void loop()'deux fonctions portent le même nomun second loop() collé dans le fichier
Le numéro de ligne désigne l'endroit où le compilateur s'est aperçu du problème
Un point-virgule oublié à la ligne 12 n'est découvert qu'en lisant la ligne 13 : c'est là que le compilateur trouve un mot qu'il n'attendait pas. Devant expected ';', il faut donc regarder la fin de la ligne au-dessus. Et devant une avalanche de messages, seul le premier compte : les suivants sont souvent des conséquences du premier.

Mettre en pratique

Ce que calcule réellement la carte

  • 1.

    Que vaut 23 / 4 entre deux int ?

  • 2.

    Que vaut 23 % 4 ?

  • 3.

    Un byte vaut 250. Quelle valeur contient-il après avoir reçu 10 de plus ?

  • 4.

    Quelle valeur la carte obtient-elle pour 40 × 1000 calculé entre deux int ?

  • 5.

    Combien de tours fait for (int i = 3; i < 10; i += 2) ?

  • 6.

    millis() est rangé par erreur dans un unsigned int. Au bout de combien de secondes la variable repart-elle à zéro ?

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 : 60 000−65 536=−5 53660\,000 - 65\,536 = -5\,536. 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).

Un chenillard doit allumer les LEDs 0, 1, 2, 3, 0, 1, 2, 3... Écrire la ligne qui fait avancer l'indice.
Arduino
int led = 0;
void setup() {}
void loop() {
led = (led + 1) % 4; // 1, 2, 3, 0, 1, 2, 3, 0...
}

Le reste de la division par 4 ne peut valoir que 0, 1, 2 ou 3 : l'indice reste dans la plage quel que soit le nombre de passages. L'écriture avec un if (led == 4) led = 0; est équivalente, mais demande de ne pas se tromper de borne.

Le compilateur signale expected ';' before 'digitalWrite' à la ligne 8. La ligne 8 est pourtant correcte. Où est l'erreur ?

À la fin de la ligne 7. Le compilateur ne s'aperçoit de l'absence de point-virgule qu'en rencontrant l'instruction suivante, digitalWrite, qu'il n'attendait pas à cet endroit. Le message nomme donc le mot qui le surprend, pas celui qui manque.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices