IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Lập trình & Điều khiển Động Cơ – Nâng cao – Bài 16: Thiết kế bộ điều khiển từ xa cho Robot (phần 4): Tối ưu điều khiển


(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 4, bạn sẽ học cách tối ưu hóa hệ thống điều khiển robot từ xa, giúp xe chạy mượt mà, phản ứng nhanh, giảm giật và ổn định hơn.

Đây là bước quan trọng giúp robot đạt cảm giác “real-time control”, tương tự như tay cầm điều khiển xe đua hoặc drone.

⚙️ 1. Vấn đề của điều khiển cơ bản

Trong phần trước, chúng ta đã điều khiển robot bằng cách gửi lệnh “F, B, L, R, S” từ tay cầm qua NRF24L01.

Mặc dù robot hoạt động đúng, nhưng vẫn tồn tại một số vấn đề cần tối ưu:

  • ? Xe giật mạnh khi khởi động hoặc đổi hướng.
  • ⏱️ Độ phản hồi chưa mượt (khi thả tay, xe dừng gấp).
  • ⚡ Cần điều chỉnh tốc độ linh hoạt theo joystick.
  • ? Tốc độ động cơ không đều giữa hai bánh (lệch hướng).

? 2. Giải pháp tối ưu

Nhóm cải tiếnMục tiêuGiải pháp
Điều khiển tốc độ mượt (Soft Start/Stop)Tránh giật khi khởi động/dừngDùng hàm tăng tốc/tăng giảm PWM dần theo thời gian
Độ nhạy tay cầmKiểm soát mượt khi dùng joystickDùng hàm map() và giới hạn “dead zone”
Chế độ điều khiểnPhù hợp từng người dùngTạo “Kid mode” (chậm) và “Sport mode” (nhạy)
Cân bằng bánh xeGiảm lệch hướng khi đi thẳngTinh chỉnh hệ số PWM hai bánh

⚙️ 3. Làm mượt tốc độ – Soft Start / Stop

Khi thay đổi hướng hoặc tốc độ đột ngột, mô-men xoắn của động cơ DC có thể làm xe giật hoặc trượt.

Giải pháp là thay đổi PWM từ từ bằng cách nội suy giữa tốc độ hiện tại và mục tiêu.

? Code ví dụ:

int currentSpeedL = 0, currentSpeedR = 0;

void smoothDrive(int targetL, int targetR) {
  int step = 5; // mức tăng/giảm PWM mỗi chu kỳ
  if (currentSpeedL < targetL) currentSpeedL += step;
  else if (currentSpeedL > targetL) currentSpeedL -= step;

  if (currentSpeedR < targetR) currentSpeedR += step;
  else if (currentSpeedR > targetR) currentSpeedR -= step;

  ledcWrite(0, constrain(currentSpeedL, 0, 255));
  ledcWrite(1, constrain(currentSpeedR, 0, 255));
}

✅ Ưu điểm:

  • Xe di chuyển mượt mà hơn.
  • Giảm dòng khởi động đột ngột → tiết kiệm pin.
  • Giảm áp lực cơ học lên bánh và trục motor.

? 4. Tối ưu độ nhạy Joystick – Dead Zone & Mapping

Khi joystick ở vị trí gần trung tâm, giá trị đọc có thể dao động nhẹ khiến robot di chuyển không mong muốn.

Ta thêm “vùng chết (dead zone)” để loại bỏ nhiễu.

? Code tay cầm:

int joyY = analogRead(1); // 0–4095
int deadZone = 300;

if (abs(joyY - 2048) < deadZone) joyY = 2048; // giữ nguyên ở giữa

// Map sang tốc độ PWM
int speed = map(joyY, 0, 4095, 120, 255);

? Kết quả: Tay cầm phản hồi mượt hơn, không rung nhẹ khi không chạm joystick.


? 5. Thêm chế độ điều khiển (Mode Switch)

Để phù hợp với từng đối tượng (người mới học – người đã quen), ta thêm 2 chế độ:

  • Kid Mode: tốc độ giới hạn, dễ điều khiển.
  • Sport Mode: phản hồi nhanh, tốc độ tối đa.

? Code ví dụ:

#define BTN_MODE 12
bool sportMode = false;

void loop() {
  if (!digitalRead(BTN_MODE)) {
    sportMode = !sportMode;
    delay(300); // chống dội phím
  }

  int joyY = analogRead(1);
  int speed = map(joyY, 0, 4095, 100, sportMode ? 255 : 180);
}

? Khi bật/tắt mode, hiển thị LED hoặc OLED để báo trạng thái.

⚙️ 6. Cân bằng động cơ hai bánh

Trong thực tế, hai bánh xe có thể khác nhau về ma sát hoặc mô-men, khiến robot đi lệch dù lệnh là “tiến thẳng”.

Giải pháp: nhân hệ số cân bằng cho mỗi bánh.

? Code:

#define MOTOR_L_FACTOR 1.00
#define MOTOR_R_FACTOR 0.95

int leftPWM = targetSpeed * MOTOR_L_FACTOR;
int rightPWM = targetSpeed * MOTOR_R_FACTOR;
smoothDrive(leftPWM, rightPWM);

⚖️ Dùng thực nghiệm để tinh chỉnh cho xe chạy thẳng đều.

? Code mới nhất:

tx_controller.ino — ESP32-C3 + NRF24L01 (Tay cầm)

#include <SPI.h>
#include <RF24.h>

// ================== NRF24 PINS (ESP32-C3) ==================
#define CE_PIN   7
#define CSN_PIN 10

// ================== INPUT PINS ==================
#define BTN_F   2
#define BTN_B   3
#define BTN_L   8
#define BTN_R   9
#define BTN_MODE 12            // Nhấn để chuyển Kid/Sport
#define JOY_Y    1             // Analog Y (0..4095)

// ================== RF CONFIG ==================
RF24 radio(CE_PIN, CSN_PIN);
const byte pipeAddr[6] = "ROBOT";

struct Command {
  char action;     // 'F','B','L','R','S'
  uint8_t speed;   // 0..255
};
Command cmd;

// ================== STATE ==================
bool sportMode = false;        // false = Kid, true = Sport
unsigned long lastModeToggle = 0;

// ================== HELPERS ==================
static inline bool pressed(uint8_t pin) { return !digitalRead(pin); }

char readAction() {
  if (pressed(BTN_F)) return 'F';
  if (pressed(BTN_B)) return 'B';
  if (pressed(BTN_L)) return 'L';
  if (pressed(BTN_R)) return 'R';
  return 'S';
}

uint8_t mapSpeedFromJoystick(bool sport) {
  // Dead zone quanh 2048
  const int center = 2048;
  const int dead   = 300;

  int raw = analogRead(JOY_Y); // 0..4095
  if (abs(raw - center) < dead) return 0; // coi như thả tay

  // Map 0..4095 -> 100..(180|255)
  int maxOut = sport ? 255 : 180;
  int sp = map(raw, 0, 4095, 100, maxOut);
  sp = constrain(sp, 0, 255);
  return (uint8_t)sp;
}

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);
  pinMode(BTN_MODE, INPUT_PULLUP);
  analogReadResolution(12);

  SPI.begin();
  radio.begin();
  radio.setChannel(100);
  radio.setDataRate(RF24_1MBPS);
  radio.setPALevel(RF24_PA_LOW);
  radio.setAutoAck(true);
  radio.enableDynamicPayloads();
  radio.openWritingPipe(pipeAddr);
  radio.stopListening();

  Serial.println("TX Ready (C3 + NRF24) | Mode: KID");
}

