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 động | 3.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 button | Tact switch, active LOW |
| Số chân | 5 (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ân | Ký hiệu | Mô tả |
|---|---|---|
| GND | – | Mass |
| VCC | + | Nguồn 3.3V-5V |
| VRx | X | Analog out trục X |
| VRy | Y | Analog out trục Y |
| SW | Button | Push 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ụng | Chi tiết |
|---|---|
| Điều khiển robot xe | Differential drive: X=rẽ, Y=tiến/lùi |
| Điều khiển servo camera pan-tilt | 2 servo cho trục X và Y |
| Game controller | Gử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.


