Series
Series 37 Module Cảm Biến
  1. Series 37 Module Cảm Biến: Nguyên lý hoạt động và cách test với Arduino, ESP32
  2. 1Series 37 Module Cảm Biến – DS3231 RTC: TCXO ±2ppm, I2C, Alarm Ngắt SQW & Đồng Hồ Thực Với Backup Pin
  3. 2Series 37 Module Cảm Biến – SD Card Module SPI: FAT32, Ghi Log CSV Cảm Biến & Đọc Config File Không Hardcode
  4. 3Series 37 Module Cảm Biến – LCD 1602 I2C: PCF8574 Expander, LiquidCrystal_I2C, Cuộn Chữ & Ký Tự Tùy Chỉnh
  5. 4Series 37 Module Cảm Biến – 7-Segment 1 Chữ Số: Mã Segment 0x3F-0x7F, Common Anode vs Cathode & Đếm 0-9
  6. 5Series 37 Module Cảm Biến – OLED SSD1306 0.96\” I2C: Frame Buffer, Adafruit GFX & Hiển Thị Dữ Liệu Thực Tế
  7. 6Series 37 Module Cảm Biến – Nguyên Lý LM35 Analog: Đặc Tuyến 10mV/°C, Đọc ADC Chính Xác & Hiệu Chỉnh Nhiệt Độ
  8. 7Series 37 Module Cảm Biến – Nguyên Lý Knock Sensor KY-031: Piezoelectric Thuận, Va Đập → Điện Áp & Nhận Dạng Pattern
  9. 8Series 37 Module Cảm Biến – Nguyên Lý MPU-6050: IMU 6 Trục, MEMS Gia Tốc + Con Quay, Complementary Filter & Góc Nghiêng
  10. 9Series 37 Module Cảm Biến – Nguyên Lý Relay 5V SRD-05VDC: Cơ Điện Từ, Cách Ly Galvanic & Điều Khiển Thiết Bị 220V
  11. 10Series 37 Module Cảm Biến – Nguyên Lý Hall Effect Analog (49E/SS49E): Đo Cường Độ Từ Trường Tuyến Tính & ADC
  12. 11Series 37 Module Cảm Biến – Nguyên Lý Hall Effect KY-003: Hiệu Ứng Hall, IC Số Unipolar & Phát Hiện Nam Châm
  13. 12Series 37 Module Cảm Biến – Nguyên Lý Reed Switch: Tiếp Điểm Từ Trong Kính, Phát Hiện Cửa & Đo Tốc Độ Quay
  14. 13Series 37 Module Cảm Biến – Nguyên Lý Vibration Switch SW-18010P: Lò Xo Dẫn Điện, Phát Hiện Rung & Pattern Gõ
  15. 14Series 37 Module Cảm Biến – Nguyên Lý Tilt Switch SW-520D: Bi Kim Loại Dẫn Điện, Phát Hiện Nghiêng & Báo Động
  16. 15Series 37 Module Cảm Biến – Nguyên Lý Touch Sensor TTP223: Capacitive Self-Capacitance, Chế Độ Toggle & Latch
  17. 16Series 37 Module Cảm Biến – Nguyên Lý Rotary Encoder KY-040: Xung Quadrature, Đếm Hướng Xoay & Điều Khiển Menu
  18. 17Series 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
  19. 18Series 37 Module Cảm Biến – Nguyên Lý Sound Sensor: Microphone Electret, LM393 Comparator & Phát Hiện Tiếng Ồn
  20. 19Series 37 Module Cảm Biến – Nguyên Lý Passive Buzzer: PWM Điều Khiển Tần Số, Hàm tone() & Phát Nhạc Arduino ESP32
  21. 20Series 37 Module Cảm Biến – Nguyên Lý Active Buzzer: Oscillator Tích Hợp, Phân Biệt Active/Passive & Code Cảnh Báo
  22. 21Series 37 Module Cảm Biến – Nguyên Lý 7-Color LED Tự Đổi Màu: IC Tích Hợp, Tần Số Dao Động Nội & Ứng Dụng Trang Trí
  23. 22Series 37 Module Cảm Biến – Nguyên Lý RGB LED Module KY-016: Điốt 3 Màu, PWM Mix Màu 16 Triệu Sắc & Hiệu Ứng
  24. 23Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động IR Receiver VS1838B: Demodulate 38kHz, NEC Protocol & Điều Khiển Từ Xa
  25. 24Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động LDR Quang Trở: CdS Photoconductor, Voltage Divider & Điều Khiển Theo Ánh Sáng
  26. 25Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động Laser Module KY-008: Diode Laser 650nm, Cộng Hưởng Quang Học & An Toàn Laser
  27. 26Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động Flame Sensor: Phổ IR Lửa 760-1100nm, LM393 & Ứng Dụng Chữa Cháy
  28. 27Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động Line Tracking Sensor TCRT5000: Dò Đường IR, Khoảng Cách Tối Ưu & Robot Theo Vạch
  29. 28Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động IR Obstacle Sensor: Phản Xạ Hồng Ngoại, LM393 & Tránh Vật Cản Robot
  30. 29Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động PIR HC-SR501: Pyroelectric Infrared, Fresnel Lens & Phát Hiện Chuyển Động
  31. 30Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động HC-SR04: Sóng Siêu Âm 40kHz, Thời Gian Bay & Đo Khoảng Cách Chính Xác
  32. 31Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động MQ-2: Semiconductor SnO₂, Đường Cong Nhạy Khí & Đo Nồng Độ Gas
  33. 32Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động Rain Sensor FC-37: Phát Hiện Mưa, LM393 Comparator & Ứng Dụng Thực Tế
  34. 33Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động Soil Moisture Sensor: Điện Trở Đất, Hiệu Chỉnh & Tưới Cây Tự Động
  35. 34Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động BMP280: Cảm Biến Áp Suất & Nhiệt Độ Chính Xác Của Bosch
  36. 35Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động DS18B20: Cảm Biến Nhiệt Độ 1-Wire Chống Nước
  37. 36Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động DHT22 (AM2302): Độ Chính Xác Cao, Dải Đo Rộng
  38. 37Series 37 Module Cảm Biến – Nguyên Lý Hoạt Động DHT11: Cảm Biến Nhiệt Độ & Độ Ẩm Phổ Biến Nhất
