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.
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.