ESP32 + Stepper 28BYJ-48 + ULN2003: Precise Stepper Motor Control
Intermediate1/8/2026- Author: IoTSpark Maker

ESP32 + Stepper 28BYJ-48 + ULN2003: Precise Stepper Motor Control

Control a 28BYJ-48 stepper motor through a ULN2003 driver using an 8-step half-step sequence, rotating precisely by angle or step count — a foundation for turntables and automatic valve control.

esp32stepper28byj-48uln2003motor-controldieu-khien-dong-co
0 steps2 components

A stepper motor differs from a servo or DC motor in that it moves in precise, countable discrete steps — allowing accurate angular positioning without a position-feedback sensor (open-loop control), as long as it doesn't lose steps (falling out of sync from overload or spinning too fast).

The 28BYJ-48 + ULN2003 kit is the most popular combo for getting started: the ULN2003 driver (a Darlington array) boosts the 3.3V GPIO signal to enough current to drive the motor's 4 coils.

This project uses an 8-step half-step sequence for smoother rotation and better holding torque than full-step, supporting commands to rotate by full turns or by a precise angle over Serial.

Detailed guide

Control a 28BYJ-48 stepper motor through a ULN2003 driver using an 8-step half-step sequence, rotating precisely by angle or step count.

1. Introduction

A stepper motor differs from servos and DC motors in that it moves in discrete, countable steps — enabling precise angular positioning without a feedback sensor (open-loop control), as long as it doesn't lose steps (falling out of sync from overload or spinning too fast).

The 28BYJ-48 + ULN2003 kit is the most popular combo for getting started: the 28BYJ-48 is a 4-phase stepper motor with a 1:64 reduction gearbox (64 motor steps × 64 = 4096 actual steps per output-shaft revolution), while the ULN2003 is a Darlington driver board that boosts current, since the ESP32's GPIO (3.3V, ~20mA) can't directly drive the motor coils.

This project uses an 8-step half-step sequence (smoother and with better torque than 4-step full-step) to rotate by full turns or by a precise angle via Serial commands.

2. Components Needed

Component

Qty

Notes

ESP32 DevKit V4

1

The main control board

ULN2003 + 28BYJ-48 — Stepper Motor

1

A driver + 5V stepper motor kit, 4096 actual steps per revolution

3. Wiring Diagram

ULN2003 pin

ESP32 pin

Notes

VCC

5V (VIN)

The 28BYJ-48 needs ~200-300mA — don't use 3V3

GND

GND

A shared GND is mandatory

IN1

GPIO16

Phase A

IN2

GPIO17

Phase B

IN3

GPIO18

Phase C

IN4

GPIO19

Phase D

All four IN1–IN4 pins use GPIO16–19, which aren't part of the boot-strapping pin group, so they're safe to use as outputs right from boot.

4. Sample Code

/*
 * 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. Code Walkthrough

HALF_STEP_SEQ is an 8-state table that energizes the 4 coils in the correct order for the 28BYJ-48 — each step moves the shaft by a tiny angle (360°/4096 ≈ 0.088°). applyStep() writes IN1–IN4 according to that table's rows.

The main loop compares currentSteps against targetSteps: if they differ, it advances one step in the right direction every stepIntervalMicros (1500µs); once it reaches the target, it calls releaseCoils() to de-energize all the coils — saving power and reducing motor heat when holding torque isn't needed.

The Serial commands R/L rotate exactly one revolution (4096 steps), while A<deg> converts an angle into an absolute step count. The demo automatically rotates 90° every 3 seconds, repeating 4 times before reversing direction, to illustrate cyclic motion.

6. Hardware Safety Notes

  • The ULN2003 is a Darlington driver with built-in flyback protection diodes for all 4 coils — no separate diodes needed like some other discrete drivers.

  • Each 28BYJ-48 coil draws around 60-80mA, while each ULN2003 channel handles up to 500mA — plenty of headroom for standard half-step operation, no extra heatsinking needed.

  • The 28BYJ-48 runs at 5V — it must be powered through the ESP32's 5V/VIN pin, not 3V3, which doesn't provide enough voltage/current for the driver to run reliably.

  • If you're running several motors/servos at once on the same weak 5V source (say, a computer's USB port), use a higher-capacity external 5V supply to avoid brown-out resets on the ESP32.

  • Always call releaseCoils() when the motor sits idle for a while — keeping continuous current through the coils when it's not needed just heats up the motor and driver unnecessarily.

7. Common Issues

Issue

Cause

Fix

The motor vibrates but doesn't turn, or jerks in the wrong direction

IN1–IN4 wired in the wrong order, or the phase order is reversed relative to the HALF_STEP_SEQ table

Double-check the wire color order (typically blue-pink-yellow-orange on a standard 28BYJ-48 cable) matches IN1–IN4

The motor loses steps and doesn't rotate the full requested angle

stepIntervalMicros is too small (spinning too fast) for the motor to keep up, or the mechanical load is too heavy

Increase stepIntervalMicros (slow it down), or reduce the load on the output shaft

The motor gets unusually hot while idle

releaseCoils() isn't being called, so the coils keep drawing current even while not moving

Make sure the currentSteps == targetSteps logic calls releaseCoils(), exactly as in the sample code

8. Summary

This guide walked through controlling a 28BYJ-48 stepper motor via a ULN2003 driver using an 8-step half-step sequence, supporting full-turn or precise-angle rotation, and optimizing power by releasing the coils while idle.

This is the foundation for applications needing precise angular positioning without a servo, in the Motor Control category — such as camera turntables, valve open/close mechanisms, or a product display turntable.