void loop() {
  // Toggle Kid/Sport
  if (pressed(BTN_MODE) && (millis() - lastModeToggle > 250)) {
    sportMode = !sportMode;
    lastModeToggle = millis();
    Serial.printf("Mode: %s\n", sportMode ? "SPORT" : "KID");
  }

  cmd.action = readAction();
  cmd.speed  = (cmd.action == 'S') ? 0 : mapSpeedFromJoystick(sportMode);

  // Nếu không dùng joystick: dùng ga mặc định
  if (cmd.speed == 0 && cmd.action != 'S') {
    cmd.speed = sportMode ? 200 : 150;
  }

  radio.write(&cmd, sizeof(cmd));
  // Debug gọn để không spam
  // Serial.printf("Send %c (%u)\n", cmd.action, cmd.speed);

  delay(20); // ~50Hz
}

rx_robot.ino — ESP32 DevKit 38-pin + NRF24L01 + L298N (Xe)

#include <SPI.h>
#include <RF24.h>

// ================== NRF24 PINS (ESP32 DevKit) ==================
#define CE_PIN   17
#define CSN_PIN  16

RF24 radio(CE_PIN, CSN_PIN);
const byte pipeAddr[6] = "ROBOT";

// ================== MOTOR PINS (L298N) ==================
#define IN1 26
#define IN2 27
#define IN3 25
#define IN4 33
#define ENA 14   // PWM Left
#define ENB 32   // PWM Right

// ================== OPTIONAL ALERT ==================
// #define LED_STATUS 2
// #define BUZZER     13

// ================== PACKET ==================
struct Command {
  char action;    // 'F','B','L','R','S'
  uint8_t speed;  // 0..255
};
Command cmd;

// ================== CONTROL STATE ==================
int currentSpeedL = 0, currentSpeedR = 0;     // PWM hiện tại
int targetSpeedL  = 0, targetSpeedR  = 0;     // PWM mục tiêu
bool dirLForward  = true, dirRForward = true; // hướng

// Cân bằng hai bánh (tinh chỉnh thực tế)
#define MOTOR_L_FACTOR 1.00
#define MOTOR_R_FACTOR 0.95

// Failsafe
unsigned long lastPacketMs = 0;

// ================== HELPERS ==================
void setupPWMs() {
  ledcSetup(0, 5000, 8); ledcAttachPin(ENA, 0);
  ledcSetup(1, 5000, 8); ledcAttachPin(ENB, 1);
}

