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ị
- ESP32 DevKit V1 30 chân (hoặc loại chip nạp CH340).
- Khung xe 2 bánh: tấm đế, 2 động cơ giảm tốc vỏ vàng, 2 bánh xe, 1 bánh đa hướng.
- Module L298N.
- Khay pin 18650 loại 2 viên nối tiếp (2S), hai pin 18650 và một công tắc gạt.
- Dây cắm test board, ốc vít, băng keo hai mặt.
- Nếu muốn nguồn cho ESP32 ổn định hơn: mạch hạ áp LM2596 chỉnh về 5 V.
2. Bố trí và nối dây

| Từ | Tới |
|---|---|
| ESP32 D27, D26, D25 | L298N ENA, IN1, IN2 (bánh trái) |
| ESP32 D13, D33, D32 | L298N 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ắc | L298N 12V |
| L298N 5V (giữ jumper 5V) | ESP32 VIN |
| Pin −, L298N GND | ESP32 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

- 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,rlà 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">▲</button><span></span>
<button data-l="-1" data-r="1">◀</button><button id="dung" data-l="0" data-r="0">DỪNG</button><button data-l="1" data-r="-1">▶</button>
<span></span><button data-l="-1" data-r="-1">▼</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
TRANGvà được gửi khi điện thoại mở địa chỉ gốc/. nhanLenh()đọc hai tham sốl,rrồi gọichay(trai, phai).- Trong
loop(), nếu xe đang chạy mà quáHET_GIO_MSkhông có lệnh thì dừng.
5. Lái vi sai

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.
- Đườ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.
- Đ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.
- 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. - 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
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Không thấy Wi-Fi PYWORLD-XE | ESP32 chưa chạy chương trình; nguồn yếu | Xem 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ó Internet | Chọ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òng | Mộ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ấp | Duty dưới mức khởi động, pin yếu | Tă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ạy | Sụt áp lúc động cơ khởi động | Dù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út | Sóng yếu, lệnh đến trễ hơn 0,6 giây | Lại gần xe; tăng HET_GIO_MS lên 1000 |
8. Xem thêm
- ESP32 làm web server điều khiển relay qua Wi-Fi
- Kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh
- Nạp chương trình cho ESP32 qua Wi-Fi (OTA)
- ESP-NOW: làm tay điều khiển bằng một ESP32 thứ hai
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.
Xem thêm