Cảm biến

Series 37 Module Cảm Biến – Nguyên Lý Rotary Encoder KY-040: Xung Quadrature, Đếm Hướng Xoay & Điều Khiển Menu

Th7 2026
Series 37 Module Cảm Biến – Nguyên Lý Rotary Encoder KY-040: Xung Quadrature, Đếm Hướng Xoay & Điều Khiển Menu

Rotary encoder trông như biến trở xoay nhưng về nguyên lý hoàn toàn khác: không có điện trở, không có góc giới hạn, và quan trọng nhất — nó biết bạn xoay theo chiều nào. Bí quyết là 2 tín hiệu lệch pha nhau 90° (quadrature). Bài này giải thích từng bit và xây dựng code interrupt không mất nhịp.

Nguyên Lý Hoạt Động

1. Cơ Chế Encoder Cơ Học

KY-040 là encoder cơ học (mechanical incremental encoder):

Đĩa tiếp điểm bên trong:

Đĩa xoay  20 răng dẫn điện:
    ┌─────────────────────────────┐
      ████░░████░░████░░████░░     Răng dẫn (nối GND)
      ░░░░██░░░░██░░░░██░░░░██   
    └─────────────────────────────┘
                    
         A (CLK)     B (DT)    2 tiếp điểm lệch nhau 1/4 bước

Khi đĩa xoay:
- Mỗi răng đi qua tiếp điểm A: CLK tạo 1 xung LOW
- Mỗi răng đi qua tiếp điểm B: DT tạo 1 xung LOW (lệch 90°)

Pull-up resistor: Tiếp điểm A và B thường ở trạng thái HIGH (module có pull-up). Khi răng dẫn điện chạm → kéo xuống LOW.

2. Quadrature Encoding — 2 Kênh Lệch Pha 90°

Bí mật xác định hướng xoay:

Chiều Kim Đồng Hồ (Clockwise — CW):

CLK ──┐    ┌────┐    ┌────
      └────┘    └────┘
DT  ────┐    ┌────┐    ┌──
        └────┘    └────┘
      ← CLK trước, DT sau 90°

Ngược Kim Đồng Hồ (Counter-Clockwise — CCW):

CLK ──┐    ┌────┐    ┌────
      └────┘    └────┘
DT  ──┐    ┌────┐    ┌────
      └────┘    └────┘
      ← DT trước, CLK sau 90°

Quy tắc đọc đơn giản cho KY-040:

  • Quan sát CLK khi nó chuyển từ HIGH → LOW (falling edge)
  • Lúc đó đọc DT: – DT = HIGH → đang xoay ClockwiseDT = LOW → đang xoay Counter-Clockwise