void setMotorDir(bool leftFwd, bool rightFwd) {
  // Left
  digitalWrite(IN1, leftFwd ? HIGH : LOW);
  digitalWrite(IN2, leftFwd ? LOW  : HIGH);
  // Right
  digitalWrite(IN3, rightFwd ? HIGH : LOW);
  digitalWrite(IN4, rightFwd ? LOW  : HIGH);
}

void applyPWM(int leftPWM, int rightPWM) {
  ledcWrite(0, constrain(leftPWM, 0, 255));
  ledcWrite(1, constrain(rightPWM, 0, 255));
}

void smoothDriveStep() {
  const int step = 6; // speed ramp step

  if (currentSpeedL < targetSpeedL) currentSpeedL += step;
  else if (currentSpeedL > targetSpeedL) currentSpeedL -= step;

  if (currentSpeedR < targetSpeedR) currentSpeedR += step;
  else if (currentSpeedR > targetSpeedR) currentSpeedR -= step;

  applyPWM(currentSpeedL, currentSpeedR);
}

void stopNow() {
  targetSpeedL = targetSpeedR = 0;
  setMotorDir(true, true);
}

void handleCommand(const Command& c) {
  // Base speed + balance
  int baseL = (int)(c.speed * MOTOR_L_FACTOR);
  int baseR = (int)(c.speed * MOTOR_R_FACTOR);

  switch (c.action) {
    case 'F': // Forward
      dirLForward = true;  dirRForward = true;
      targetSpeedL = baseL; targetSpeedR = baseR;
      break;
    case 'B': // Backward
      dirLForward = false; dirRForward = false;
      targetSpeedL = baseL; targetSpeedR = baseR;
      break;
    case 'L': // Turn Left (left backward, right forward)
      dirLForward = false; dirRForward = true;
      targetSpeedL = baseL; targetSpeedR = baseR;
      break;
    case 'R': // Turn Right (left forward, right backward)
      dirLForward = true;  dirRForward = false;
      targetSpeedL = baseL; targetSpeedR = baseR;
      break;
    default:  // 'S' or unknown
      stopNow();
      break;
  }

  setMotorDir(dirLForward, dirRForward);
}

void setup() {
  Serial.begin(115200);

  pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  setupPWMs();

  // if defined(LED_STATUS) pinMode(LED_STATUS, OUTPUT);
  // if defined(BUZZER)     pinMode(BUZZER, OUTPUT);

  SPI.begin();
  radio.begin();
  radio.setChannel(100);
  radio.setDataRate(RF24_1MBPS);
  radio.setPALevel(RF24_PA_LOW);
  radio.setAutoAck(true);
  radio.enableDynamicPayloads();
  radio.openReadingPipe(1, pipeAddr);
  radio.startListening();

  setMotorDir(true, true);
  applyPWM(0, 0);

  Serial.println("RX Ready (DevKit + NRF24 + L298N)");
}

void loop() {
  // Nhận lệnh
  if (radio.available()) {
    radio.read(&cmd, sizeof(cmd));
    lastPacketMs = millis();

    handleCommand(cmd);
    // Serial.printf("Recv %c (%u)\n", cmd.action, cmd.speed);
  }

  // Failsafe: nếu >300ms không có gói → dừng
  if (millis() - lastPacketMs > 300) {
    stopNow();
  }

  // Ramp mượt mỗi vòng lặp
  smoothDriveStep();

  delay(10); // tăng nhịp mượt mà (100 Hz ramp)
}

Ghi chú nhanh

  • TX (C3): giữ nguyên nút F/B/L/R, thêm nút MODE (Kid/Sport), Dead Zone cho joystick, gửi ~50Hz.
  • RX (DevKit): Soft Start/Stop (ramp), cân bằng bánh, failsafe 300ms, định hướng bánh theo lệnh.
  • Nguồn & nhiễu: nhớ tụ 10µF + 100nF sát NRF24L01; mass chung; đường SPI ngắn.

? 7. Kiểm thử sau khi tối ưu

  1. Quan sát xe khi khởi động và dừng – có mượt không.
  2. Kiểm tra joystick trung tâm – xe có đứng yên ổn định.
  3. Chuyển chế độ Kid/Sport – tốc độ có thay đổi đúng mong muốn.
  4. Chạy thẳng 3–5m – kiểm tra lệch hướng.

? 8. Kết quả mong đợi

Sau khi áp dụng các kỹ thuật tối ưu:

  • Xe khởi động và dừng êm ái, không giật.
  • Điều khiển phản hồi nhanh và chính xác.
  • Tốc độ được điều chỉnh mượt theo joystick.
  • Hệ thống hoạt động ổn định, dễ lái, thân thiện với người dùng.

? 9. Bước tiếp theo

Trong (phần 5): Báo hiệu thông minh, chúng ta sẽ học cách thêm LED và Buzzer để robot phản hồi bằng ánh sáng và âm thanh – giúp người điều khiển nhận biết trạng thái robot trong thời gian thực.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *