ESP32 · Bases — ESP32-S3, PlatformIO et Arduino-ESP32#

← ALT in&out XP · Programme · Réseau →

Ces pages s'adressent à des étudiant.e.s qui programment déjà en C++ et en Godot : on va droit aux spécificités de l'ESP32. Pour une initiation pas à pas, voir le Jour 2.

Page Contenu
Bases (ici) la carte, PlatformIO, GPIO / ADC / PWM / interruptions, filtrage, encodeur, servo, buzzer, rubans LED, souris USB
Réseau Wi-Fi, WiFiManager, mDNS, OSC avec Godot, ESP-NOW
Web serveur HTTP, API JSON, site LittleFS, requêtes HTTP sortantes, WebSocket (navigateur et Godot)
Multitâche FreeRTOS : tâches, files, mutex, notifications
Protocoles UART, I²C (accéléromètre MPU6050), SPI (lecteur RFID)
OTA & NVS mise à jour par Wi-Fi, réglages persistants
Matrices LED dalles HUB75, image envoyée par Godot, grille de dalles

Tous les exemples ont été compilés pour l'ESP32-S3 avec la plateforme PlatformIO espressif32 (Arduino-ESP32 2.0.17).


La carte : ESP32-S3 Super Mini#

CPU Xtensa LX7 double cœur, 240 MHz
Mémoire 512 Ko de SRAM, 2 Mo de PSRAM, 4 Mo de flash
Radio Wi-Fi 4 (2,4 GHz), Bluetooth 5 LE
USB natif (OTG) : port série, JTAG, ou périphérique HID / MIDI…
Périphériques 3 UART (+ le port série USB), 2 I²C, 2 SPI, PWM (LEDC), 2 ADC 12 bits, touch capacitif, RMT, I²S, LCD/caméra
E/S 3,3 V uniquement, toutes les fonctions peuvent être routées sur (presque) n'importe quelle broche (matrice GPIO)

Brochage de l'ESP32-S3 Super Mini

À savoir GPIO
LED RGB intégrée (WS2812) 48
Bouton BOOT (strapping) 0
Strapping, à éviter au démarrage 0, 3, 45, 46
USB natif (D− / D+) 19, 20
UART0 (TX / RX) 43, 44
ADC1 (utilisable avec le Wi-Fi) 1 à 10
ADC2 (inutilisable quand le Wi-Fi est actif) 11 à 20
Touch 1 à 14

PlatformIO#

Un projet = un dossier avec platformio.ini, src/main.cpp, lib/ (librairies locales), include/.

Le platformio.ini peut définir plusieurs environnements ([env:nom]), par exemple un par mode USB. Pour ne pas tout répéter, on regroupe les réglages communs dans une section de base, et chaque environnement en hérite avec extends :

; ---------- commun à tous les environnements ----------
[env]
platform = espressif32                 ; Arduino-ESP32 2.0.17 (ESP-IDF 4.4)
board = esp32-s3-devkitc-1
framework = arduino
board_upload.flash_size = 4MB
board_build.partitions = default.csv   ; 2 × 1,25 Mo d'application (OTA possible) + NVS + SPIFFS
board_build.arduino.memory_type = qio_qspi
monitor_speed = 115200
monitor_filters = esp32_exception_decoder   ; décode les plantages (backtrace → fichier:ligne)
build_flags =
  -DBOARD_HAS_PSRAM
  -DARDUINO_USB_CDC_ON_BOOT=1          ; Serial = port série USB
  -DCORE_DEBUG_LEVEL=3                 ; logs log_e/log_w/log_i (0 = aucun, 5 = tout)
lib_deps =
  hideakitai/ArduinoOSC
  links2004/WebSockets
  adafruit/Adafruit NeoPixel

; ---------- USB-Serial-JTAG : upload et reset automatiques, débogage JTAG ----------
[env:usb-jtag]
build_flags = ${env.build_flags} -DARDUINO_USB_MODE=1
debug_tool = esp-builtin

