Xe robot ESP32 điều khiển bằng điện thoại qua Wi-Fi: không cần router, không cần cài ứng dụng

Làm xe robot hai bánh với ESP32 và L298N, điều khiển bằng trình duyệt điện thoại qua Wi-Fi do ESP32 phát ra: bố trí, nối dây, code web, tự dừng khi mất sóng, lái vi sai, STEM.

Xe robot hai bánh điều khiển bằng điện thoại là dự án STEM được học sinh yêu thích nhất: vừa có cơ khí, vừa có điện tử, vừa có lập trình, và có thể mang ra sân thi đấu. Với ESP32, không cần mua thêm module Bluetooth hay cài ứng dụng riêng. ESP32 tự phát Wi-Fi, điện thoại kết nối vào và mở một trang web có các nút điều khiển.

Bài viết hướng dẫn bố trí linh kiện trên khung xe, nối dây, chương trình web điều khiển có cơ chế tự dừng khi mất sóng, nguyên lý lái vi sai và các gợi ý mở rộng cho giờ học STEM.

1. Linh kiện cần chuẩn bị

2. Bố trí và nối dây

Hình minh họa khung xe robot hai bánh nhìn từ trên với ESP32, khay pin 18650 và L298N đặt giữa thân xe, kèm bảng nối dây ESP32 với L298N và động cơ
Hình minh họa: bố trí linh kiện trên khung xe và bảng nối dây.
Bảng nối dây
TừTới
ESP32 D27, D26, D25L298N ENA, IN1, IN2 (bánh trái)
ESP32 D13, D33, D32L298N ENB, IN3, IN4 (bánh phải)
L298N OUT1, OUT2 / OUT3, OUT4Động cơ trái / động cơ phải
Pin + qua công tắcL298N 12V
L298N 5V (giữ jumper 5V)ESP32 VIN
Pin −, L298N GNDESP32 GND
  • Bánh trái dùng cùng chân với bài ESP32 điều khiển động cơ DC với L298N. Bánh phải dùng ENB, IN3, IN4.
  • Rút jumper ENA và ENB trên L298N. Giữ jumper 5V để mạch ổn áp trên L298N cấp 5 V cho ESP32 qua chân VIN.
  • Đặt khay pin ở giữa xe để trọng tâm thấp và cân hai bên.
  • Nạp chương trình qua USB khi công tắc pin đang tắt, để ESP32 không bị cấp nguồn từ hai phía cùng lúc.

3. Điều khiển bằng trình duyệt

Hình minh họa trang điều khiển xe robot trên điện thoại với các nút tiến, lùi, trái, phải, dừng và thanh tốc độ; điện thoại gửi lệnh GET /lenh tới ESP32 phát Wi-Fi 192.168.4.1 mỗi 200 ms khi giữ nút
Hình minh họa: trang điều khiển và đường đi của lệnh.
  • ESP32 chạy ở chế độ phát Wi-Fi (AP) tên PYWORLD-XE. Không cần router, nên mang ra sân trường vẫn dùng được.
  • Điện thoại kết nối vào Wi-Fi đó rồi mở http://192.168.4.1. Nếu điện thoại báo "không có Internet", chọn tiếp tục kết nối.
  • Khi giữ một nút, trang web gửi lệnh /lenh?l=…&r=… mỗi 200 ms; l, r là phần trăm tốc độ của bánh trái và bánh phải. Thả nút thì gửi lệnh dừng.
  • An toàn: nếu ESP32 không nhận lệnh mới trong 0,6 giây (điện thoại khóa màn hình, ra khỏi vùng sóng), xe tự dừng.

4. Chương trình

Không cần cài thêm thư viện: WiFi.h và WebServer.h có sẵn trong gói esp32. Nên đổi MAT_KHAU_AP thành mật khẩu riêng (từ 8 ký tự) để xe của nhóm khác không điều khiển nhầm được.

/*
  PYWORLD.VN - Xe robot hai bánh điều khiển bằng điện thoại qua Wi-Fi (ESP32 phát Wi-Fi riêng, không cần router)
  L298N: bánh trái ENA GPIO27, IN1 GPIO26, IN2 GPIO25; bánh phải ENB GPIO13, IN3 GPIO33, IN4 GPIO32; GND chung
  Điện thoại kết nối Wi-Fi "PYWORLD-XE" rồi mở http://192.168.4.1. Mất lệnh quá 0,6 giây thì xe tự dừng.
*/

#include <WiFi.h>
#include <WebServer.h>

const char *TEN_AP = "PYWORLD-XE";
const char *MAT_KHAU_AP = "pyworld123";      // tối thiểu 8 ký tự; nên đổi mật khẩu riêng

#define ENA 27
#define IN1 26
#define IN2 25
#define ENB 13
#define IN3 33
#define IN4 32

