(Từ khóa học: Lập trình & Điều khiển Động Cơ từ Cơ Bản tới Nâng Cao)
Mục tiêu bài học
Trong phần 3, bạn sẽ học cách lập trình cơ bản cho tay cầm điều khiển và xe robot, sử dụng ESP32-C3 và ESP32 DevKit kết hợp với module NRF24L01 để gửi – nhận lệnh điều khiển qua sóng RF 2.4GHz.
Kết thúc bài học, bạn sẽ tạo được một bộ điều khiển robot từ xa hoàn chỉnh:
- Tay cầm gửi lệnh di chuyển (Tiến, Lùi, Trái, Phải, Dừng).
- Xe robot nhận lệnh và điều khiển hai động cơ DC thông qua driver L298N.
1. Tổng quan chương trình
Hệ thống được chia thành hai chương trình độc lập:
| Thiết bị | Vi điều khiển | Chức năng |
|---|---|---|
| Tay cầm điều khiển (TX) | ESP32-C3 | Gửi lệnh điều khiển qua NRF24L01 |
| Xe robot (RX) | ESP32 DevKit 38Pin | Nhận lệnh và điều khiển động cơ |
Cả hai chương trình đều sử dụng thư viện RF24 (Arduino) để giao tiếp NRF24L01 qua SPI.
2. Chương trình cho tay cầm điều khiển (Transmitter – TX)
Thành phần:
- ESP32-C3
- NRF24L01
- 4 nút nhấn: Tiến, Lùi, Trái, Phải
- (Tùy chọn) Joystick để điều chỉnh tốc độ
Code mẫu:
#include <SPI.h>
#include <RF24.h>
// Khai báo chân kết nối NRF24L01
#define CE_PIN 7
#define CSN_PIN 10
RF24 radio(CE_PIN, CSN_PIN);
const byte address[6] = "ROBOT";
// Cấu trúc dữ liệu gói tin gửi đi
struct Command {
char action; // F, B, L, R, S
uint8_t speed; // 0-255
} cmd;
// Khai báo các nút điều khiển
#define BTN_F 2
#define BTN_B 3
#define BTN_L 8
#define BTN_R 9
#define JOY_Y 1 // trục Y (nếu có joystick)
void setup() {
Serial.begin(115200);
pinMode(BTN_F, INPUT_PULLUP);
pinMode(BTN_B, INPUT_PULLUP);
pinMode(BTN_L, INPUT_PULLUP);
pinMode(BTN_R, INPUT_PULLUP);
analogReadResolution(12);
SPI.begin();
radio.begin();
radio.setChannel(100);
radio.setDataRate(RF24_1MBPS);
radio.setPALevel(RF24_PA_LOW);
radio.openWritingPipe(address);
radio.stopListening();
Serial.println("Remote TX Ready");
}
char readButton() {
if (!digitalRead(BTN_F)) return 'F';
if (!digitalRead(BTN_B)) return 'B';
if (!digitalRead(BTN_L)) return 'L';
if (!digitalRead(BTN_R)) return 'R';
return 'S';
}
void loop() {
cmd.action = readButton();
int joy = analogRead(JOY_Y);
cmd.speed = constrain(map(joy, 0, 4095, 120, 255), 0, 255);
if (cmd.action == 'S') cmd.speed = 0;
radio.write(&cmd, sizeof(cmd));
Serial.printf("Send: %c (%d)\n", cmd.action, cmd.speed);
delay(20);
}
Giải thích:
- radio.openWritingPipe(address) → mở kênh gửi dữ liệu tới “ROBOT”.
- radio.write(&cmd, sizeof(cmd)) → gửi gói lệnh điều khiển.
- Dữ liệu gửi đi chỉ gồm 2 byte (action và speed) → cực kỳ nhanh.
- Mỗi 20ms (50Hz), tay cầm gửi 1 gói lệnh → điều khiển mượt mà.
3. Chương trình cho xe robot (Receiver – RX)
Thành phần:
- ESP32 DevKit 38Pin
- NRF24L01
- Driver L298N
- 2 motor DC
Code mẫu:
#include <SPI.h>
#include <RF24.h>
#define CE_PIN 17
#define CSN_PIN 16
RF24 radio(CE_PIN, CSN_PIN);
const byte address[6] = "ROBOT";
struct Command {
char action;
uint8_t speed;
} cmd;
#define IN1 26
#define IN2 27
#define IN3 25
#define IN4 33
#define ENA 14
#define ENB 32
void setupMotors() {
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
ledcSetup(0, 5000, 8); ledcAttachPin(ENA, 0);
ledcSetup(1, 5000, 8); ledcAttachPin(ENB, 1);
}
void drive(char action, uint8_t speed) {
switch (action) {
case 'F': // Forward
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
break;
case 'B': // Backward
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH);
break;
case 'L': // Turn Left
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
break;
case 'R': // Turn Right
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH);
break;
default: // Stop
digitalWrite(IN1, LOW); digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW); digitalWrite(IN4, LOW);
}
ledcWrite(0, speed);
ledcWrite(1, speed);
}
unsigned long lastPacket = 0;
void setup() {
Serial.begin(115200);
setupMotors();
SPI.begin();
radio.begin();
radio.setChannel(100);
radio.setDataRate(RF24_1MBPS);
radio.setPALevel(RF24_PA_LOW);
radio.openReadingPipe(1, address);
radio.startListening();
Serial.println("Robot RX Ready");
}
void loop() {
if (radio.available()) {
radio.read(&cmd, sizeof(cmd));
drive(cmd.action, cmd.speed);
Serial.printf("Recv: %c (%d)\n", cmd.action, cmd.speed);
lastPacket = millis();
}
// Dừng xe nếu mất tín hiệu > 300ms
if (millis() - lastPacket > 300) {
drive('S', 0);
}
}
4. Giải thích logic chương trình
| Thành phần | Vai trò | Mô tả hoạt động |
|---|---|---|
| radio.begin() | Khởi tạo module NRF24L01 | Chuẩn bị SPI và kênh RF |
| radio.write() | Gửi dữ liệu từ TX | Tay cầm gửi gói lệnh |
| radio.read() | Nhận dữ liệu từ RX | Xe nhận lệnh từ tay cầm |
| drive() | Hàm điều khiển động cơ | Thực thi lệnh tương ứng |
| lastPacket | Biến giám sát tín hiệu | Dừng xe khi mất kết nối |
5. Mẹo lập trình và kiểm thử
- Test từng phần: kiểm tra NRF trước, sau đó mới thêm điều khiển motor.
- Nếu “radio not responding” → kiểm tra CE/CSN, tụ 10µF gần NRF.
- Thêm Serial.println() để theo dõi dữ liệu truyền đi – nhận về.
- Sử dụng delay(20) để duy trì tốc độ gửi ~50Hz.
- Đảm bảo mass chung giữa ESP32 và L298N.
6. Kết quả mong đợi
Sau khi nạp chương trình:
- Khi bấm nút tiến/lùi/rẽ, robot phản hồi ngay lập tức.
- Khi thả tay khỏi nút, robot tự động dừng lại.
- Nếu tắt tay cầm → robot tự dừng trong 0.3 giây (failsafe).
7. Chuẩn bị cho bài tiếp theo
Bài tiếp theo – (phần 4): Tối ưu điều khiển, bạn sẽ học cách:
- Làm mượt tốc độ PWM.
- Giảm rung giật khi khởi động.
- Cải thiện phản hồi điều khiển.
- Thêm chế độ “Kid Mode” và “Sport Mode” cho tay cầm.


