Codes en vrac#

Dépôt de bouts de code et de fonctions, ajoutés au fur et à mesure du workshop.


MPU6050 sans library#

// ---------------------------------------------------------------------------
// Test du MPU6050 (accéléromètre + gyroscope) sur un ESP32-S3, en I2C
// Sans bibliothèque externe : uniquement Wire
// ---------------------------------------------------------------------------
#include <Arduino.h>
#include <Wire.h>

// Broches I2C
const uint8_t SDA_PIN = 1;
const uint8_t SCL_PIN = 2;

// Registres du MPU6050
const uint8_t REG_PWR_MGMT_1 = 0x6B;    // gestion de l'alimentation
const uint8_t REG_ACCEL_XOUT_H = 0x3B;  // début des 14 octets de mesures
const uint8_t REG_WHO_AM_I = 0x75;      // identifiant de la puce

// Adresse I2C trouvée au démarrage (0x68, ou 0x69 si la broche AD0 est à 3,3 V)
uint8_t mpuAddress = 0;

// Vérifie si un composant répond à cette adresse
bool devicePresent(uint8_t address) {
  Wire.beginTransmission(address);
  return Wire.endTransmission() == 0;
}

// Lit un registre d'un octet
uint8_t readRegister(uint8_t reg) {
  Wire.beginTransmission(mpuAddress);
  Wire.write(reg);
  Wire.endTransmission(false);
  Wire.requestFrom(mpuAddress, (uint8_t)1);
  return Wire.read();
}

// Écrit un registre d'un octet
void writeRegister(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(mpuAddress);
  Wire.write(reg);
  Wire.write(value);
  Wire.endTransmission();
}

void setup() {
  Serial.begin(115200);
  delay(3000);  // laisse le temps au port USB d'apparaître

  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(400000);

  // Recherche du capteur aux deux adresses possibles
  if (devicePresent(0x68))
    mpuAddress = 0x68;
  else if (devicePresent(0x69))
    mpuAddress = 0x69;

  if (mpuAddress == 0) {
    Serial.println(
        "MPU6050 introuvable : verifiez le cablage (VCC, GND, SDA, SCL)");
    while (true) delay(1000);
  }

  // Un vrai MPU6050 répond 0x68 ; certains clones répondent une autre valeur
  Serial.printf("Capteur trouve a l'adresse 0x%02X, WHO_AM_I = 0x%02X\n",
                mpuAddress, readRegister(REG_WHO_AM_I));

  // Le capteur démarre en veille : on le réveille
  writeRegister(REG_PWR_MGMT_1, 0x00);
  delay(100);
}

void loop() {
  // Lecture des 14 octets : accélération (6), température (2), gyroscope (6)
  Wire.beginTransmission(mpuAddress);
  Wire.write(REG_ACCEL_XOUT_H);
  Wire.endTransmission(false);
  if (Wire.requestFrom(mpuAddress, (uint8_t)14) != 14) {
    Serial.println("Erreur de lecture I2C");
    delay(500);
    return;
  }

  // Chaque valeur est sur 16 bits signés, octet de poids fort en premier
  int16_t raw[7];
  for (int i = 0; i < 7; i++) {
    raw[i] = (int16_t)((Wire.read() << 8) | Wire.read());
  }

  // Conversion avec les échelles par défaut : ±2 g et ±250 °/s
  float accelX = raw[0] / 16384.0f;
  float accelY = raw[1] / 16384.0f;
  float accelZ = raw[2] / 16384.0f;
  float temperature = raw[3] / 340.0f + 36.53f;
  float gyroX = raw[4] / 131.0f;
  float gyroY = raw[5] / 131.0f;
  float gyroZ = raw[6] / 131.0f;

  Serial.printf(
      "Accel (g) : %6.2f %6.2f %6.2f | Gyro (deg/s) : %7.1f %7.1f %7.1f | Temp "
      ": %.1f C\n",
      accelX, accelY, accelZ, gyroX, gyroY, gyroZ, temperature);

  delay(200);
}

MPU6050 + BLE GamePad#

// ---------------------------------------------------------------------------
// MPU6050 + manette Bluetooth (ESP32-BLE-Gamepad) sur un ESP32-S3
// - Stick gauche piloté par l'inclinaison : accelX -> axe X, accelY -> axe Y
// - Calcul du roulis, du tangage et du lacet relatifs à la position de départ
// ---------------------------------------------------------------------------
#include <Arduino.h>
#include <Wire.h>
#include <BleGamepad.h>

// Broches I2C
const uint8_t SDA_PIN = 8;
const uint8_t SCL_PIN = 9;

