IoTLabs

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

Series 37 Module Cảm Biến – Nguyên Lý Joystick KY-023: Biến Trở 2 Trục, Dead Zone & Điều Khiển Robot / Servo

Joystick KY-023 về cơ bản là 2 biến trở vuông góc cộng với 1 nút nhấn. Không có IC xử lý bên trong — tất cả chỉ là phần cơ học chia điện áp. Bài này giải thích rõ cơ chế, vấn đề drift tại center position, và cách normalize output thành tọa độ -1.0 đến +1.0 dùng được ngay.

Nguyên Lý Hoạt Động

1. Biến Trở Potentiometer — Chia Điện Áp

Mỗi trục (X và Y) có một biến trở 10kΩ dạng wiper (con trượt):

Biến trở trục X (10kΩ):

VCC ───[===|===]─── GND
           │
          VRx (wiper)

Khi cần sang TRÁI:  wiper về GND → VRx ≈ 0V
Khi ở GIỮA:        wiper giữa → VRx ≈ VCC/2
Khi cần sang PHẢI: wiper về VCC → VRx ≈ VCC

Điện áp đầu ra:

VRx = VCC × (R_wiper_to_GND / R_total)
    = VCC × (position / 10kΩ)

Trục Y hoạt động tương tự nhưng vuông góc: lên = gần 0V hoặc VCC tùy hướng lắp.

2. Cấu Trúc Cơ Học 2 Trục

Joystick — Nhìn từ trên:

          ↑ Y+
          │
    ┌─────┼─────┐
X- ←──    ●    ──→ X+    ← Tay cầm (stick) ở trung tâm
    └─────┼─────┘
          │
          ↓ Y-

Cấu tạo:
- Trục X: biến trở nằm ngang, wiper gắn với stick
- Trục Y: biến trở nằm dọc, wiper gắn với stick
- Button: nút nhấn ở dưới — ấn stick xuống để kích

3. Push Button — Active LOW

Button SW (Z-axis) là switch cơ học thông thường:

  • Module có pull-up resistor kéo SW lên VCC
  • SW = HIGH bình thường (không nhấn)
  • SW = LOW khi nhấn (active LOW)

Với MCU: cấu hình INPUT_PULLUP để bật pull-up nội bộ là đủ (module có thể đã có pull-up).

4. Vấn Đề Center Drift

Cơ học không hoàn hảo: trung tâm lý thuyết là VCC/2 nhưng thực tế dao động:

  • Arduino 10-bit: center ≈ 500-524 (không phải chính xác 512)
  • ESP32 12-bit: center ≈ 1900-2100 (không phải chính xác 2048)

Dead zone giải quyết vấn đề này: Vùng ±X quanh center được coi là “0” — không di chuyển. Chỉ khi vượt ra ngoài dead zone mới coi là có input.

Dead zone ±5% (Arduino 10-bit):

0     487    537   1023
├──────┤│ 0  │├──────┤
        ← dead →
         zone

Thông Số Kỹ Thuật

Thông sốGiá trị
Điện áp hoạt động3.3V – 5V
Biến trở10kΩ × 2 (X và Y)
Center position (5V)~2.5V (≈512 với 10-bit ADC)
Center position (3.3V)~1.65V (≈2048 với 12-bit ADC)
Loại buttonTact switch, active LOW
Số chân5 (GND, VCC, VRx, VRy, SW)

Sơ Đồ Chân (Pinout)

KY-023 — Nhìn từ trên mạch:

┌──────────────────────────────┐
│  [Stick lớn]                 │
│                              │
└──────────────────────────────┘
  GND  VCC  VRx  VRy  SW
ChânKý hiệuMô tả
GNDMass
VCC+Nguồn 3.3V-5V
VRxXAnalog out trục X
VRyYAnalog out trục Y
SWButtonPush button (LOW khi nhấn)

Kết Nối Phần Cứng

KY-023 với ESP32 DevKit V1

ESP32 DevKit V1           KY-023 Module
─────────────────────     ─────────────────
3V3  ─────────────────→  VCC
GND  ─────────────────→  GND
GPIO34 (ADC Input)────→  VRx   ← Input-only, ADC1 Ch6
GPIO35 (ADC Input)────→  VRy   ← Input-only, ADC1 Ch7
GPIO4  (Input_PU) ────→  SW    ← Digital input với pull-up

GPIO34 và GPIO35: Input-only pins, thuộc ADC1 — không bị ảnh hưởng khi WiFi bật.

KY-023 với Arduino Uno

