🔗 Dopiero zaczynasz? Wróć do prostszej wersji tego projektu z dwoma czujnikami i bez PWM: Robocik-autko z AI na ESP8266 →

Rozbudowana wersja robota-autka z lokalnym AI — tym razem na ESP32. Dwa rdzenie CPU, 520 kB RAM i pełna obsługa sprzętowego PWM otwierają możliwości niedostępne na ESP8266: trzy czujniki HC-SR04 (przód, lewy bok, prawy bok), płynna regulacja prędkości i bogatszy model Edge Impulse rozróżniający sześć scenariuszy jazdy.

Co nowego w porównaniu do wersji ESP8266?

  • Trzy HC-SR04 zamiast dwóch — robot ma pełny obraz otoczenia: przód, lewo, prawo
  • Sprzętowe PWM (LEDC) — płynna regulacja prędkości obu silników niezależnie
  • Sześć klas ML — w tym wąski korytarz i blokada totalna
  • Dwa rdzenie — inference można wydzielić na Core 0, sterowanie silnikami na Core 1
  • Więcej GPIO — ponad 30 pinów bez kompromisów

Lista materiałów

  • ESP32 DevKitC (30-pin lub 38-pin) — ~15 zł
  • Podwozie 2WD lub 4WD z silnikami DC, przekładnia ≥ 1:48 — ~30 zł
  • Sterownik L298N lub TB6612FNG — ~10 zł
  • HC-SR04 × 3 (przód, lewy bok, prawy bok) — ~6 zł/szt.
  • Zasilanie: 2× 18650 Li-Ion + holder + BMS lub powerbank USB — ~30 zł
  • Przewody dupont żeńsko-męskie, płytka stykowa — ~8 zł

HC-SR04 na ESP32 — kilka sztuk, bez I²C

Tak samo jak na ESP8266, standardowy HC-SR04 podłączamy bezpośrednio do pinów GPIO — nie potrzeba wersji I²C. Każdy czujnik zajmuje tylko dwa piny: TRIG i ECHO. ESP32 ma ponad 30 GPIO, więc trzy czujniki, sterownik silników i jeszcze coś zostaje w rezerwie.

💡 Bezpieczne GPIO na ESP32: Bez ograniczeń bootstrapujących: GPIO 4, 5, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33. Unikaj GPIO 0, 2, 12, 15 dla linii ECHO — mogą wymagać konkretnego poziomu logicznego podczas startu układu.

Schemat połączeń

HC-SR04 #1 — PRZÓD
  VCC  →  5V    GND  →  GND
  TRIG →  GPIO 5    ECHO →  GPIO 4

HC-SR04 #2 — LEWY BOK
  VCC  →  5V    GND  →  GND
  TRIG →  GPIO 18   ECHO →  GPIO 19

HC-SR04 #3 — PRAWY BOK
  VCC  →  5V    GND  →  GND
  TRIG →  GPIO 22   ECHO →  GPIO 23

L298N — kierunek
  IN1  →  GPIO 25   (lewy silnik — przód)
  IN2  →  GPIO 26   (lewy silnik — tył)
  IN3  →  GPIO 27   (prawy silnik — przód)
  IN4  →  GPIO 14   (prawy silnik — tył)

L298N — prędkość (PWM)
  ENA  →  GPIO 33   (lewy silnik)
  ENB  →  GPIO 32   (prawy silnik)

Szkic do zbierania danych — 3 czujniki

// Zbieranie danych treningowych — ESP32, 3× HC-SR04
const int TRIG[] = {5,  18, 22};   // przód, lewo, prawo
const int ECHO[] = {4,  19, 23};

long zmierz(int trig, int echo) {
  digitalWrite(trig, LOW);  delayMicroseconds(2);
  digitalWrite(trig, HIGH); delayMicroseconds(10);
  digitalWrite(trig, LOW);
  long t = pulseIn(echo, HIGH, 25000);
  return t == 0 ? 200 : t / 58;
}

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < 3; i++) {
    pinMode(TRIG[i], OUTPUT);
    pinMode(ECHO[i], INPUT);
  }
}

void loop() {
  for (int i = 0; i < 3; i++) {
    Serial.print(zmierz(TRIG[i], ECHO[i]));
    if (i < 2) Serial.print(",");
  }
  Serial.println();
  delay(100);
}

Konfiguracja impulsu — 6 klas, 3 kanały

W Edge Impulse utwórz projekt z 3 kanałami wejściowymi (przód, lewo, prawo). Zbierz po 80–100 próbek na każdą z klas:

  • wolna_droga — wszystkie trzy > 35 cm
  • przeszkoda_przod — przód < 20 cm
  • przeszkoda_lewo — lewy < 15 cm
  • przeszkoda_prawo — prawy < 15 cm
  • waski_korytarz — lewy i prawy < 20 cm, przód > 30 cm
  • blokada — wszystkie trzy < 15 cm (ściana z każdej strony)

Ustawienia impulsu: Window 1000 ms, increase 100 ms, blok Flatten, sieć klasyfikacyjna 64→32→6, 80 epok, LR 0.001. ESP32 bez problemu przyjmie model do 200 kB Flash i 50 kB RAM.

Pełny kod robota z regulacją prędkości

#include <Arduino.h>
#include <moj_robot_esp32_inferencing.h>

const int TRIG[] = {5,  18, 22};   // przód, lewo, prawo
const int ECHO[] = {4,  19, 23};

const int IN1 = 25, IN2 = 26;      // lewy silnik
const int IN3 = 27, IN4 = 14;      // prawy silnik
const int ENA = 33, ENB = 32;
const int CH_L = 0, CH_P = 1;