; ---------- USB OTG (TinyUSB) : clavier, manette, MIDI… ----------
[env:usb-otg]
build_flags = ${env.build_flags} -DARDUINO_USB_MODE=0
board_upload.use_1200bps_touch = yes      ; passe la carte en mode programmation via le port série
board_upload.wait_for_upload_port = yes
board_upload.require_upload_port = yes
board_upload.after_reset = watchdog_reset ; redémarre la carte après l'upload

; ---------- mise à jour par le Wi-Fi (voir OTA & NVS) ----------
[env:ota]
extends = env:usb-jtag
upload_protocol = espota
upload_port = enjmin-01.local
upload_flags = --auth=enjmin
; ---------- exemple : un projet qui a besoin d'une librairie en plus ----------
[env:hub75]
extends = env:usb-jtag
lib_deps =
  ${env.lib_deps}
  https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA.git
  adafruit/Adafruit GFX Library

Dans VS Code, on choisit l'environnement actif dans la barre d'état (icône env:…) ; en ligne de commande, avec -e.

Commande (terminal PlatformIO) Effet
pio run -t upload compile et envoie (environnement par défaut)
pio run -e usb-otg -t upload idem avec un autre environnement
pio device monitor moniteur série (Ctrl + C pour quitter)
pio run -t clean nettoie la compilation
pio run -t erase efface toute la flash (NVS comprise)
pio debug / onglet Run and Debug débogage pas à pas par l'USB (env usb-jtag)

Si l'upload échoue (programme qui plante au démarrage, premier upload en mode OTG…) : maintenez BOOT, appuyez sur RST, relâchez BOOT, puis relancez l'upload.

Arduino-ESP32 2.x ou 3.x ?#

La plateforme officielle espressif32 de PlatformIO est restée sur Arduino-ESP32 2.0.17. Pour la version 3.x (ESP-IDF 5), on utilise le fork communautaire pioarduino :

platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip

Les exemples de ces pages sont écrits pour la 2.0.17. Principales différences avec la 3.x :

2.0.x 3.x
PWM ledcSetup(channel, freq, bits) + ledcAttachPin(pin, channel) + ledcWrite(channel, v) ledcAttach(pin, freq, bits) + ledcWrite(pin, v)
LED RGB intégrée neopixelWrite(48, r, g, b) rgbLedWrite(48, r, g, b)
Réception ESP-NOW cb(const uint8_t* mac, const uint8_t* data, int len) cb(const esp_now_recv_info_t* info, const uint8_t* data, int len)
Timers matériels timerBegin(0, 80, true) timerBegin(frequency)

Infos système, logs et PSRAM#

#include <Arduino.h>

void setup() {
  Serial.begin(115200);
  delay(2000);  // laisse le temps au moniteur série USB de se connecter

  Serial.printf("Puce       : %s rév. %d, %d cœurs à %d MHz\n",
                ESP.getChipModel(), ESP.getChipRevision(), ESP.getChipCores(), ESP.getCpuFreqMHz());
  Serial.printf("Flash      : %d Mo\n", ESP.getFlashChipSize() / (1024 * 1024));
  Serial.printf("RAM libre  : %d Ko (sur %d)\n", ESP.getFreeHeap() / 1024, ESP.getHeapSize() / 1024);
  Serial.printf("PSRAM libre: %d Ko (sur %d)\n", ESP.getFreePsram() / 1024, ESP.getPsramSize() / 1024);
  Serial.printf("Programme  : %d Ko (place libre : %d Ko)\n", ESP.getSketchSize() / 1024, ESP.getFreeSketchSpace() / 1024);
  Serial.printf("MAC        : %012llX\n", ESP.getEfuseMac());
  Serial.printf("SDK        : %s, Arduino-ESP32 %d.%d.%d\n", ESP.getSdkVersion(),
                ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH);

  // Logs de l'ESP-IDF : niveau réglé par -DCORE_DEBUG_LEVEL (0 à 5) dans platformio.ini
  log_i("message d'info (affiché si CORE_DEBUG_LEVEL >= 3)");
  log_e("message d'erreur (affiché si CORE_DEBUG_LEVEL >= 1)");

  // Un gros tableau en PSRAM plutôt qu'en RAM interne
  uint8_t* buffer = (uint8_t*) ps_malloc(1024 * 1024);
  Serial.printf("1 Mo alloué en PSRAM : %s\n", buffer ? "oui" : "non");
  free(buffer);
}

