ESP32 + Stepper 28BYJ-48 + ULN2003: Điều khiển động cơ bước chính xác
Trung cấp1/8/2026- Tác giả: IoTlabs Maker

ESP32 + Stepper 28BYJ-48 + ULN2003: Điều khiển động cơ bước chính xác

Điều khiển động cơ bước 28BYJ-48 qua driver ULN2003 với half-step 8 bước, quay chính xác theo góc và số bước, nền tảng cho mâm xoay, mở đóng van tự động.

esp32stepper28byj-48uln2003motor-controldieu-khien-dong-co

Động cơ bước (stepper motor) khác servo và motor DC ở chỗ nó di chuyển theo từng bước rời rạc chính xác, không cần cảm biến phản hồi vị trí (open-loop).

Kit 28BYJ-48 + ULN2003 là combo phổ biến nhất để nhập môn: driver ULN2003 (Darlington array) khuếch đại tín hiệu GPIO 3.3V đủ dòng để kéo 4 cuộn dây của động cơ.

Bài này dùng chuỗi half-step 8 bước để quay mượt và giữ momen xoắn tốt hơn full-step, hỗ trợ lệnh quay theo vòng hoặc theo góc chính xác qua Serial.

Hướng dẫn chi tiết

Điều khiển động cơ bước 28BYJ-48 qua ULN2003 với chuỗi half-step 8 bước trên ESP32, quay chính xác theo góc/số vòng, nhả cuộn dây khi đứng yên.

1. Giới thiệu

Động cơ bước (stepper motor) khác biệt với servo và DC motor ở chỗ nó di chuyển theo từng bước rời rạc có thể đếm được — cho phép định vị chính xác theo góc mà không cần cảm biến phản hồi (open-loop control), miễn là không bị trượt bước (mất đồng bộ do quá tải hoặc quay quá nhanh).

Kit 28BYJ-48 + ULN2003 là combo phổ biến nhất để nhập môn: 28BYJ-48 là động cơ bước 4 pha có hộp giảm tốc tỉ số 1:64 (64 bước động cơ × 64 = 4096 bước/vòng trục ra thực tế), còn ULN2003 là board Darlington driver khuếch đại dòng vì GPIO ESP32 (3.3V, ~20mA) không đủ dòng kéo trực tiếp cuộn dây động cơ.

Bài này dùng chuỗi half-step 8 bước (mượt và giữ momen tốt hơn full-step 4 bước) để quay theo số vòng hoặc theo góc chính xác qua lệnh Serial.

2. Linh kiện cần có

Linh kiện

Số lượng

Ghi chú

ESP32 DevKit V4

1

Board điều khiển chính

ULN2003 + 28BYJ-48 – Stepper Motor

1

Kit driver + động cơ bước 5V, 4096 bước/vòng thực

3. Sơ đồ kết nối

Chân ULN2003

Chân ESP32

Ghi chú

VCC

5V (VIN)

28BYJ-48 cần dòng ~200-300mA, không dùng 3V3

GND

GND

Bắt buộc GND chung

IN1

GPIO16

Pha A

IN2

GPIO17

Pha B

IN3

GPIO18

Pha C

IN4

GPIO19

Pha D

Cả 4 chân IN1-IN4 dùng GPIO16-19, không thuộc nhóm boot-strapping pin nên an toàn dùng làm output ngay từ lúc khởi động.

4. Code mẫu

/*
 * ESP32 + Stepper 28BYJ-48 + ULN2003 - Dieu khien dong co buoc chinh xac
 * Board: ESP32 DevKit V4 (board_esp32_devkitc)
 * Module: ULN2003 + 28BYJ-48 - Stepper Motor (mod_uln2003_stepper)
 *
 * Wiring:
 *   ULN2003 VCC -> ESP32 5V (VIN)   [28BYJ-48 can dong ~200-300mA, KHONG dung 3V3]
 *   ULN2003 GND -> ESP32 GND
 *   ULN2003 IN1 -> ESP32 GPIO16
 *   ULN2003 IN2 -> ESP32 GPIO17
 *   ULN2003 IN3 -> ESP32 GPIO18
 *   ULN2003 IN4 -> ESP32 GPIO19
 *
 * 28BYJ-48: 4096 buoc/vong thuc te (64 buoc/vong dong co x 64 ti so banh rang).
 * Dung 8-step half-step sequence (thu vien tu viet, khong can cai Stepper.h)
 * de chay muot va giu momen xoan tot hon full-step.
 *
 * An toan phan cung:
 *  - ULN2003 la Darlington driver, tich hop san diode flyback bao ve cho 4 cuon
 *    day cua 28BYJ-48 - khong can diode ngoai.
 *  - Dong dinh muc moi cuon ~ 60-80mA, ULN2003 chiu toi 500mA/kenh -> an toan du du.
 *  - Cap nguon 5V rieng cho VCC neu dung nhieu servo/dong co cung luc de tranh sut ap
 *    lam ESP32 reset (brown-out).
 */