// Registres du MPU6050
const uint8_t REG_PWR_MGMT_1 = 0x6B;    // gestion de l'alimentation
const uint8_t REG_ACCEL_XOUT_H = 0x3B;  // début des 14 octets de mesures

// Plage des axes de la manette
const int16_t AXIS_MIN = 0;
const int16_t AXIS_MAX = 32767;

// Période de mise à jour (ms) : 50 fois par seconde
const uint32_t UPDATE_PERIOD_MS = 20;

// Filtre complémentaire : part du gyroscope (le reste vient de l'accéléromètre)
const float GYRO_WEIGHT = 0.98f;

// Nombre de mesures pour calibrer le gyroscope au démarrage (environ 1 s)
const int CALIBRATION_SAMPLES = 500;

// Nom visible lors de l'appairage Bluetooth
BleGamepad bleGamepad("ESP32 Tilt Gamepad", "DIY", 100);

// Adresse I2C du capteur, 0 tant qu'il n'est pas trouvé
uint8_t mpuAddress = 0;

// Décalage du gyroscope au repos (valeurs brutes), mesuré à la calibration
float gyroBiasX = 0, gyroBiasY = 0, gyroBiasZ = 0;

// Orientation absolue (degrés) et position de référence
float roll = 0, pitch = 0, yaw = 0;
float rollZero = 0, pitchZero = 0, yawZero = 0;
bool orientationReady = false;  // faux jusqu'à la première mesure
uint32_t lastMicros = 0;        // instant de la mesure précédente (µs)

// Vérifie si un composant répond à cette adresse
bool devicePresent(uint8_t address) {
  Wire.beginTransmission(address);
  return Wire.endTransmission() == 0;
}

// Lit les 7 valeurs brutes : accélération XYZ, température, gyroscope XYZ
bool readRaw(int16_t raw[7]) {
  Wire.beginTransmission(mpuAddress);
  Wire.write(REG_ACCEL_XOUT_H);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(mpuAddress, (uint8_t)14) != 14) return false;

  // Chaque valeur est sur 16 bits signés, octet de poids fort en premier
  for (int i = 0; i < 7; i++) {
    raw[i] = (int16_t)((Wire.read() << 8) | Wire.read());
  }
  return true;
}

// Mesure le décalage du gyroscope. Le capteur doit rester immobile.
void calibrateGyro() {
  long sumX = 0, sumY = 0, sumZ = 0;
  int count = 0;
  int16_t raw[7];

  for (int i = 0; i < CALIBRATION_SAMPLES; i++) {
    if (readRaw(raw)) {
      sumX += raw[4];
      sumY += raw[5];
      sumZ += raw[6];
      count++;
    }
    delay(2);
  }

  if (count > 0) {
    gyroBiasX = (float)sumX / count;
    gyroBiasY = (float)sumY / count;
    gyroBiasZ = (float)sumZ / count;
  }
}

// Cherche le capteur, le réveille et calibre le gyroscope
bool initSensor() {
  if (devicePresent(0x68)) mpuAddress = 0x68;
  else if (devicePresent(0x69)) mpuAddress = 0x69;
  else return false;

  // Le capteur démarre en veille : on le réveille
  Wire.beginTransmission(mpuAddress);
  Wire.write(REG_PWR_MGMT_1);
  Wire.write(0x00);
  if (Wire.endTransmission() != 0) {
    mpuAddress = 0;
    return false;
  }
  delay(100);

  Serial.println("Calibration du gyroscope : ne pas bouger le capteur...");
  calibrateGyro();
  orientationReady = false;  // l'orientation sera réinitialisée à la prochaine mesure

  Serial.printf("MPU6050 pret a l'adresse 0x%02X\n", mpuAddress);
  return true;
}

// Définit la position actuelle comme référence (angles relatifs à zéro)
void resetOrientation() {
  rollZero = roll;
  pitchZero = pitch;
  yawZero = yaw;
}

// Ramène un angle dans l'intervalle -180° à +180°
float wrapAngle(float angle) {
  while (angle > 180.0f) angle -= 360.0f;
  while (angle < -180.0f) angle += 360.0f;
  return angle;
}