void loop() {}

GPIO, ADC, PWM, touch, interruptions#

Montage : potentiomètre sur GPIO 4, LED + 220 Ω sur GPIO 5, bouton sur GPIO 6, fil tactile sur GPIO 7.

Montage GPIO

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>

#define LED_PIN    5    // LED externe (avec résistance)
#define BUTTON_PIN 6    // bouton vers GND
#define POT_PIN    4    // potentiomètre (ADC1 : GPIO 1 à 10)
#define TOUCH_PIN  7    // fil / surface conductrice (touch : GPIO 1 à 14)

// --- Interruption : le compteur est modifié depuis l'ISR → volatile ---
volatile uint32_t pressCount = 0;
volatile uint32_t lastPress = 0;

void IRAM_ATTR onPress() {            // IRAM_ATTR : la fonction reste en RAM (obligatoire pour une ISR)
  uint32_t t = millis();
  if (t - lastPress > 200) {       // anti-rebond simple
    pressCount++;
    lastPress = t;
  }
}

void setup() {
  Serial.begin(115200);

  // --- PWM matériel (LEDC) : 5 kHz, 12 bits ---
  ledcSetup(0, 5000, 12);              // canal 0
  ledcAttachPin(LED_PIN, 0);

  // --- ADC : résolution et plage de tension ---
  analogReadResolution(12);            // 0 à 4095
  analogSetPinAttenuation(POT_PIN, ADC_11db);   // mesure jusqu'à ~3,1 V

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
}

void loop() {
  int raw = analogRead(POT_PIN);
  int mv = analogReadMilliVolts(POT_PIN);       // valeur calibrée en millivolts
  ledcWrite(0, raw);                            // la LED suit le potentiomètre

  Serial.printf("pot=%4d (%4d mV)  touch=%6d  appuis=%u\n",
                raw, mv, touchRead(TOUCH_PIN), pressCount);
  delay(100);
}

La LED RGB intégrée, sans librairie#

#include <Arduino.h>

// La LED RGB intégrée (WS2812 sur GPIO 48) se pilote sans librairie
void setup() {}

void loop() {
  for (int h = 0; h < 360; h += 2) {
    // teinte → RVB, version courte
    float r = max(0.0f, cosf((h      ) * DEG_TO_RAD));
    float g = max(0.0f, cosf((h - 120) * DEG_TO_RAD));
    float b = max(0.0f, cosf((h - 240) * DEG_TO_RAD));
    neopixelWrite(48, r * 40, g * 40, b * 40);   // (broche, rouge, vert, bleu) 0 à 255
    delay(20);
  }
}

Pour des rubans de LED, utilisez Adafruit NeoPixel ou FastLED (le pilote passe par le périphérique RMT, sans charge pour le CPU).


Lire un capteur proprement : filtrage#

Une mesure analogique brute tremble toujours un peu. Trois outils, à combiner :

Potentiomètre sur GPIO 4

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>

#define SENSOR_PIN 4

// --- 1. Moyenne glissante sur N mesures ---
const int N = 16;
int samples[N];
int sampleIndex = 0;
long sampleSum = 0;

int movingAverage(int value) {
  sampleSum += value - samples[sampleIndex];   // on retire la plus ancienne, on ajoute la nouvelle
  samples[sampleIndex] = value;
  sampleIndex = (sampleIndex + 1) % N;
  return sampleSum / N;
}

// --- 2. Filtre exponentiel (passe-bas) : une seule variable ---
float smoothed = 0;
const float ALPHA = 0.1;                       // 0..1 : petit = très lissé mais plus lent

float exponentialFilter(int value) {
  smoothed += ALPHA * (value - smoothed);
  return smoothed;
}

// --- 3. Seuil avec hystérésis : évite de clignoter autour du seuil ---
const int THRESHOLD_ON = 2200;
const int THRESHOLD_OFF = 1800;
bool active = false;

bool hysteresis(float value) {
  if (!active && value > THRESHOLD_ON) active = true;
  if (active && value < THRESHOLD_OFF) active = false;
  return active;
}

