Séance 22 · Partie D · Électronique et embarqué
Arduino 1 :
premiers programmes en C/C++.
Un Arduino Uno : 16 MHz, 2 Ko de RAM, 32 Ko de programme, pas de système d’exploitation. Mille fois moins qu’un PC, et pourtant il fait tourner un robot. Parce qu’il ne fait que ça, sans rien entre le code et les broches.
Durée : 2 séances · Objectifs : l’IDE et le téléversement, setup/loop, les types et la syntaxe du C, entrées/sorties numériques, delay vs millis, le port série côté carte, boutons et anti-rebond.
Ce que vous saurez faire à la fin
- Écrire, compiler et téléverser un programme Arduino.
- Traduire votre Python en C : types, boucles, fonctions, tableaux.
- Faire plusieurs choses « en même temps » sans
delay. - Dialoguer avec le PC via le moniteur série et votre
liaison.py.
Matériel : Arduino Uno + câble USB, breadboard, 2 LED, 2 résistances 220 Ω, 1 bouton. IDE Arduino 2.x installé.
01 / Démarrer
Blink : le « Bonjour » du matériel
// Tout ce qui suit // est un commentaire
void setup() { // exécuté UNE fois au démarrage
pinMode(13, OUTPUT); // la broche 13 est une sortie
}
void loop() { // exécuté en boucle, pour toujours
digitalWrite(13, HIGH); // 5 V sur la broche : LED allumée
delay(500); // attendre 500 ms
digitalWrite(13, LOW); // 0 V : LED éteinte
delay(500);
}- IDE Arduino → Fichier → Exemples → 01.Basics → Blink.
- Outils → Carte : « Arduino Uno ». Outils → Port : celui qui apparaît quand vous branchez.
- Bouton ✓ (Vérifier) : compile. Bouton → (Téléverser) : envoie dans la carte.
- La LED intégrée (broche 13) clignote. Changez 500 en 100. Retéléversez.
Le programme reste dans la carte : débranchez, rebranchez, il repart. Pas de PC nécessaire.
Ce qui se passe au téléversement
Le code C++ est compilé en instructions machine AVR (séance 01 : ici pas d’interpréteur, pas de bytecode, le CPU exécute directement votre programme). Le fichier binaire (quelques Ko) est envoyé par USB au chargeur d’amorçage (bootloader) de la carte qui l’écrit en mémoire flash. Au reset, le CPU commence à l’adresse 0 : votre setup(), puis loop() indéfiniment. Il n’y a rien d’autre qui tourne. C’est pourquoi la gigue (séance 20) est de quelques microsecondes.
02 / Le C
Python → C : ce qui change
| Python | C / C++ (Arduino) | Remarque |
|---|---|---|
x = 5 | int x = 5; | Type obligatoire, point-virgule obligatoire |
if a < b:... | if (a < b) {...} | Parenthèses et accolades ; l’indentation n’est que pour l’humain |
for i in range(10): | for (int i = 0; i < 10; i++) { | init ; condition ; incrément |
while cond: | while (cond) { | identique |
def f(a, b): return a + b | int f(int a, int b) { return a + b; } | Type de retour et des paramètres ; void si rien |
True, and, or, not | true, &&, ||, ! | |
7 // 2, 7 / 2 | 7 / 2 → 3, 7.0 / 2 → 3.5 | Division entière si les deux sont entiers ! |
[1, 2, 3] | int t[3] = {1, 2, 3}; | Taille fixe, pas de append, pas de vérification d’indice |
print(x) | Serial.println(x); | Après Serial.begin(115200) |
# commentaire | // commentaire ou /* … */ |
La philosophie
Python vérifie tout à l’exécution et vous protège. Le C fait exactement ce que vous écrivez, sans filet : écrire t[5] dans un tableau de 3 éléments écrase une autre variable sans message. En échange, le programme compilé est minuscule et rapide, et tient dans 2 Ko de RAM. Les réflexes de la séance 08 (tester, borner, valider) deviennent vitaux.
02 / Le C
Les types : choisir la taille de ses nombres
| Type | Bits | Plage | Usage |
|---|---|---|---|
bool | 8 | true / false | drapeaux |
byte, uint8_t | 8 | 0 … 255 | octets, PWM, petits compteurs |
char | 8 | −128 … 127 ou un caractère | texte ASCII |
int | 16 | −32 768 … 32 767 | par défaut, attention ! |
unsigned int | 16 | 0 … 65 535 | |
long | 32 | ±2,1 × 10⁹ | gros compteurs |
unsigned long | 32 | 0 … 4,3 × 10⁹ | millis() |
float | 32 | ±3,4 × 10³⁸, 7 chiffres | lent sur AVR, éviter en boucle |
Règles pratiques
Temps en unsigned long, toujours. Broches et petits entiers en byte ou int. Calculs intermédiaires qui dépassent 32 767 : caster en long ((long)a * b). Éviter float dans une boucle rapide sur Uno (pas d’unité de calcul flottant : 100 fois plus lent). Sur ESP32 (32 bits, FPU), int fait 32 bits et le float est rapide : le C n’a pas de tailles fixes, d’où les types explicites int16_t, uint32_t qu’on préfère en code sérieux.
02 / Le C
Fonctions, constantes, tableaux
const byte LED_ROUGE = 9; // const : ne change jamais, le compilateur optimise
const byte LED_VERTE = 10;
const byte LEDS[] = {9, 10, 11}; // un tableau de broches
const byte NB_LEDS = sizeof(LEDS) / sizeof(LEDS[0]); // idiome : taille d'un tableau
// Une fonction : type de retour, nom, paramètres typés
void clignoter(byte broche, int duree_ms) {
digitalWrite(broche, HIGH);
delay(duree_ms);
digitalWrite(broche, LOW);
delay(duree_ms);
}
int carre(int x) {
return x * x; // attention : 200*200 déborde un int
}
void setup() {
for (byte i = 0; i < NB_LEDS; i++) {
pinMode(LEDS[i], OUTPUT);
}
}
void loop() {
for (byte i = 0; i < NB_LEDS; i++) { // chenillard
clignoter(LEDS[i], 150);
}
}Différences avec Python
Une fonction doit être déclarée avant d’être utilisée (l’IDE Arduino génère les déclarations pour vous, mais pas un compilateur C standard). Pas de valeur par défaut classique (C++ en a, l’IDE les accepte). Un tableau ne connaît pas sa taille : on la calcule avec sizeof ou on la garde dans une constante. #define LED 13 est l’ancienne façon de faire des constantes (remplacement textuel) ; const est typé et préférable.
03 / Entrées-sorties
Lire un bouton : INPUT_PULLUP et logique inversée
const byte BOUTON = 2; // bouton entre D2 et GND
const byte LED = 13;
void setup() {
pinMode(BOUTON, INPUT_PULLUP); // rappel interne : relâché = HIGH
pinMode(LED, OUTPUT);
Serial.begin(115200);
}
void loop() {
bool presse = (digitalRead(BOUTON) == LOW); // inversé !
digitalWrite(LED, presse ? HIGH : LOW);
Serial.println(presse);
delay(50);
}Montage : une patte du bouton sur D2, l’autre sur GND. Rien d’autre : la résistance de rappel est dans la puce (séance 21).
digitalRead renvoie HIGH (1) ou LOW (0). Avec le pull-up, pressé = LOW.
cond ? a : b est l’expression conditionnelle du C, comme a if cond else b.
Ouvrez le moniteur série (loupe en haut à droite, 115200 bauds) : vous voyez 0 et 1 défiler.
03 / Entrées-sorties
Compter les appuis : fronts et anti-rebond
const byte BOUTON = 2;
int compteur = 0;
bool etat_precedent = HIGH;
unsigned long dernier_changement = 0;
const unsigned long DELAI_REBOND = 30; // ms
void setup() {
pinMode(BOUTON, INPUT_PULLUP);
Serial.begin(115200);
}
void loop() {
bool etat = digitalRead(BOUTON);
if (etat != etat_precedent && millis() - dernier_changement > DELAI_REBOND) {
dernier_changement = millis();
if (etat == LOW) { // front descendant = appui
compteur++;
Serial.print("Appuis : ");
Serial.println(compteur);
}
etat_precedent = etat;
}
}Trois idées
Front : on ne réagit pas à « le bouton est pressé » (vrai pendant 200 ms = des milliers de tours de boucle) mais à « il vient de passer de relâché à pressé » : comparer avec l’état précédent. Anti-rebond : ignorer tout changement survenant moins de 30 ms après le précédent (séance 21). millis() : millisecondes écoulées depuis le démarrage, en unsigned long ; la soustraction millis() − dernier reste correcte même après le débordement à 49,7 jours grâce à l’arithmétique modulaire (séance 13). Ce motif « état précédent + horodatage » est à retenir : il sert pour tout capteur tout-ou-rien.
03 / Entrées-sorties
Le problème de delay() : tout s’arrête
// Faire clignoter une LED à 1 Hz ET réagir
// immédiatement au bouton : impossible ainsi
void loop() {
digitalWrite(LED, HIGH);
delay(500); // pendant 500 ms, le bouton
digitalWrite(LED, LOW); // est ignoré
delay(500);
if (digitalRead(BOUTON) == LOW) {
// ne sera vu qu'une fois par seconde,
// et seulement si pressé au bon moment
}
}// Solution : ne jamais attendre, comparer l'heure
unsigned long prochain_bascule = 0;
bool led_allumee = false;
void loop() {
unsigned long maintenant = millis();
if (maintenant >= prochain_bascule) { // tâche 1 : clignoter
led_allumee = !led_allumee;
digitalWrite(LED, led_allumee);
prochain_bascule += 500; // instant absolu (séance 20 !)
}
if (digitalRead(BOUTON) == LOW) { // tâche 2 : réagir tout de suite
Serial.println("bouton");
}
// loop() tourne ~100 000 fois par seconde : rien n'est raté
}La règle d’or de l’embarqué
delay() est acceptable dans un exemple, jamais dans un robot. La boucle doit tourner vite et chaque « tâche » vérifie si c’est son heure. C’est exactement la boucle à cadence fixe de la séance 20 et le style « demander / vérifier ». Avec ce schéma, on gère dix tâches à des cadences différentes dans un seul loop(), et le programme reste réactif. La bibliothèque standard propose une classe pour ça ; vous saurez l’écrire vous-même en séance 23.
03 / Entrées-sorties
Simuler le motif millis() en Python
Transposer en C
Même structure, sans classe (ou avec, en C++) : un tableau de structures {période, prochain, fonction}. Sur Uno, la boucle tourne des dizaines de milliers de fois par seconde ; ici, Python en fait quelques centaines de milliers aussi. Le compteur montre combien de fois la boucle « passe » entre deux clignotements : c’est tout ce temps disponible pour lire les capteurs.
04 / Série
Le port série côté carte : parler à liaison.py
const byte LED = 13;
String ligne = ""; // accumule les caractères reçus
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(115200);
Serial.println("PRET");
}
void traiter(String cmd) { // "LED 1", "LED 0", "PING"
cmd.trim();
if (cmd.startsWith("LED ")) {
digitalWrite(LED, cmd.substring(4).toInt() ? HIGH : LOW);
Serial.println("OK LED");
} else if (cmd == "PING") {
Serial.println("PONG");
} else {
Serial.print("ERR "); Serial.println(cmd);
}
}
void loop() {
while (Serial.available()) { // des octets sont arrivés ?
char c = Serial.read();
if (c == '\n') { traiter(ligne); ligne = ""; }
else if (ligne.length() < 60) ligne += c; // borner : 2 Ko de RAM !
}
static unsigned long prochain = 0; // static : conserve sa valeur entre les appels
if (millis() >= prochain) {
prochain += 100;
Serial.print("D="); Serial.print(analogRead(A0));
Serial.print(";T="); Serial.println(millis());
}
}Le protocole de la séance 20, côté C
Lecture non bloquante : Serial.available() dit combien d’octets attendent ; on les consomme sans jamais attendre une ligne complète. Envoi de mesures à cadence fixe. Testez avec le moniteur série (tapez LED 1 + Entrée, réglé sur « Nouvelle ligne »), puis avec votre liaison.py : tout doit fonctionner sans le modifier. La classe String d’Arduino est pratique mais fragmente la RAM : en code sérieux, on utilise un tableau de char de taille fixe.
04 / Série
Le chien de garde matériel
#include <avr/wdt.h>
void setup() {
wdt_enable(WDTO_1S); // si wdt_reset() n'est pas appelé pendant 1 s : redémarrage
Serial.begin(115200);
}
void loop() {
wdt_reset(); // « je suis vivant »
// ... le vrai travail ...
// Test : décommentez pour simuler un plantage
// while (true) {} // la carte redémarre après 1 s
}Pourquoi
Un robot qui plante moteurs en marche est dangereux. Le watchdog timer est un compteur matériel indépendant du programme : s’il n’est pas remis à zéro à temps, il redémarre la puce. Un programme qui s’est perdu dans une boucle infinie, une corruption mémoire, un capteur qui bloque : tout cela déclenche un redémarrage propre. Toute machine industrielle en a un. Complément logiciel : au démarrage, mettre tous les moteurs à l’arrêt avant toute autre chose.
05 / Défis
Défi ★ — Feu tricolore et code Morse
Consigne
1. Trois LED (rouge D9, orange D10, verte D11) : cycle de feu tricolore (vert 3 s, orange 1 s, rouge 3 s), en utilisant une fonction allumer(byte broche, int duree) et un tableau.
2. Faire clignoter « SOS » en Morse sur la LED 13 (· · · — — — · · ·) : point 200 ms, trait 600 ms, pause 200 ms entre signaux, 600 ms entre lettres, 1,4 s entre mots. Écrire une fonction morse(const char* message) avec une table pour A–Z.
Correction (Morse)
const char* MORSE[] = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--",
"-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."};
const int POINT = 200;
void signal(int duree) { digitalWrite(13, HIGH); delay(duree); digitalWrite(13, LOW); delay(POINT); }
void morse(const char* message) {
for (int i = 0; message[i] != '\0'; i++) { // une chaîne C se termine par '\0'
char c = toupper(message[i]);
if (c == ' ') { delay(POINT * 7); continue; }
if (c < 'A' || c > 'Z') continue;
const char* code = MORSE[c - 'A'];
for (int j = 0; code[j] != '\0'; j++) signal(code[j] == '.' ? POINT : 3 * POINT);
delay(POINT * 2);
}
}
void setup() { pinMode(13, OUTPUT); }
void loop() { morse("SOS robot"); delay(3000); }c - 'A' : les caractères sont des nombres (séance 07), A = 65 ; la soustraction donne l’indice 0–25. Une chaîne C est un tableau de char terminé par un zéro. delay est acceptable ici : le programme ne fait rien d’autre.
05 / Défis
Défi ★★ — Réflexomètre
Consigne
Un jeu de réflexes : la LED s’allume après un délai aléatoire (1 à 5 s, random(1000, 5000)) ; le joueur presse le bouton le plus vite possible ; la carte affiche le temps de réaction en ms sur le port série, et « TRICHE » si le bouton est pressé avant l’allumage. Garder le meilleur temps. Sans delay dans la phase d’attente : le programme doit détecter la triche pendant l’attente. Indice : une machine à états avec trois états (ATTENTE, ALLUME, RESULTAT).
Correction
const byte BOUTON = 2, LED = 13;
enum Etat { ATTENTE, ALLUME, RESULTAT };
Etat etat = ATTENTE;
unsigned long t_cible, t_allumage;
unsigned long meilleur = 999999;
void setup() {
pinMode(BOUTON, INPUT_PULLUP); pinMode(LED, OUTPUT);
Serial.begin(115200); randomSeed(analogRead(A0)); // une broche en l'air : du bruit
t_cible = millis() + random(1000, 5000);
}
void loop() {
bool presse = digitalRead(BOUTON) == LOW;
switch (etat) {
case ATTENTE:
if (presse) { Serial.println("TRICHE"); etat = RESULTAT; }
else if (millis() >= t_cible) { digitalWrite(LED, HIGH); t_allumage = millis(); etat = ALLUME; }
break;
case ALLUME:
if (presse) {
unsigned long reaction = millis() - t_allumage;
digitalWrite(LED, LOW);
if (reaction < meilleur) meilleur = reaction;
Serial.print("Reaction : "); Serial.print(reaction); Serial.print(" ms (meilleur "); Serial.print(meilleur); Serial.println(")");
etat = RESULTAT;
}
break;
case RESULTAT:
if (!presse) { delay(500); t_cible = millis() + random(1000, 5000); etat = ATTENTE; }
break;
}
}Un enum nomme les états ; switch aiguille. Cette machine à états est la structure de tout programme embarqué un peu complexe : on la reverra en séance 28 pour le comportement d’un robot. randomSeed(analogRead(A0)) : une broche non connectée fournit du bruit, la seule source de hasard d’un microcontrôleur.
05 / Défis
Défi ★★★ — Esprit prépa : registres et vitesse
Consigne
digitalWrite est lent (~4 µs) car il vérifie la broche, la traduit en port et masque. On peut écrire directement dans le registre du port : sur Uno, D8 à D13 sont les bits 0 à 5 de PORTB.
1. Faire basculer D13 (bit 5 de PORTB) le plus vite possible avec digitalWrite, puis avec PORTB ^= (1 << 5);. Mesurer la fréquence obtenue (avec un oscilloscope, ou en comptant les bascules en 1 s avec millis()).
2. Expliquer chaque opérateur bit à bit utilisé (séance 13) : DDRB |= (1 << 5), PORTB &= ~(1 << 5).
3. Combien de cycles d’horloge (16 MHz) prend une bascule dans chaque cas ?
Correction et ouverture
void setup() {
DDRB |= (1 << 5); // DDR = Data Direction Register : bit 5 en sortie (= pinMode(13, OUTPUT))
Serial.begin(115200);
}
void loop() {
unsigned long n = 0, fin = millis() + 1000;
while (millis() < fin) { digitalWrite(13, HIGH); digitalWrite(13, LOW); n++; }
Serial.print("digitalWrite : "); Serial.print(n); Serial.println(" bascules/s");
n = 0; fin = millis() + 1000;
while (millis() < fin) { PORTB |= (1 << 5); PORTB &= ~(1 << 5); n++; }
Serial.print("registre : "); Serial.print(n); Serial.println(" bascules/s");
delay(2000);
}Typiquement ~120 000 bascules/s avec digitalWrite contre ~2 000 000 avec les registres (limitées ici par millis() et le compteur ; un PORTB ^= ... seul prend 2 cycles = 125 ns, soit 8 MHz). DDRB configure la direction, PORTB l’état des sorties, PINB lit les entrées : trois registres de 8 bits, un bit par broche. Tout ce que fait Arduino se ramène à écrire des bits dans des registres, et la fiche technique de l’ATmega328P (660 pages) les décrit tous. Ce niveau est celui des pilotes de périphériques et de l’embarqué critique ; en prépa, l’option informatique le touche via l’architecture des ordinateurs.
06 / Vérification
Pourquoi éviter delay() dans un robot ?
Deux questions supplémentaires
1. Que vaut int x = 40000; sur Uno ? −25 536 : un int fait 16 bits, débordement silencieux. Utiliser long.
2. Avec INPUT_PULLUP, que lit-on quand le bouton est pressé ? LOW : la logique est inversée.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
setup() / loop() | Une fois au démarrage / en boucle infinie. |
| Compilation | Traduction du C en instructions machine avant l’envoi. |
pinMode, digitalWrite, digitalRead | Configurer, écrire, lire une broche numérique. |
millis() | Millisecondes depuis le démarrage (unsigned long). |
| Front | Changement d’état ; se détecte en comparant à l’état précédent. |
Serial | Port série : begin, print, available, read. |
| Registre | Mémoire de 8 bits qui commande directement le matériel. |
Pour continuer
Le code touche le matériel
Séance suivante : lire des capteurs analogiques, produire des signaux PWM, piloter un servomoteur et un capteur ultrason. Arduino 2.
À faire chez soi
- Refaire le chenillard, le compteur d’appuis et le réflexomètre sans regarder.
- Connecter
liaison.py(séance 20) au programme série de cette séance et afficher les mesures avec matplotlib en direct. - Lire la fiche « Digital Pins » de la documentation Arduino et noter la différence entre les broches avec ~ (PWM) et les autres.