static const int IN1_PIN = 16;
static const int IN2_PIN = 17;
static const int IN3_PIN = 18;
static const int IN4_PIN = 19;

// Half-step 8-buoc sequence chuan cho ULN2003 + 28BYJ-48
static const uint8_t HALF_STEP_SEQ[8][4] = {
  {1, 0, 0, 0},
  {1, 1, 0, 0},
  {0, 1, 0, 0},
  {0, 1, 1, 0},
  {0, 0, 1, 0},
  {0, 0, 1, 1},
  {0, 0, 0, 1},
  {1, 0, 0, 1},
};

static const int STEPS_PER_REV = 4096; // half-step, so buoc thuc te 1 vong truc ra

int stepIndex = 0;
long targetSteps = 0;   // vi tri muc tieu (so buoc tuyet doi)
long currentSteps = 0;  // vi tri hien tai
unsigned long lastStepMicros = 0;
unsigned long stepIntervalMicros = 1500; // toc do quay (nho hon = nhanh hon)

void applyStep(int idx) {
  digitalWrite(IN1_PIN, HALF_STEP_SEQ[idx][0]);
  digitalWrite(IN2_PIN, HALF_STEP_SEQ[idx][1]);
  digitalWrite(IN3_PIN, HALF_STEP_SEQ[idx][2]);
  digitalWrite(IN4_PIN, HALF_STEP_SEQ[idx][3]);
}

void releaseCoils() {
  digitalWrite(IN1_PIN, LOW);
  digitalWrite(IN2_PIN, LOW);
  digitalWrite(IN3_PIN, LOW);
  digitalWrite(IN4_PIN, LOW);
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println();
  Serial.println("=== ESP32 + Stepper 28BYJ-48/ULN2003 boot OK ===");

  pinMode(IN1_PIN, OUTPUT);
  pinMode(IN2_PIN, OUTPUT);
  pinMode(IN3_PIN, OUTPUT);
  pinMode(IN4_PIN, OUTPUT);
  releaseCoils();

  Serial.println("Lenh Serial: 'R' quay 1 vong thuan, 'L' quay 1 vong nguoc, 'A<deg>' quay den goc.");
  Serial.println("{\"status\":\"ready\",\"steps_per_rev\":4096}");
}

void handleSerialCommand() {
  if (!Serial.available()) return;
  char cmd = Serial.read();
  if (cmd == 'R' || cmd == 'r') {
    targetSteps = currentSteps + STEPS_PER_REV;
    Serial.println("{\"cmd\":\"cw_1rev\"}");
  } else if (cmd == 'L' || cmd == 'l') {
    targetSteps = currentSteps - STEPS_PER_REV;
    Serial.println("{\"cmd\":\"ccw_1rev\"}");
  } else if (cmd == 'A' || cmd == 'a') {
    int deg = Serial.parseInt();
    targetSteps = (long)((float)deg / 360.0f * STEPS_PER_REV);
    Serial.printf("{\"cmd\":\"goto_deg\",\"deg\":%d}\n", deg);
  }
  while (Serial.available() && Serial.peek() != '\n') Serial.read();
  if (Serial.available()) Serial.read();
}

void demoPattern() {
  // Demo tu dong khi chua co lenh: quay 90 do moi 3 giay, lap 4 lan roi dao chieu
  static unsigned long lastDemoMs = 0;
  static int demoCount = 0;
  static bool demoForward = true;
  const unsigned long DEMO_INTERVAL_MS = 3000;

  if (millis() - lastDemoMs < DEMO_INTERVAL_MS) return;
  lastDemoMs = millis();

  int stepsPerQuarter = STEPS_PER_REV / 4;
  targetSteps += demoForward ? stepsPerQuarter : -stepsPerQuarter;
  demoCount++;
  if (demoCount >= 4) {
    demoCount = 0;
    demoForward = !demoForward;
  }
}