void setup() {
  Serial.begin(115200);
  int first = analogRead(SENSOR_PIN);
  for (int i = 0; i < N; i++) samples[i] = first;   // on pré-remplit pour éviter un départ à 0
  sampleSum = (long) first * N;
  smoothed = first;
}

void loop() {
  int raw = analogRead(SENSOR_PIN);
  int avg = movingAverage(raw);
  float expo = exponentialFilter(raw);
  bool on = hysteresis(expo);

  // Format « nom:valeur,nom:valeur » : affichable en courbes par un traceur série (Arduino IDE, Serial Studio…)
  Serial.printf("raw:%d,avg:%d,expo:%.0f,on:%d\n", raw, avg, expo, on ? 4095 : 0);
  delay(10);
}

Encodeur rotatif#

Un bouton qui tourne sans fin, avec des crans : idéal pour un menu, un volume, une roue de contrôle. Le périphérique PCNT de l'ESP32 compte les impulsions en matériel, sans jamais en perdre, même en tournant vite. Librairie : lib_deps = madhephaestus/ESP32Encoder.

Encodeur rotatif

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>
#include <ESP32Encoder.h>

// Encodeur rotatif (type KY-040) : A et B vers deux GPIO, C (commun) vers GND, bouton SW vers un 3e GPIO
#define ENC_A   5
#define ENC_B   6
#define ENC_SW  7

ESP32Encoder encoder;   // compte les crans avec le périphérique matériel PCNT : aucun pas perdu, zéro CPU

void setup() {
  Serial.begin(115200);
  pinMode(ENC_SW, INPUT_PULLUP);

  ESP32Encoder::useInternalWeakPullResistors = puType::up;   // pull-up internes sur A et B
  encoder.attachHalfQuad(ENC_A, ENC_B);   // 2 impulsions par cran (attachFullQuad : 4, attachSingleEdge : 1)
  encoder.setFilter(1023);                // filtre matériel anti-rebond
  encoder.clearCount();
}

int64_t lastCount = 0;

void loop() {
  int64_t count = encoder.getCount() / 2;  // en crans
  if (count != lastCount) {
    Serial.printf("position : %lld (%+lld)\n", count, count - lastCount);
    lastCount = count;
  }
  if (digitalRead(ENC_SW) == LOW) {        // appui sur l'axe : remise à zéro
    encoder.clearCount();
    delay(200);
  }
  delay(5);
}

Servomoteur#

Pas besoin de librairie : un servo se pilote avec le PWM matériel (LEDC) à 50 Hz.

Servomoteur

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>

// Servomoteur (SG90, MG996R…) : fil signal sur un GPIO, alimentation 5 V séparée, GND commun
#define SERVO_PIN 5
#define SERVO_CHANNEL 0

// Un servo attend une impulsion toutes les 20 ms (50 Hz) :
// ~0,5 ms = 0°, ~1,5 ms = 90°, ~2,5 ms = 180° (à ajuster selon le modèle)
const int PULSE_MIN_US = 500;
const int PULSE_MAX_US = 2500;
const int RESOLUTION = 14;                       // 14 bits : 16384 pas sur 20 ms

void servoWrite(float angle) {
  angle = constrain(angle, 0, 180);
  float pulseUs = PULSE_MIN_US + (PULSE_MAX_US - PULSE_MIN_US) * angle / 180.0;
  uint32_t duty = pulseUs / 20000.0 * ((1 << RESOLUTION) - 1);
  ledcWrite(SERVO_CHANNEL, duty);
}

void setup() {
  ledcSetup(SERVO_CHANNEL, 50, RESOLUTION);      // 50 Hz
  ledcAttachPin(SERVO_PIN, SERVO_CHANNEL);
}

void loop() {
  // L'angle suit un potentiomètre sur le GPIO 4
  servoWrite(map(analogRead(4), 0, 4095, 0, 180));
  delay(20);
}

Buzzer#

Buzzer passif

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>

// Buzzer passif (ou petit haut-parleur avec une résistance) sur un GPIO
#define BUZZER_PIN 5
#define BUZZER_CHANNEL 1

// Une mélodie : fréquences en Hz (0 = silence) et durées en ms
const int melody[][2] = {
  {523, 150}, {659, 150}, {784, 150}, {1047, 300}, {0, 100}, {784, 150}, {1047, 450},
};

