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");
}
}