// Met à jour roulis, tangage et lacet à partir des mesures.
// Accélérations en g, vitesses de rotation en degrés par seconde.
void updateOrientation(float accelX, float accelY, float accelZ,
                       float gyroX, float gyroY, float gyroZ) {
  // Angles donnés par la gravité seule (bruités mais sans dérive)
  float accelRoll = atan2f(accelY, accelZ) * RAD_TO_DEG;
  float accelPitch = atan2f(-accelX, sqrtf(accelY * accelY + accelZ * accelZ)) * RAD_TO_DEG;

  uint32_t nowMicros = micros();

  // Première mesure : on part des angles de l'accéléromètre et on fixe la référence
  if (!orientationReady) {
    roll = accelRoll;
    pitch = accelPitch;
    yaw = 0;
    resetOrientation();
    lastMicros = nowMicros;
    orientationReady = true;
    return;
  }

  // Temps écoulé depuis la mesure précédente, en secondes
  float dt = (nowMicros - lastMicros) / 1000000.0f;
  lastMicros = nowMicros;

  // Filtre complémentaire : le gyroscope suit les mouvements rapides,
  // l'accéléromètre corrige lentement la dérive
  roll = GYRO_WEIGHT * (roll + gyroX * dt) + (1.0f - GYRO_WEIGHT) * accelRoll;
  pitch = GYRO_WEIGHT * (pitch + gyroY * dt) + (1.0f - GYRO_WEIGHT) * accelPitch;

  // Lacet : intégration du gyroscope seul (dérive lente inévitable)
  yaw = wrapAngle(yaw + gyroZ * dt);
}

// Convertit une accélération (-1 g à +1 g) en valeur d'axe de manette
int16_t accelToAxis(float accel) {
  accel = constrain(accel, -1.0f, 1.0f);
  return (int16_t)((accel + 1.0f) * 0.5f * (AXIS_MAX - AXIS_MIN) + AXIS_MIN);
}

void setup() {
  Serial.begin(115200);
  delay(3000);  // laisse le temps au port USB d'apparaître

  // 1) Le bus I2C et le capteur d'abord
  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(400000);
  if (!initSensor()) {
    Serial.println("MPU6050 introuvable : nouvelle tentative chaque seconde");
  }

  // 2) La manette Bluetooth ensuite, avec une plage d'axes explicite
  BleGamepadConfiguration config;
  config.setAxesMin(AXIS_MIN);
  config.setAxesMax(AXIS_MAX);
  bleGamepad.begin(&config);

  Serial.println("Manette Bluetooth demarree, en attente de connexion");
}

void loop() {
  static uint32_t lastUpdate = 0;
  static uint32_t lastPrint = 0;
  static uint32_t lastRetry = 0;
  uint32_t now = millis();

  // Capteur absent : on réessaie une fois par seconde
  if (mpuAddress == 0) {
    if (now - lastRetry >= 1000) {
      lastRetry = now;
      initSensor();
    }
    return;
  }

  // Mise à jour à cadence fixe, sans delay()
  if (now - lastUpdate < UPDATE_PERIOD_MS) return;
  lastUpdate = now;

  int16_t raw[7];
  if (!readRaw(raw)) return;

  // Conversion avec les échelles par défaut : ±2 g et ±250 °/s
  float accelX = raw[0] / 16384.0f;
  float accelY = raw[1] / 16384.0f;
  float accelZ = raw[2] / 16384.0f;
  float gyroX = (raw[4] - gyroBiasX) / 131.0f;
  float gyroY = (raw[5] - gyroBiasY) / 131.0f;
  float gyroZ = (raw[6] - gyroBiasZ) / 131.0f;

  updateOrientation(accelX, accelY, accelZ, gyroX, gyroY, gyroZ);

  // Angles relatifs à la position de référence
  float relRoll = wrapAngle(roll - rollZero);
  float relPitch = wrapAngle(pitch - pitchZero);
  float relYaw = wrapAngle(yaw - yawZero);

  // Manette : toujours pilotée par accelX et accelY
  int16_t axisX = accelToAxis(accelX);
  int16_t axisY = accelToAxis(accelY);
  if (bleGamepad.isConnected()) {
    bleGamepad.setLeftThumb(axisX, axisY);
  }

  // Affichage de contrôle, 5 fois par seconde
  if (now - lastPrint >= 200) {
    lastPrint = now;
    Serial.printf("Roulis %7.1f | Tangage %7.1f | Lacet %7.1f | Axes %5d %5d | %s\n",
                  relRoll, relPitch, relYaw, axisX, axisY,
                  bleGamepad.isConnected() ? "connecte" : "non connecte");
  }
}

Reception multi telecommande IR#

// ---------------------------------------------------------------------------
// 4 récepteurs infrarouges VS1838B sur un ESP32-S3 avec IRremote
// Basé sur l'exemple MultipleReceivers : une instance IRrecv par récepteur,
// toutes décodées en parallèle. Protocole Sony uniquement.
// ---------------------------------------------------------------------------

