PYTHON → ROBOTIQUE · 22

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);
}
  1. IDE Arduino → Fichier → Exemples → 01.Basics → Blink.
  2. Outils → Carte : « Arduino Uno ». Outils → Port : celui qui apparaît quand vous branchez.
  3. Bouton ✓ (Vérifier) : compile. Bouton → (Téléverser) : envoie dans la carte.
  4. 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

PythonC / C++ (Arduino)Remarque
x = 5int 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 + bint f(int a, int b) { return a + b; }Type de retour et des paramètres ; void si rien
True, and, or, nottrue, &&, ||, !
7 // 2, 7 / 27 / 2 → 3, 7.0 / 2 → 3.5Division 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

TypeBitsPlageUsage
bool8true / falsedrapeaux
byte, uint8_t80 … 255octets, PWM, petits compteurs
char8−128 … 127 ou un caractèretexte ASCII
int16−32 768 … 32 767par défaut, attention !
unsigned int160 … 65 535
long32±2,1 × 10⁹gros compteurs
unsigned long320 … 4,3 × 10⁹millis()
float32±3,4 × 10³⁸, 7 chiffreslent 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

MotDéfinition
setup() / loop()Une fois au démarrage / en boucle infinie.
CompilationTraduction du C en instructions machine avant l’envoi.
pinMode, digitalWrite, digitalReadConfigurer, écrire, lire une broche numérique.
millis()Millisecondes depuis le démarrage (unsigned long).
FrontChangement d’état ; se détecte en comparant à l’état précédent.
SerialPort série : begin, print, available, read.
RegistreMé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

Ressources

Référence du langage Arduino · Exemples intégrés

← Séance 21SommaireSéance 23 : Arduino 2 →