void playTone(int freq, int durationMs) {
  ledcWriteTone(BUZZER_CHANNEL, freq);           // fréquence du signal carré (0 = arrêt)
  delay(durationMs);
}

void setup() {
  ledcSetup(BUZZER_CHANNEL, 2000, 10);
  ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);

  for (auto& note : melody) playTone(note[0], note[1]);
  ledcWriteTone(BUZZER_CHANNEL, 0);
}

void loop() {
  // Sirène : la fréquence monte et descend
  for (int f = 400; f < 1200; f += 10) { ledcWriteTone(BUZZER_CHANNEL, f); delay(3); }
  for (int f = 1200; f > 400; f -= 10) { ledcWriteTone(BUZZER_CHANNEL, f); delay(3); }
}

Rubans LED (WS2812B / NeoPixel)#

Ruban LED

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>
#include <Adafruit_NeoPixel.h>

// Ruban WS2812B : DIN sur un GPIO (résistance 330 Ω en série conseillée), 5 V et GND séparés
#define STRIP_PIN 5
#define NUM_LEDS  60

Adafruit_NeoPixel strip(NUM_LEDS, STRIP_PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  strip.begin();
  strip.setBrightness(60);   // limite le courant : ~60 mA par LED en blanc à pleine puissance !
  strip.show();
}

uint16_t hueOffset = 0;

void loop() {
  int level = map(analogRead(4), 0, 4095, 0, NUM_LEDS);   // barre de niveau pilotée par le GPIO 4

  for (int i = 0; i < NUM_LEDS; i++) {
    if (i < level) {
      // arc-en-ciel qui défile : teinte sur 16 bits (0 à 65535)
      uint16_t hue = hueOffset + i * 65536L / NUM_LEDS;
      strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(hue)));
    } else {
      strip.setPixelColor(i, 0);
    }
  }
  strip.show();              // envoie tout le ruban d'un coup (pilote RMT : sans bloquer les interruptions)
  hueOffset += 256;
  delay(16);
}

Souris USB#

Avec l'environnement usb-otg, l'ESP32 devient une souris : un capteur pilote le curseur dans n'importe quel logiciel, Godot compris. (Clavier et manette : voir Jour 3.)

Joystick analogique

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>
#include "USB.h"
#include "USBHIDMouse.h"

// Environnement usb-otg (ARDUINO_USB_MODE=0)
// Joystick analogique (2 potentiomètres) : X sur GPIO 4, Y sur GPIO 5, bouton sur GPIO 6
#define JOY_X  4
#define JOY_Y  5
#define BUTTON 6

USBHIDMouse Mouse;
int centerX, centerY;

int axis(int pin, int center) {
  int v = analogRead(pin) - center;
  if (abs(v) < 150) return 0;               // zone morte au centre
  return constrain(v / 100, -20, 20);       // déplacement relatif, en pixels par envoi
}

void setup() {
  pinMode(BUTTON, INPUT_PULLUP);
  centerX = analogRead(JOY_X);              // position de repos = centre
  centerY = analogRead(JOY_Y);
  Mouse.begin();
  USB.begin();
}

bool wasPressed = false;

void loop() {
  int dx = axis(JOY_X, centerX);
  int dy = axis(JOY_Y, centerY);
  if (dx || dy) Mouse.move(dx, dy);         // move(x, y, molette, molette horizontale)

  bool pressed = digitalRead(BUTTON) == LOW;
  if (pressed && !wasPressed) Mouse.press(MOUSE_LEFT);
  if (!pressed && wasPressed) Mouse.release(MOUSE_LEFT);
  wasPressed = pressed;

  delay(10);                                // ~100 rapports par seconde
}

Et pour Godot ?#

Trois façons de relier l'ESP32 à un jeu :

Lien Latence Avantages Page
USB HID (clavier / manette) ~1 ms rien à installer côté jeu, marche avec tout logiciel Jour 3
OSC / UDP par le Wi-Fi quelques ms sans fil, bidirectionnel, plusieurs ESP32 Réseau
WebSocket quelques ms bidirectionnel, aussi depuis un navigateur Web
Éditer