Arduino Uno               KY-023 Module
─────────────────────     ─────────────────
5V   ─────────────────→  VCC
GND  ─────────────────→  GND
A0 (Analog Input) ────→  VRx
A1 (Analog Input) ────→  VRy
Pin 2 (Input_PU)  ────→  SW

Code Arduino IDE

Code Đọc Joystick Cơ Bản — Arduino Uno

/*
 * Joystick KY-023 — Đọc X, Y, Button cơ bản
 * Board: Arduino Uno
 * Kết nối: VCC→5V, GND→GND, VRx→A0, VRy→A1, SW→Pin2
 */

const int VRx_PIN = A0;   // Trục X
const int VRy_PIN = A1;   // Trục Y
const int SW_PIN  = 2;    // Nút nhấn

void setup() {
  Serial.begin(9600);
  pinMode(SW_PIN, INPUT_PULLUP); // Pull-up nội bộ — LOW khi nhấn
  Serial.println("=== Joystick KY-023 ===");
  Serial.println("X (0-1023) | Y (0-1023) | Button");
  Serial.println("-----------|-----------|-------");
}

void loop() {
  int xVal   = analogRead(VRx_PIN); // 0-1023
  int yVal   = analogRead(VRy_PIN); // 0-1023
  int swVal  = digitalRead(SW_PIN); // HIGH hoặc LOW

  // SW active LOW: LOW = đang nhấn
  String btnStr = (swVal == LOW) ? "NHẤN" : "Thả";

  Serial.print(xVal);
  Serial.print("          | ");
  Serial.print(yVal);
  Serial.print("          | ");
  Serial.println(btnStr);

  delay(200);
}

Code Joystick Normalize -1.0 đến +1.0 (Arduino Uno)

/*
 * Joystick KY-023 — Normalize output -1.0 đến +1.0 với dead zone
 * Board: Arduino Uno
 * Kết nối: VCC→5V, GND→GND, VRx→A0, VRy→A1, SW→Pin2
 *
 * Lợi ích normalize:
 * - -1.0 = hết sang trái/xuống
 * -  0.0 = trung tâm (trong dead zone)
 * - +1.0 = hết sang phải/lên
 * → Dùng trực tiếp để điều khiển tốc độ motor, góc servo
 */

const int VRx_PIN = A0;
const int VRy_PIN = A1;
const int SW_PIN  = 2;

// Center và dead zone (hiệu chỉnh theo joystick thực tế)
const int CENTER_X    = 512;  // Đọc giá trị thực khi center
const int CENTER_Y    = 512;
const int DEAD_ZONE   = 50;   // ±50 quanh center = vùng "0"
const int MAX_VAL     = 1023; // Arduino 10-bit

// Normalize giá trị joystick về -1.0 đến +1.0
float normalizeAxis(int raw, int center, int deadZone, int maxVal) {
  int delta = raw - center;

  // Trong dead zone → trả về 0
  if (abs(delta) <= deadZone) return 0.0f;

  // Ngoài dead zone → normalize
  if (delta > 0) {
    // Phạm vi: (center+deadZone) đến maxVal → 0.0 đến 1.0
    return (float)(delta - deadZone) / (maxVal - center - deadZone);
  } else {
    // Phạm vi: 0 đến (center-deadZone) → -1.0 đến 0.0
    return (float)(delta + deadZone) / (center - deadZone);
  }
}

void setup() {
  Serial.begin(9600);
  pinMode(SW_PIN, INPUT_PULLUP);
  Serial.println("=== Joystick Normalized ===");
}

void loop() {
  int rawX = analogRead(VRx_PIN);
  int rawY = analogRead(VRy_PIN);
  int swVal = digitalRead(SW_PIN);

  // Normalize về -1.0 đến +1.0
  float normX = normalizeAxis(rawX, CENTER_X, DEAD_ZONE, MAX_VAL);
  float normY = normalizeAxis(rawY, CENTER_Y, DEAD_ZONE, MAX_VAL);
  bool btnPressed = (swVal == LOW);

  // Xác định hướng dựa trên normalized values
  String direction = "CENTER";
  if (normX > 0.3)       direction = "PHẢI";
  else if (normX < -0.3) direction = "TRÁI";
  if (normY > 0.3)       direction = "LÊN";
  else if (normY < -0.3) direction = "XUỐNG";
  if (btnPressed)        direction = "NHẤN";

  Serial.print("X="); Serial.print(normX, 2);
  Serial.print(" Y="); Serial.print(normY, 2);
  Serial.print(" → "); Serial.println(direction);

  delay(100);
}