void loop() {
  handleSerialCommand();
  demoPattern();

  if (currentSteps != targetSteps && micros() - lastStepMicros >= stepIntervalMicros) {
    lastStepMicros = micros();
    if (currentSteps < targetSteps) {
      stepIndex = (stepIndex + 1) % 8;
      currentSteps++;
    } else {
      stepIndex = (stepIndex + 7) % 8;
      currentSteps--;
    }
    applyStep(stepIndex);
  } else if (currentSteps == targetSteps) {
    releaseCoils(); // nha cuon day khi dung yen -> tiet kiem dien, giam nong
  }

  static unsigned long lastTelemetryMs = 0;
  if (millis() - lastTelemetryMs >= 1000) {
    lastTelemetryMs = millis();
    float degNow = (float)currentSteps / STEPS_PER_REV * 360.0f;
    Serial.printf("{\"steps\":%ld,\"deg\":%.1f,\"target_steps\":%ld}\n", currentSteps, degNow, targetSteps);
  }
}

5. Giải thích code

HALF_STEP_SEQ là bảng 8 trạng thái kích hoạt 4 cuộn dây theo thứ tự đúng của 28BYJ-48 — mỗi bước chỉ dịch chuyển trục một góc rất nhỏ (360°/4096 ≈ 0.088°). applyStep() ghi 4 chân IN1-IN4 theo hàng trong bảng.

Vòng lặp chính so sánh currentSteps với targetSteps: nếu khác nhau, cứ mỗi stepIntervalMicros (1500µs) lại tiến 1 bước theo đúng chiều; khi đã đến đích, gọi releaseCoils() để tắt toàn bộ cuộn dây — vừa tiết kiệm điện vừa giảm nhiệt độ động cơ khi không cần giữ momen giữ (holding torque).

Lệnh Serial R/L quay đúng 1 vòng (4096 bước), lệnh A<deg> quy đổi góc sang số bước tuyệt đối. Demo tự động xoay 90° mỗi 3 giây, lặp 4 lần rồi đảo chiều để minh họa chuyển động tuần hoàn.

6. Lưu ý an toàn phần cứng

  • ULN2003 là driver Darlington, tích hợp sẵn diode bảo vệ flyback cho cả 4 cuộn dây — không cần lắp diode rời như một số driver rời rạc khác.

  • Dòng định mức mỗi cuộn dây 28BYJ-48 khoảng 60-80mA, trong khi mỗi kênh ULN2003 chịu tới 500mA — dư an toàn cho ứng dụng half-step tiêu chuẩn, không cần tản nhiệt thêm.

  • 28BYJ-48 chạy ở 5V — bắt buộc cấp qua chân 5V/VIN của ESP32, không dùng 3V3 vì không đủ áp/dòng để driver hoạt động ổn định.

  • Nếu chạy nhiều động cơ/servo cùng lúc trên cùng một nguồn 5V yếu (ví dụ qua cổng USB máy tính), nên dùng nguồn 5V ngoài công suất lớn hơn để tránh brown-out reset ESP32.

  • Luôn gọi releaseCoils() khi động cơ đứng yên lâu — giữ dòng liên tục qua cuộn dây khi không cần thiết gây nóng động cơ và driver không cần thiết.

7. Lỗi thường gặp

Lỗi

Nguyên nhân

Cách sửa

Động cơ rung nhưng không quay, hoặc quay giật cục sai hướng

Đấu sai thứ tự IN1-IN4, hoặc đảo ngược thứ tự pha so với bảng HALF_STEP_SEQ

Kiểm tra lại đúng thứ tự dây theo màu (thường xanh dương-hồng-vàng-cam trên cáp 28BYJ-48 chuẩn) khớp IN1-IN4

Động cơ mất bước (trượt), quay không đủ góc yêu cầu

stepIntervalMicros quá nhỏ (quay quá nhanh) khiến động cơ không theo kịp, hoặc tải cơ khí quá nặng

Tăng stepIntervalMicros (giảm tốc độ) hoặc giảm tải trên trục ra

Động cơ nóng bất thường khi đứng yên

Không gọi releaseCoils(), cuộn dây vẫn được cấp dòng liên tục dù không di chuyển

Đảm bảo logic currentSteps == targetSteps gọi releaseCoils() đúng như trong code mẫu

8. Tổng kết

Bài này đã hướng dẫn điều khiển động cơ bước 28BYJ-48 qua driver ULN2003 bằng chuỗi half-step 8 bước, hỗ trợ quay theo vòng hoặc theo góc chính xác, và tối ưu năng lượng bằng cách nhả cuộn dây khi đứng yên.

Đây là nền tảng cho các ứng dụng cần định vị góc chính xác không cần servo trong subcategory Điều khiển động cơ, như mâm xoay camera, cơ cấu mở/đóng van, hoặc bàn quay trưng bày sản phẩm.