// installer la librairie IRremote dans le fichier platform.ini
// lib_deps =
//  z3t0/IRremote @ ^4.7.1

#include <Arduino.h>

// Ces définitions doivent être placées avant #include <IRremote.hpp>
#define DECODE_SONY  // on ne décode que le protocole Sony
#define SUPPORT_MULTIPLE_RECEIVER_INSTANCES  // autorise plusieurs récepteurs
void UserIRReceiveTimerInterruptHandler();   // définie plus bas

#include <IRremote.hpp>

// Nombre de récepteurs
const int NUM_RECEIVERS = 4;

// Broches pour ESP32-S3 (une par récepteur)
const uint8_t PINS[NUM_RECEIVERS] = {4, 5, 6, 7};

// Le premier récepteur est l'instance globale IrReceiver de la bibliothèque.
// Les trois autres sont des instances supplémentaires.
IRrecv receiver1(PINS[1]);
IRrecv receiver2(PINS[2]);
IRrecv receiver3(PINS[3]);

// Tous les récepteurs, dans le même ordre que PINS
IRrecv* receivers[NUM_RECEIVERS] = {&IrReceiver, &receiver1, &receiver2,
                                    &receiver3};

// ---------------------------------------------------------------------------
// Fonctions de rappel : une par récepteur, appelées depuis loop().
// Remplacez leur contenu par vos propres actions.
// ---------------------------------------------------------------------------
typedef void (*IrHandler)(uint16_t addr, uint16_t cmd);

void printCode(int index, uint16_t addr, uint16_t cmd) {
  Serial.printf("Recepteur %d : Sony, adresse 0x%02X, commande 0x%02X\n", index,
                addr, cmd);
}

void onReceiver0(uint16_t addr, uint16_t cmd) { printCode(0, addr, cmd); }
void onReceiver1(uint16_t addr, uint16_t cmd) { printCode(1, addr, cmd); }
void onReceiver2(uint16_t addr, uint16_t cmd) { printCode(2, addr, cmd); }
void onReceiver3(uint16_t addr, uint16_t cmd) { printCode(3, addr, cmd); }

// Table des fonctions de rappel, dans le même ordre que PINS
IrHandler handlers[NUM_RECEIVERS] = {
    onReceiver0,
    onReceiver1,
    onReceiver2,
    onReceiver3,
};

// ---------------------------------------------------------------------------
// Initialisation
// ---------------------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  delay(3000);  // laisse le temps au port USB d'apparaître

  // Démarre le premier récepteur et le minuteur partagé, sans clignotement de
  // LED
  IrReceiver.begin(PINS[0], DISABLE_LED_FEEDBACK);

  Serial.println("Pret : 4 recepteurs infrarouges Sony a l'ecoute");
}

// ---------------------------------------------------------------------------
// Boucle principale : traite chaque récepteur et appelle la bonne fonction
// ---------------------------------------------------------------------------
void loop() {
  for (int i = 0; i < NUM_RECEIVERS; i++) {
    IRrecv* receiver = receivers[i];
    if (!receiver->decode()) continue;

    // On copie le résultat avant de relancer la réception
    decode_type_t protocol = receiver->decodedIRData.protocol;
    uint16_t addr = receiver->decodedIRData.address;
    uint16_t cmd = receiver->decodedIRData.command;
    bool isRepeat = receiver->decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT;

    receiver->resume();  // prêt pour la trame suivante

    // Une télécommande Sony envoie chaque trame au moins 3 fois :
    // on ignore les répétitions pour n'appeler la fonction qu'une fois par
    // appui
    if (protocol == SONY && !isRepeat) {
      handlers[i](addr, cmd);
    }
  }
}

// ---------------------------------------------------------------------------
// Appelée par la bibliothèque à chaque échantillonnage (toutes les 50 µs),
// juste après le traitement du premier récepteur. On y traite les autres.
// ---------------------------------------------------------------------------
IRAM_ATTR void UserIRReceiveTimerInterruptHandler() {
  receiver1.ReceiveInterruptHandler();
  receiver2.ReceiveInterruptHandler();
  receiver3.ReceiveInterruptHandler();
}

Lecture RAW VS1838B#

// ---------------------------------------------------------------------------
// Lecture brute d'un récepteur infrarouge VS1838B sur un ESP32-S3
// Enregistre la durée de chaque impulsion et de chaque silence, quel que soit
// le protocole de la télécommande, puis les affiche après chaque trame.
// ---------------------------------------------------------------------------
#include <Arduino.h>