Code Điều Khiển 2 Servo Bằng Joystick — Arduino Uno

/*
 * Joystick KY-023 — Điều khiển 2 servo: X→Servo1, Y→Servo2
 * Board: Arduino Uno
 * Kết nối:
 *   VCC→5V, GND→GND, VRx→A0, VRy→A1, SW→Pin2
 *   Servo1 signal→Pin9, Servo2 signal→Pin10
 *   Servo VCC → nguồn 5V độc lập (không lấy từ Arduino!)
 *
 * Lưu ý: dùng nguồn ngoài cho servo — servo cần 200-500mA
 */

#include <Servo.h>

const int VRx_PIN    = A0;
const int VRy_PIN    = A1;
const int SW_PIN     = 2;
const int SERVO1_PIN = 9;   // Servo trục X
const int SERVO2_PIN = 10;  // Servo trục Y

Servo servo1;
Servo servo2;

int servoAngle1 = 90; // Góc hiện tại servo 1 (0-180)
int servoAngle2 = 90; // Góc hiện tại servo 2

void setup() {
  Serial.begin(9600);
  pinMode(SW_PIN, INPUT_PULLUP);
  servo1.attach(SERVO1_PIN);
  servo2.attach(SERVO2_PIN);
  // Reset về giữa
  servo1.write(servoAngle1);
  servo2.write(servoAngle2);
  Serial.println("Joystick Servo Control — Nhấn để reset về 90°");
}

void loop() {
  int rawX = analogRead(VRx_PIN); // 0-1023
  int rawY = analogRead(VRy_PIN); // 0-1023
  bool btnPressed = (digitalRead(SW_PIN) == LOW);

  // Nhấn button → reset về 90°
  if (btnPressed) {
    servoAngle1 = 90;
    servoAngle2 = 90;
    Serial.println("Reset về 90°");
  } else {
    // Map joystick 0-1023 → góc servo 0-180°
    servoAngle1 = map(rawX, 0, 1023, 0, 180);
    servoAngle2 = map(rawY, 0, 1023, 0, 180);
  }

  // Constrain đảm bảo không vượt 0-180°
  servoAngle1 = constrain(servoAngle1, 0, 180);
  servoAngle2 = constrain(servoAngle2, 0, 180);

  servo1.write(servoAngle1);
  servo2.write(servoAngle2);

  Serial.print("Servo1="); Serial.print(servoAngle1);
  Serial.print("° Servo2="); Serial.print(servoAngle2); Serial.println("°");

  delay(50); // Refresh 20Hz — đủ mượt cho servo
}

Code ESP32 — Joystick Điều Khiển Robot 2 Bánh

/*
 * Joystick KY-023 — ESP32, điều khiển robot 2 bánh qua L298N
 * Board: ESP32 DevKit V1
 * Kết nối joystick: VCC→3V3, GND→GND, VRx→GPIO34, VRy→GPIO35, SW→GPIO4
 * Kết nối L298N:
 *   ENA→GPIO18 (PWM), IN1→GPIO19, IN2→GPIO21
 *   ENB→GPIO22 (PWM), IN3→GPIO23, IN4→GPIO25
 */

// Joystick
const int VRx_PIN = 34;
const int VRy_PIN = 35;
const int SW_PIN  = 4;

// L298N Motor Driver
const int ENA_PIN = 18; // PWM trái
const int IN1_PIN = 19;
const int IN2_PIN = 21;
const int ENB_PIN = 22; // PWM phải
const int IN3_PIN = 23;
const int IN4_PIN = 25;

// LEDC cho PWM
const int LEDC_CH_A = 0;
const int LEDC_CH_B = 1;
const int LEDC_FREQ = 1000; // 1kHz
const int LEDC_BITS = 8;    // 0-255

const int CENTER    = 2048; // ESP32 12-bit center
const int DEAD_ZONE = 200;  // ±200 quanh center
const int MAX_RAW   = 4095;

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

  // Motor pins
  pinMode(IN1_PIN, OUTPUT); pinMode(IN2_PIN, OUTPUT);
  pinMode(IN3_PIN, OUTPUT); pinMode(IN4_PIN, OUTPUT);

  // PWM setup
  ledcSetup(LEDC_CH_A, LEDC_FREQ, LEDC_BITS);
  ledcAttachPin(ENA_PIN, LEDC_CH_A);
  ledcSetup(LEDC_CH_B, LEDC_FREQ, LEDC_BITS);
  ledcAttachPin(ENB_PIN, LEDC_CH_B);

  pinMode(SW_PIN, INPUT_PULLUP);
  Serial.println("=== Robot Joystick Control ===");
}