long zmierz(int trig, int echo) {
  digitalWrite(trig, LOW);  delayMicroseconds(2);
  digitalWrite(trig, HIGH); delayMicroseconds(10);
  digitalWrite(trig, LOW);
  long t = pulseIn(echo, HIGH, 25000);
  return t == 0 ? 200 : t / 58;
}

void predkosc(int l, int p) { ledcWrite(CH_L, l); ledcWrite(CH_P, p); }

void jedz(int s = 190)   { predkosc(s,s);
  digitalWrite(IN1,HIGH);digitalWrite(IN2,LOW);
  digitalWrite(IN3,HIGH);digitalWrite(IN4,LOW); }
void cofnij(int s = 160) { predkosc(s,s);
  digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH);
  digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH); }
void skrecL(int s = 170) { predkosc(0,s);
  digitalWrite(IN1,LOW); digitalWrite(IN2,LOW);
  digitalWrite(IN3,HIGH);digitalWrite(IN4,LOW); }
void skrecP(int s = 170) { predkosc(s,0);
  digitalWrite(IN1,HIGH);digitalWrite(IN2,LOW);
  digitalWrite(IN3,LOW); digitalWrite(IN4,LOW); }
void stoj()              { predkosc(0,0);
  digitalWrite(IN1,LOW); digitalWrite(IN2,LOW);
  digitalWrite(IN3,LOW); digitalWrite(IN4,LOW); }

float bufor[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
int   idx = 0;

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < 3; i++) {
    pinMode(TRIG[i],OUTPUT); pinMode(ECHO[i],INPUT);
  }
  for (int p : {IN1,IN2,IN3,IN4}) pinMode(p,OUTPUT);

  ledcSetup(CH_L, 1000, 8); ledcAttachPin(ENA, CH_L);
  ledcSetup(CH_P, 1000, 8); ledcAttachPin(ENB, CH_P);

  stoj();
  delay(2000);
}

void loop() {
  if (idx + 3 <= EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
    for (int i = 0; i < 3; i++)
      bufor[idx++] = (float)zmierz(TRIG[i], ECHO[i]);
  }

  if (idx >= EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
    idx = 0;
    signal_t sig;
    numpy::signal_from_buffer(bufor, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &sig);
    ei_impulse_result_t wynik;
    if (run_classifier(&sig, &wynik, false) != EI_IMPULSE_OK) return;

    int best = 0;
    for (int i = 1; i < EI_CLASSIFIER_LABEL_COUNT; i++)
      if (wynik.classification[i].value > wynik.classification[best].value) best = i;

    String k = wynik.classification[best].label;
    Serial.printf("[AI] %s (%.0f%%)\n", k.c_str(), wynik.classification[best].value * 100);

    if      (k == "wolna_droga")     jedz(200);
    else if (k == "przeszkoda_przod") { cofnij(); delay(400); skrecP(); delay(300); }
    else if (k == "przeszkoda_lewo")  skrecP();
    else if (k == "przeszkoda_prawo") skrecL();
    else if (k == "waski_korytarz")   jedz(110);   // wolniej przez wąski
    else if (k == "blokada")        { cofnij(); delay(600); skrecP(); delay(500); }
    else                              stoj();
  }
  delay(100);
}

Opcja: inference na Core 0, silniki na Core 1

ESP32 ma dwa rdzenie Xtensa LX6. Wydzielenie inference na osobny rdzeń eliminuje szarpanie silników wywołane czasem obliczeń modelu:

volatile String aktywnaKlasa = "stoj";

void zadanieAI(void* params) {
  float bufor[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
  int idx = 0;
  for (;;) {
    for (int i = 0; i < 3; i++) bufor[idx++] = (float)zmierz(TRIG[i], ECHO[i]);
    if (idx >= EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
      idx = 0;
      signal_t sig;
      numpy::signal_from_buffer(bufor, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &sig);
      ei_impulse_result_t w;
      if (run_classifier(&sig, &w, false) == EI_IMPULSE_OK) {
        int best = 0;
        for (int i = 1; i < EI_CLASSIFIER_LABEL_COUNT; i++)
          if (w.classification[i].value > w.classification[best].value) best = i;
        aktywnaKlasa = w.classification[best].label;
      }
    }
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}

void setup() {
  // ... inicjalizacja pinów, ledcSetup ...
  xTaskCreatePinnedToCore(zadanieAI, "AI", 8192, NULL, 1, NULL, 0); // Core 0
}

void loop() { // Core 1
  String k = aktywnaKlasa;
  if      (k == "wolna_droga")      jedz(200);
  else if (k == "przeszkoda_przod") { cofnij(); delay(400); skrecP(); delay(300); }
  else if (k == "przeszkoda_lewo")  skrecP();
  else if (k == "przeszkoda_prawo") skrecL();
  else if (k == "waski_korytarz")   jedz(110);
  else if (k == "blokada")        { cofnij(); delay(600); skrecP(); delay(500); }
  else                              stoj();
  delay(50);
}

Co dalej — rozszerzenia projektu

  • ESP32-CAM + OV2640: Dodaj klasyfikację obrazu — robot rozpoznaje kolory taśmy na podłodze i jedzie po trasie.
  • Anomaly detection: Blok wykrywania anomalii w Edge Impulse — robot zaświeci LED gdy trafi w nieznane środowisko.
  • MQTT + Home Assistant: Wysyłaj aktywną klasę i odległości przez WiFi na dashboard HA.
  • MPU6050: Dodaj żyroskop i akcelerometr jako czwarty i piąty kanał — model wykrywa poślizg i pochylenie terenu.
🔗 Prostsza wersja: Jeśli dopiero zaczynasz z TinyML lub masz tylko ESP8266, wróć do podstawowej wersji projektu z dwoma czujnikami: Robocik-autko z AI na ESP8266 →