const uint8_t IR_PIN = 4;

// Nombre maximal de fronts enregistrés par trame
const int MAX_EDGES = 400;

// Silence qui marque la fin d'une trame (µs)
const uint32_t FRAME_GAP_US = 15000;

// Instants des fronts (µs), remplis par l'interruption
volatile uint32_t edgeTimes[MAX_EDGES];
volatile int edgeCount = 0;
volatile uint32_t lastEdge = 0;

// Interruption : appelée à chaque changement de niveau de la broche
void IRAM_ATTR onChange() {
  uint32_t now = micros();
  if (edgeCount < MAX_EDGES) edgeTimes[edgeCount++] = now;
  lastEdge = now;
}

void setup() {
  Serial.begin(115200);
  delay(3000);  // laisse le temps au port USB d'apparaître

  // La sortie du VS1838B possède déjà sa résistance de rappel interne
  pinMode(IR_PIN, INPUT);
  attachInterrupt(IR_PIN, onChange, CHANGE);

  Serial.println("Pret : appuyez sur une touche de la telecommande");
}

void loop() {
  // On lit lastEdge avant micros() pour éviter une différence négative
  uint32_t last = lastEdge;
  if (edgeCount == 0 || micros() - last < FRAME_GAP_US) return;

  // Trame terminée : on copie les données en bloquant brièvement les interruptions
  static uint32_t frame[MAX_EDGES];
  noInterrupts();
  int count = edgeCount;
  for (int i = 0; i < count; i++) frame[i] = edgeTimes[i];
  edgeCount = 0;
  interrupts();

  // Le premier front est le début d'une impulsion, donc les durées alternent :
  // impulsion (+), silence (-), impulsion (+), ...
  Serial.printf("Trame : %d durees (us)\n", count - 1);
  for (int i = 1; i < count; i++) {
    uint32_t duration = frame[i] - frame[i - 1];
    Serial.printf("%c%lu ", (i % 2 == 1) ? '+' : '-', (unsigned long)duration);
    if (i % 16 == 0) Serial.println();
  }
  Serial.println("\n");
}

Matrice analogique#

Une simple matrice de 4x4 sans diodes

const int ROWS[4] = {...}, COLS[4] = {...};   // COLS on ADC1 pins (GPIO 1 to 10), 100 kΩ pull-down each

int readCell(int r, int c) {
  for (int i = 0; i < 4; i++) { pinMode(ROWS[i], OUTPUT); digitalWrite(ROWS[i], LOW); }
  digitalWrite(ROWS[r], HIGH);              // seule la ligne lue est HIGH
  delayMicroseconds(200);                   // Laisser la ligne se stabiliser (eau + fils = capacitance)
  int v = analogRead(COLS[c]);
  digitalWrite(ROWS[r], LOW);               // retour à 0 V: aucun courant ne passe entre deux lectures 
  return v;
}

Tachymètre#

// Tachymètre avec capteur infrarouge
// Arduino Uno/Nano : interruption sur D2 ou D3

#include <Arduino.h>

const byte CAPTEUR_IR = 2;
const byte TOURS_PAR_IMPULSION = 1;

volatile unsigned long impulsions = 0;
volatile unsigned long derniereImpulsion = 0;

unsigned long derniereMesure = 0;
const unsigned long INTERVALLE_MESURE = 1000; // 1 seconde

void compterImpulsion() {
  unsigned long maintenant = micros();

  // Filtre anti-parasites : ignore les impulsions trop rapprochées
  if (maintenant - derniereImpulsion > 2000) {
    impulsions++;
    derniereImpulsion = maintenant;
  }
}

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

  // Beaucoup de modules IR ont une sortie active à LOW
  pinMode(CAPTEUR_IR, INPUT_PULLUP);

  attachInterrupt(
    digitalPinToInterrupt(CAPTEUR_IR),
    compterImpulsion,
    FALLING
  );

  Serial.println("Tachymetre pret");
}

void loop() {
  unsigned long maintenant = millis();

  if (maintenant - derniereMesure >= INTERVALLE_MESURE) {
    derniereMesure = maintenant;

    noInterrupts();
    unsigned long nombreImpulsions = impulsions;
    impulsions = 0;
    interrupts();

    // Comme la mesure dure 1 seconde :
    // RPM = impulsions par seconde x 60
    float rpm = (nombreImpulsions * 60000.0) /
                (INTERVALLE_MESURE * TOURS_PAR_IMPULSION);

    Serial.print("Vitesse : ");
    Serial.print(rpm);
    Serial.println(" tr/min");
  }
}
Éditer