const unsigned long HET_GIO_MS = 600;       // không nhận lệnh trong khoảng này thì dừng xe
WebServer server(80);
unsigned long tLenhCuoi = 0;
bool dangChay = false;

// Một bánh: v từ -100 đến 100 (%)
void banh(int chanEN, int chanA, int chanB, int v) {
  v = constrain(v, -100, 100);
  digitalWrite(chanA, v > 0 ? HIGH : LOW);
  digitalWrite(chanB, v < 0 ? HIGH : LOW);
  ledcWrite(chanEN, (uint32_t)abs(v) * 1023 / 100);
}

void chay(int trai, int phai) {
  banh(ENA, IN1, IN2, trai);
  banh(ENB, IN3, IN4, phai);
  dangChay = (trai != 0 || phai != 0);
}

// Trang điều khiển: giữ nút thì gửi lệnh mỗi 200 ms, thả nút thì gửi lệnh dừng
const char TRANG[] PROGMEM = R"rawliteral(<!DOCTYPE html>
<html lang="vi"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Xe robot ESP32</title>
<style>
body{font-family:sans-serif;text-align:center;background:#f6f8fc;margin:0;padding:16px;touch-action:none;user-select:none}
h1{font-size:20px;color:#0f2747}
.luoi{display:grid;grid-template-columns:repeat(3,90px);grid-template-rows:repeat(3,90px);gap:10px;justify-content:center;margin:20px auto}
button{font-size:28px;border:0;border-radius:16px;background:#1f5fae;color:#fff}
button:active{background:#0f2747}
#dung{background:#e03131;font-size:18px}
</style></head><body>
<h1>Xe robot ESP32</h1>
<div class="luoi">
<span></span><button data-l="1" data-r="1">&#9650;</button><span></span>
<button data-l="-1" data-r="1">&#9664;</button><button id="dung" data-l="0" data-r="0">DỪNG</button><button data-l="1" data-r="-1">&#9654;</button>
<span></span><button data-l="-1" data-r="-1">&#9660;</button><span></span>
</div>
<p>Tốc độ: <b id="gt">70</b> %</p>
<input id="toc" type="range" min="30" max="100" value="70">
<script>
var hen=null,l=0,r=0;
function gui(){fetch('/lenh?l='+l+'&r='+r).catch(function(){});}
function bat(e){var t=document.getElementById('toc').value;l=e.target.dataset.l*t;r=e.target.dataset.r*t;gui();clearInterval(hen);hen=setInterval(gui,200);e.preventDefault();}
function tha(){if(hen===null)return;clearInterval(hen);hen=null;l=0;r=0;gui();}
document.querySelectorAll('button').forEach(function(b){b.addEventListener('pointerdown',bat);b.addEventListener('pointerup',tha);b.addEventListener('pointerleave',tha);b.addEventListener('pointercancel',tha);});
document.getElementById('toc').oninput=function(){document.getElementById('gt').textContent=this.value;};
</script></body></html>)rawliteral";

void trangChu() {
  server.send_P(200, "text/html; charset=utf-8", TRANG);
}

void nhanLenh() {
  int trai = server.arg("l").toInt();
  int phai = server.arg("r").toInt();
  chay(trai, phai);
  tLenhCuoi = millis();
  server.send(200, "text/plain", "ok");
}

void setup() {
  Serial.begin(115200);
  int chan[] = {IN1, IN2, IN3, IN4};
  for (int p : chan) pinMode(p, OUTPUT);
  ledcAttach(ENA, 1000, 10);
  ledcAttach(ENB, 1000, 10);
  chay(0, 0);

  WiFi.softAP(TEN_AP, MAT_KHAU_AP);
  Serial.printf("Wi-Fi: %s | mo trinh duyet: http://%s\n", TEN_AP, WiFi.softAPIP().toString().c_str());

  server.on("/", trangChu);
  server.on("/lenh", nhanLenh);
  server.begin();
}

void loop() {
  server.handleClient();
  if (dangChay && millis() - tLenhCuoi > HET_GIO_MS) {
    chay(0, 0);
    Serial.println("Mat lenh: dung xe");
  }
}
  • WiFi.softAP() phát Wi-Fi. Địa chỉ mặc định của ESP32 là 192.168.4.1, được in ra Serial Monitor khi khởi động.
  • Trang HTML lưu trong biến TRANG và được gửi khi điện thoại mở địa chỉ gốc /.
  • nhanLenh() đọc hai tham số l, r rồi gọi chay(trai, phai).
  • Trong loop(), nếu xe đang chạy mà quá HET_GIO_MS không có lệnh thì dừng.

5. Lái vi sai

Hình minh họa sáu kiểu lái xe hai bánh theo tốc độ bánh trái và bánh phải: tiến, lùi, quay trái tại chỗ, quay phải tại chỗ, rẽ trái và rẽ phải vòng rộng
Hình minh họa: tốc độ hai bánh quyết định hướng đi của xe.

Xe hai bánh không có bánh lái mà rẽ bằng cách cho hai bánh quay khác nhau. Hai bánh quay ngược chiều thì xe quay tại chỗ. Hai bánh cùng chiều nhưng khác tốc độ thì xe rẽ theo vòng cung, bánh chậm nằm ở phía trong.

6. Gợi ý dùng trong giờ học STEM

Mục tiêu: học sinh lắp ráp hoàn chỉnh một sản phẩm cơ điện tử, hiểu mạng Wi-Fi, máy chủ web, lái vi sai, và thử nghiệm để cải tiến.

  1. Đường đua thử thách. Dán băng keo trên sàn làm đường đua có khúc cua, tính thời gian các nhóm. Mỗi nhóm tự chọn tốc độ và cách lái.
  2. Đo phạm vi điều khiển. Đi xa dần tới khi xe tự dừng vì mất lệnh, đo khoảng cách. So sánh trong nhà và ngoài trời, có vật cản và không vật cản.
  3. Thêm nút rẽ vòng rộng. Sửa trang HTML: thêm nút có data-l="0.5", data-r="1". Đây là bài tập nhỏ về HTML và JavaScript.
  4. Xe chạy thẳng. Đặt cùng tốc độ hai bánh nhưng xe vẫn lệch, vì hai động cơ không giống hệt nhau. Thêm hệ số bù cho một bánh, hoặc dùng encoder và PID như bài giữ tốc độ trong cùng loạt.

7. Xử lý lỗi thường gặp

Lỗi thường gặp
Hiện tượngNguyên nhân thường gặpCách xử lý
Không thấy Wi-Fi PYWORLD-XEESP32 chưa chạy chương trình; nguồn yếuXem Serial Monitor khi cắm USB; kiểm tra pin đã sạc
Mở 192.168.4.1 không đượcĐiện thoại tự chuyển sang 4G vì Wi-Fi không có InternetChọn giữ kết nối Wi-Fi; tắt dữ liệu di động khi điều khiển
Bấm tiến mà xe quay vòngMột động cơ nối ngược cựcĐảo hai dây động cơ bên quay sai
Bánh không quay ở tốc độ thấpDuty dưới mức khởi động, pin yếuTăng tốc độ tối thiểu trên thanh trượt; sạc pin
ESP32 khởi động lại khi xe vừa chạySụt áp lúc động cơ khởi độngDùng pin tốt; cấp ESP32 bằng mạch hạ áp riêng; thêm tụ 470 µF ở chân VIN
Xe tự dừng giật cục khi đang giữ nútSóng yếu, lệnh đến trễ hơn 0,6 giâyLại gần xe; tăng HET_GIO_MS lên 1000

8. Xem thêm

Cùng loạt: ESP32 điều khiển động cơ DC với L298N, xe tránh vật cản với cảm biến siêu âm.


Tin tức liên quan

Hướng dẫn ESP32 làm web server điều khiển relay 4 kênh qua WiFi
Hướng dẫn ESP32 làm web server điều khiển relay 4 kênh qua WiFi

Hướng dẫn dùng ESP32 DevKit V1 làm web server trong mạng WiFi nội bộ, mở trang trên điện thoại để bật, tắt module relay 4 kênh; có sơ đồ nối dây, code Arduino IDE và lưu ý an toàn.

Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung
Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung

Hướng dẫn dùng ngắt GPIO trên ESP32 với Arduino IDE: attachInterrupt, các kiểu kích ngắt, chống dội nút nhấn, đếm xung đo tần số và quy tắc viết hàm ngắt an toàn.

Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient

Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn dùng module thẻ nhớ Micro SD với ESP32
Hướng dẫn dùng module thẻ nhớ Micro SD với ESP32

Hướng dẫn nối module thẻ nhớ Micro SD với ESP32 DevKit V1 qua SPI, định dạng thẻ FAT32, chạy code Arduino đọc ghi file, ghi dữ liệu định kỳ ra CSV và xử lý lỗi Card Mount Failed.

Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên
Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên

Hướng dẫn nối module LoRa SX1278 Ra-02 433 MHz với ESP32 qua SPI, cài thư viện LoRa trên Arduino IDE, viết chương trình gửi và nhận gói tin, đọc RSSI, SNR và xử lý lỗi thường gặp.

Hướng dẫn lưu dữ liệu vào bộ nhớ flash của ESP32 bằng thư viện Preferences
Hướng dẫn lưu dữ liệu vào bộ nhớ flash của ESP32 bằng thư viện Preferences

Hướng dẫn dùng thư viện Preferences trên ESP32 với Arduino IDE để lưu số đếm, trạng thái relay và thông số cài đặt vào bộ nhớ flash, không mất khi tắt nguồn.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.


Bình luận
Đã thêm vào giỏ hàng