Điều này không phải quadrature hoàn hảo về mặt kỹ thuật, nhưng đây là cách hoạt động thực tế của KY-040 với pull-up resistors.

3. Vấn Đề Bounce (Rung Tiếp Điểm)

Cơ học → tiếp điểm nảy → mỗi click có thể tạo ra 5-10 xung thay vì 1. Giải pháp:

Phương phápƯu điểmNhược điểm
Phần cứng: tụ 100nF song songĐơn giản, không cần codeLàm chậm signal
Phần mềm: debounce delayKhông cần linh kiệnCó thể mất nhịp nhanh
State machine đầy đủChính xác nhấtCode phức tạp hơn

KY-040 thường đã có tụ trên module — debounce đơn giản trong code là đủ.

4. Push Button SW

Nút nhấn ở trung tâm encoder:

  • SW = HIGH bình thường (pull-up)
  • SW = LOW khi nhấn (active LOW)
  • Dùng để xác nhận lựa chọn trong menu, reset counter, v.v.

Thông Số Kỹ Thuật

Thông sốGiá trị
Điện áp hoạt động3.3V – 5V
Số detent (click/vòng)20
Xung per detent2 xung (CLK + DT)
LoạiIncremental (tương đối, không tuyệt đối)
Tuổi thọ~15,000 cycles
Số chân5 (GND, VCC, SW, DT, CLK)

Sơ Đồ Chân (Pinout)

KY-040 — Nhìn từ mặt trước:

┌──────────────────────────────────┐
│        [Núm xoay lớn]            │
│                                  │
└──────────────────────────────────┘
  GND  VCC   SW   DT   CLK
ChânKý hiệuMô tả
GNDMass
VCC+Nguồn 3.3V-5V
SWButtonPush button (LOW khi nhấn)
DTBKênh B — xác định hướng
CLKAKênh A — tạo xung khi xoay

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

KY-040 với ESP32 DevKit V1

ESP32 DevKit V1           KY-040 Module
─────────────────────     ─────────────────
3V3  ─────────────────→  VCC
GND  ─────────────────→  GND
GPIO18 (Input) ────────→  CLK   ← Interrupt trên falling edge
GPIO19 (Input) ────────→  DT    ← Đọc khi CLK falling
GPIO5  (Input) ────────→  SW    ← Pull-up nội bộ

GPIO18 và GPIO19: interrupt-capable, không phải strapping pin.

KY-040 với Arduino Uno

Arduino Uno               KY-040 Module
─────────────────────     ─────────────────
5V   ─────────────────→  VCC
GND  ─────────────────→  GND
Pin 2 (INT0) ─────────→  CLK   ← Interrupt 0 (falling edge)
Pin 3 (INT1) ─────────→  DT    ← Interrupt 1 hoặc digitalRead
Pin 4 (Input)─────────→  SW    ← INPUT_PULLUP

Pin 2 và 3 trên Arduino Uno là 2 interrupt pins (INT0 và INT1).

Code Arduino IDE

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

/*
 * Rotary Encoder KY-040 — Đọc hướng xoay cơ bản
 * Board: Arduino Uno
 * Kết nối: VCC→5V, GND→GND, CLK→Pin2, DT→Pin3, SW→Pin4
 *
 * Nguyên tắc: khi CLK falling, đọc DT để biết hướng
 *   DT = HIGH → Clockwise (CW)
 *   DT = LOW  → Counter-Clockwise (CCW)
 */

const int CLK_PIN = 2;  // INT0 — CLK
const int DT_PIN  = 3;  // DT — hướng
const int SW_PIN  = 4;  // Button

int counter = 0;         // Vị trí hiện tại (có thể âm)
int lastCLK  = HIGH;     // Trạng thái CLK trước đó

void setup() {
  Serial.begin(9600);
  pinMode(CLK_PIN, INPUT_PULLUP); // Module đã có pull-up nhưng thêm cho chắc
  pinMode(DT_PIN,  INPUT_PULLUP);
  pinMode(SW_PIN,  INPUT_PULLUP);

  lastCLK = digitalRead(CLK_PIN);
  Serial.println("=== Rotary Encoder KY-040 ===");
  Serial.println("Counter = 0");
}

void loop() {
  int currentCLK = digitalRead(CLK_PIN);

  // Phát hiện falling edge trên CLK (HIGH → LOW)
  if (currentCLK == LOW && lastCLK == HIGH) {
    // Đọc DT để xác định hướng
    int dtVal = digitalRead(DT_PIN);

    if (dtVal == HIGH) {
      counter++; // Clockwise
      Serial.print("CW  → Counter = ");
    } else {
      counter--; // Counter-Clockwise
      Serial.print("CCW → Counter = ");
    }
    Serial.println(counter);
  }

  lastCLK = currentCLK;

  // Kiểm tra button nhấn
  if (digitalRead(SW_PIN) == LOW) {
    counter = 0;
    Serial.println("RESET → Counter = 0");
    delay(300); // Debounce button
  }
}