// Điều khiển motor trái: speed -255 đến +255
void setLeftMotor(int speed) {
  if (speed > 0) {
    digitalWrite(IN1_PIN, HIGH);
    digitalWrite(IN2_PIN, LOW);
    ledcWrite(LEDC_CH_A, speed);
  } else if (speed < 0) {
    digitalWrite(IN1_PIN, LOW);
    digitalWrite(IN2_PIN, HIGH);
    ledcWrite(LEDC_CH_A, -speed);
  } else {
    digitalWrite(IN1_PIN, LOW);
    digitalWrite(IN2_PIN, LOW);
    ledcWrite(LEDC_CH_A, 0);
  }
}

// Điều khiển motor phải: speed -255 đến +255
void setRightMotor(int speed) {
  if (speed > 0) {
    digitalWrite(IN3_PIN, HIGH);
    digitalWrite(IN4_PIN, LOW);
    ledcWrite(LEDC_CH_B, speed);
  } else if (speed < 0) {
    digitalWrite(IN3_PIN, LOW);
    digitalWrite(IN4_PIN, HIGH);
    ledcWrite(LEDC_CH_B, -speed);
  } else {
    digitalWrite(IN3_PIN, LOW);
    digitalWrite(IN4_PIN, LOW);
    ledcWrite(LEDC_CH_B, 0);
  }
}

// Normalize raw ADC về -255 đến +255 với dead zone
int normalizeToMotor(int raw, int center, int deadZone, int maxRaw) {
  int delta = raw - center;
  if (abs(delta) <= deadZone) return 0;
  if (delta > 0) return map(delta, deadZone, maxRaw - center, 0, 255);
  return map(delta, -(center), -deadZone, -255, 0);
}

void loop() {
  int rawX = analogRead(VRx_PIN); // Trái/Phải
  int rawY = analogRead(VRy_PIN); // Tiến/Lùi
  bool btnStop = (digitalRead(SW_PIN) == LOW);

  if (btnStop) {
    // Nhấn nút → dừng hết
    setLeftMotor(0);
    setRightMotor(0);
    Serial.println("DỪNG");
  } else {
    int forward = normalizeToMotor(rawY, CENTER, DEAD_ZONE, MAX_RAW); // Y → tiến/lùi
    int turn    = normalizeToMotor(rawX, CENTER, DEAD_ZONE, MAX_RAW); // X → rẽ

    // Differential drive: trái = tiến - rẽ, phải = tiến + rẽ
    int leftSpeed  = constrain(forward - turn, -255, 255);
    int rightSpeed = constrain(forward + turn, -255, 255);

    setLeftMotor(leftSpeed);
    setRightMotor(rightSpeed);

    Serial.printf("Fwd=%d Turn=%d | L=%d R=%d\n", forward, turn, leftSpeed, rightSpeed);
  }

  delay(50);
}

Ứng Dụng Thực Tế

Ứng dụngChi tiết
Điều khiển robot xeDifferential drive: X=rẽ, Y=tiến/lùi
Điều khiển servo camera pan-tilt2 servo cho trục X và Y
Game controllerGửi tọa độ qua WiFi/Bluetooth
Cần điều khiển crane/cánh tay robotÁnh xạ từng trục theo cơ cấu

Lưu Ý Khi Sử Dụng

1. Hiệu chỉnh CENTER thực tế

Đọc giá trị X và Y khi không chạm joystick. Ghi lại con số thực và dùng làm CENTERX, CENTERY trong code — không cứng nhắc dùng 512 hoặc 2048.

2. ESP32 ADC độ phi tuyến

Tương tự sound sensor — ESP32 ADC phi tuyến ở dải cực trị. Với joystick không cần chính xác cao (điều khiển motor) → OK. Nếu cần tọa độ chính xác → hiệu chỉnh ADC.

3. Nguồn ngoài cho servo/motor

Servo SG90 cần 200-500mA, động cơ DC cần 500mA-2A. Không lấy từ 3V3 hoặc 5V Arduino — hãy dùng nguồn ngoài (pin 2S LiPo 7.4V qua L298N, hoặc adapter 5V 2A riêng).

4. Rung và nhiễu ADC

Joystick cơ học rung tay → ADC dao động liên tục. Giải pháp: moving average 5-10 mẫu, hoặc tăng dead zone.

Bài tiếp theo: