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.
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.
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.
| 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 |
double | 4 | identique à float sur l'Uno | aucun 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 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 combinaisons : de 0 à 65 535 sans signe, de −32 768 à 32 767 avec.
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 .
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.
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.
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.
Les cases pleines sont les bits à 1. La valeur est la somme de leurs poids.
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.
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.
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.
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.
| 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 ==.
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.
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.
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.
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 :
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.
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.
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
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.
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.
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.
| Message | Ce qu'il veut dire | Où chercher |
|---|---|---|
expected ';' before '}' token | un point-virgule manque | à la fin de la ligne précédente |
'ledPin' was not declared in this scope | ce nom n'existe pas ici | faute de frappe, majuscule, ou variable locale d'une autre fonction |
expected '}' at end of input | une accolade ouvrante n'a jamais été fermée | compter les { et les } du fichier |
expected unqualified-id before 'if' | une instruction est hors de toute fonction | un if ou un digitalWrite écrit hors de setup() et loop() |
redefinition of 'void loop()' | deux fonctions portent le même nom | un second loop() collé dans le fichier |
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 : . 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.
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
La division entière, le dépassement silencieux, et le compte exact des tours d'une boucle.
- La division qui perd sa virguleNiveau 2
- Le dépassement qui ne dit rienNiveau 3
- Compter les tours plutôt que les devinerNiveau 2