Code Interrupt-Based — Arduino Uno (Không Mất Nhịp)

/*
 * Rotary Encoder KY-040 — Interrupt-based, không bỏ sót xung
 * Board: Arduino Uno
 * Kết nối: VCC→5V, GND→GND, CLK→Pin2, DT→Pin3, SW→Pin4
 *
 * ISR trên falling edge của CLK → xử lý ngay lập tức
 * Dùng volatile cho biến chia sẻ với ISR
 */

const int CLK_PIN = 2;
const int DT_PIN  = 3;
const int SW_PIN  = 4;

volatile int counter = 0;     // Biến volatile: chia sẻ ISR và main loop
volatile bool changed = false; // Cờ báo có thay đổi

// ISR — gọi khi CLK falling edge (HIGH → LOW)
void onCLKFalling() {
  // Đọc DT ngay lập tức (không delay!)
  if (digitalRead(DT_PIN) == HIGH) {
    counter++; // Clockwise
  } else {
    counter--; // Counter-Clockwise
  }
  changed = true;
}

void setup() {
  Serial.begin(9600);
  pinMode(CLK_PIN, INPUT_PULLUP);
  pinMode(DT_PIN,  INPUT_PULLUP);
  pinMode(SW_PIN,  INPUT_PULLUP);

  // Attach interrupt trên CLK: trigger khi CLK falling (HIGH→LOW)
  attachInterrupt(digitalPinToInterrupt(CLK_PIN), onCLKFalling, FALLING);

  Serial.println("=== Encoder Interrupt Mode ===");
}

unsigned long lastBtnTime = 0;

void loop() {
  // In kết quả khi có thay đổi (xử lý ngoài ISR)
  if (changed) {
    changed = false;
    Serial.print("Counter = ");
    Serial.println(counter);
  }

  // Button với debounce 200ms
  if (digitalRead(SW_PIN) == LOW) {
    if (millis() - lastBtnTime > 200) {
      lastBtnTime = millis();
      counter = 0;
      Serial.println("RESET → 0");
    }
  }
}

Code Điều Chỉnh Giá Trị Trong Phạm Vi — Arduino Uno

/*
 * Rotary Encoder — Điều chỉnh giá trị trong phạm vi min-max
 * Ứng dụng: âm lượng 0-100, nhiệt độ 16-30°C, tốc độ PWM 0-255
 * Board: Arduino Uno
 */

const int CLK_PIN = 2;
const int DT_PIN  = 3;
const int SW_PIN  = 4;
const int LED_PIN = 9; // LED để demo điều chỉnh độ sáng

// Phạm vi giá trị
const int VALUE_MIN = 0;
const int VALUE_MAX = 255;
const int VALUE_STEP = 5; // Mỗi click thay đổi 5 đơn vị

volatile int targetValue = 128; // Giá trị mặc định

void onCLKFalling() {
  if (digitalRead(DT_PIN) == HIGH) {
    targetValue = min(targetValue + VALUE_STEP, VALUE_MAX);
  } else {
    targetValue = max(targetValue - VALUE_STEP, VALUE_MIN);
  }
}

void setup() {
  Serial.begin(9600);
  pinMode(CLK_PIN, INPUT_PULLUP);
  pinMode(DT_PIN,  INPUT_PULLUP);
  pinMode(SW_PIN,  INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(CLK_PIN), onCLKFalling, FALLING);
  Serial.println("Xoay để điều chỉnh độ sáng LED");
}

int lastPrintedValue = -1;

void loop() {
  // Áp dụng giá trị vào LED
  analogWrite(LED_PIN, targetValue);

  // In khi có thay đổi
  if (targetValue != lastPrintedValue) {
    lastPrintedValue = targetValue;
    int percent = map(targetValue, 0, 255, 0, 100);
    Serial.print("Độ sáng: ");
    Serial.print(percent);
    Serial.println("%");
  }

  // Nhấn button → reset về 50%
  if (digitalRead(SW_PIN) == LOW) {
    targetValue = 128;
    delay(300);
  }
}

Code ESP32 — Encoder Điều Hướng Menu

/*
 * Rotary Encoder KY-040 — ESP32, điều hướng menu đơn giản
 * Board: ESP32 DevKit V1
 * Kết nối: VCC→3V3, GND→GND, CLK→GPIO18, DT→GPIO19, SW→GPIO5
 */

const int CLK_PIN = 18;
const int DT_PIN  = 19;
const int SW_PIN  = 5;

// Menu items
const char* menuItems[] = {
  "1. Đọc nhiệt độ",
  "2. Điều khiển LED",
  "3. WiFi settings",
  "4. Thông tin hệ thống"
};
const int MENU_SIZE = 4;

volatile int menuIndex  = 0;  // Vị trí menu hiện tại
volatile bool menuChanged = false;
volatile bool btnPressed  = false;

void IRAM_ATTR onCLKISR() {
  if (digitalRead(DT_PIN) == HIGH) {
    menuIndex = (menuIndex + 1) % MENU_SIZE; // Tiến — vòng lại đầu
  } else {
    menuIndex = (menuIndex - 1 + MENU_SIZE) % MENU_SIZE; // Lùi — vòng lại cuối
  }
  menuChanged = true;
}

void IRAM_ATTR onSWISR() {
  btnPressed = true;
}

void printMenu() {
  Serial.println("\n=== MENU ===");
  for (int i = 0; i < MENU_SIZE; i++) {
    if (i == menuIndex) {
      Serial.print("► "); // Con trỏ
    } else {
      Serial.print("  ");
    }
    Serial.println(menuItems[i]);
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(CLK_PIN, INPUT_PULLUP);
  pinMode(DT_PIN,  INPUT_PULLUP);
  pinMode(SW_PIN,  INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(CLK_PIN), onCLKISR, FALLING);
  attachInterrupt(digitalPinToInterrupt(SW_PIN),  onSWISR,  FALLING);

  printMenu(); // Hiển thị menu ban đầu
}

unsigned long lastBtnTime = 0;

void loop() {
  // Cập nhật hiển thị khi menu thay đổi
  if (menuChanged) {
    menuChanged = false;
    printMenu();
  }

  // Xử lý button nhấn (debounce 300ms)
  if (btnPressed) {
    btnPressed = false;
    if (millis() - lastBtnTime > 300) {
      lastBtnTime = millis();
      Serial.print("\n>>> CHỌN: ");
      Serial.println(menuItems[menuIndex]);
      // Thực thi action tương ứng với menuIndex ở đây
    }
  }
}

Kết Quả Mong Đợi

=== MENU ===
► 1. Đọc nhiệt độ
  2. Điều khiển LED
  3. WiFi settings
  4. Thông tin hệ thống

(Xoay CW)
=== MENU ===
  1. Đọc nhiệt độ
► 2. Điều khiển LED
  3. WiFi settings
  4. Thông tin hệ thống

(Nhấn)
>>> CHỌN: 2. Điều khiển LED

Ứng Dụng Thực Tế

Ứng dụngChi tiết
Điều chỉnh âm lượngXoay encoder thay đổi 0-100%
Điều hướng menu OLEDKết hợp với SSD1306 (Bài 33)
Nhập số PIN/mật khẩuXoay chọn chữ số, nhấn xác nhận
Điều chỉnh nhiệt độ set pointThermostat thủ công
Điều khiển tốc độ motorXoay tăng/giảm PWM

Lưu Ý Khi Sử Dụng

1. ISR phải nhanh — không delay(), không Serial trong ISR

ISR chạy với interrupt tắt. Serial.print() trong ISR gây treo máy. Chỉ set cờ và thay đổi biến; xử lý in ấn ở loop().

2. Biến chia sẻ phải là volatile

Compiler có thể optimize biến không volatile → ISR thay đổi nhưng loop() không thấy. Luôn khai báo volatile cho biến chia sẻ giữa ISR và main code.

3. Encoder bounce gây đếm sai

Mỗi click thực sự tạo ra 2-5 edge giả do bounce. Giải pháp đơn giản:

  • Giữ nguyên FALLING trigger (chỉ CLK falling)
  • Đừng dùng CHANGE trigger (sẽ đếm double)
  • Thêm tụ 100nF từ CLK xuống GND nếu vẫn bị lỗi

4. KY-040 không đọc vị trí tuyệt đối

Incremental encoder chỉ đếm số bước từ lúc bật nguồn. Mỗi lần reset mất vị trí. Nếu cần vị trí tuyệt đối → dùng absolute encoder hoặc AS5600 (magnetic encoder với I2C).

Bài tiếp theo: