ESP32 điều khiển động cơ bước 28BYJ-48 với ULN2003: nửa bước, quay đúng góc, kim giây
Hướng dẫn điều khiển động cơ bước 28BYJ-48 bằng ESP32 qua mạch ULN2003: chế độ nửa bước, 4076 bước một vòng, quay đúng góc theo lệnh, kim giây đồng hồ và thí nghiệm STEM.
Động cơ bước 28BYJ-48 kèm mạch ULN2003 là bộ động cơ bước rẻ và dễ dùng nhất cho học sinh. Động cơ bước không quay liên tục như động cơ DC mà nhích từng bước nhỏ theo thứ tự cấp điện các cuộn dây. Vì vậy chương trình biết chính xác trục đã quay bao nhiêu mà không cần cảm biến. Bộ này hợp với bàn xoay chụp ảnh, kim đồng hồ mô hình, cơ cấu mở rèm, máy cho cá ăn.
Bài viết giải thích chế độ nửa bước, cách tính số bước cho một góc, cách nối ESP32 với ULN2003, hai chương trình mẫu (quay đúng góc theo lệnh, kim giây đồng hồ) và gợi ý thí nghiệm STEM.
1. Động cơ bước và chế độ nửa bước

- 28BYJ-48 có bốn cuộn dây. Dây đỏ là đầu chung nối +5 V; bốn dây xanh dương, hồng, vàng, cam đi qua ULN2003 xuống GND khi chân IN tương ứng ở mức cao.
- Nửa bước: lần lượt cấp điện một cuộn, rồi hai cuộn liền nhau, cứ thế qua tám pha. Mỗi pha rôto nhích 5,625°. Chế độ này êm hơn và chia góc mịn hơn so với cấp từng cuộn một.
- Hộp số bên trong giảm tốc khoảng 63,68 lần. Trục ra quay một vòng sau khoảng 64 × 63,68 ≈ 4076 nửa bước. Nhiều tài liệu ghi 4096; con số này lệch khoảng 0,5 % nên sau nhiều vòng sẽ thấy sai rõ.
- ULN2003 là mạch đệm dòng gồm 7 cặp transistor Darlington. Mạch nhận mức 3,3 V của ESP32 và đóng ngắt dòng khoảng 100–250 mA của mỗi cuộn dây. Bốn LED trên mạch sáng theo pha, rất tiện để quan sát.
2. Linh kiện cần chuẩn bị
- ESP32 DevKit V1 30 chân.
- Động cơ bước 28BYJ-48 loại 5 V và mạch ULN2003 (thường bán theo bộ).
- Breadboard 830 lỗ, dây cắm test board.
3. Sơ đồ nối dây

| ULN2003 | ESP32 |
|---|---|
| IN1 | D19 (GPIO19) |
| IN2 | D18 (GPIO18) |
| IN3 | TX2 (GPIO17) |
| IN4 | RX2 (GPIO16) |
| + | 5 V: chân VIN khi ESP32 cắm USB, hoặc nguồn 5 V riêng |
| − | GND |
Chân TX2, RX2 trên mạch là GPIO17 và GPIO16. Chương trình dùng chúng làm chân ra bình thường. Một động cơ 28BYJ-48 lấy 5 V từ cổng USB được; khi chạy hai động cơ trở lên thì dùng nguồn 5 V riêng, GND nối chung.
4. Chương trình 1: quay đúng góc theo lệnh
Gõ một số góc trên Serial Monitor (115200 baud, kiểu xuống dòng "New Line"). Ví dụ 90 quay 90°, -45 quay ngược 45°, v 3 đặt 3 ms cho mỗi nửa bước, 0 quay về vị trí lúc bật nguồn.
/*
PYWORLD.VN - ESP32 điều khiển động cơ bước 28BYJ-48 qua mạch ULN2003, chế độ nửa bước
ULN2003: IN1 -> GPIO19, IN2 -> GPIO18, IN3 -> GPIO17, IN4 -> GPIO16; nguồn mạch 5 V, GND chung với ESP32
Lệnh trên Serial Monitor (115200, New Line): 90 = quay 90 độ, -45 = quay ngược 45 độ, v 3 = mỗi nửa bước 3 ms, 0 = về vị trí đầu
*/
const uint8_t CHAN[4] = {19, 18, 17, 16}; // IN1..IN4
// Tám pha của chế độ nửa bước: lần lượt cấp điện 1 cuộn, rồi 2 cuộn liền nhau
const uint8_t NUA_BUOC[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}
};
// 64 nửa bước một vòng rôto x hộp số khoảng 63,68 : 1 = khoảng 4076 nửa bước một vòng trục ra
const long BUOC_MOI_VONG = 4076;
long viTri = 0; // vị trí so với lúc bật nguồn, đơn vị nửa bước
int pha = 0; // chỉ số hàng đang dùng trong bảng NUA_BUOC
int msMoiBuoc = 2; // thời gian mỗi nửa bước; nhỏ hơn 1 ms động cơ dễ trượt bước
void xuatPha(int p) {
for (int i = 0; i < 4; i++) digitalWrite(CHAN[i], NUA_BUOC[p][i]);
}
void nhaCuon() {
for (int i = 0; i < 4; i++) digitalWrite(CHAN[i], LOW);
}
void diBuoc(long soBuoc) {
int huong = (soBuoc >= 0) ? 1 : -1;
for (long i = 0; i < labs(soBuoc); i++) {
pha = (pha + huong + 8) % 8;
xuatPha(pha);
viTri += huong;
delay(msMoiBuoc);
}
nhaCuon(); // ngắt điện các cuộn dây cho đỡ nóng; trục được giữ nhờ ma sát hộp số
}
void inViTri() {
Serial.printf("Vi tri: %ld nua buoc = %.1f do\n", viTri, viTri * 360.0 / BUOC_MOI_VONG);
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < 4; i++) pinMode(CHAN[i], OUTPUT);
nhaCuon();
delay(500);
Serial.println("28BYJ-48: go so do (vi du 90, -45), v <ms>, hoac 0 de ve vi tri dau");
}
void loop() {
if (!Serial.available()) return;
String s = Serial.readStringUntil('\n');
s.trim();
if (s.length() == 0) return;
if (s.charAt(0) == 'v' || s.charAt(0) == 'V') {
int ms = s.substring(1).toInt();
if (ms >= 1 && ms <= 50) {
msMoiBuoc = ms;
Serial.printf("Moi nua buoc %d ms (khoang %.1f vong/phut)\n", ms, 60000.0 / (ms * BUOC_MOI_VONG));
}
return;
}
long buoc;
if (s == "0") {
buoc = -viTri; // quay ngược lại đúng số bước đã đi
} else {
float goc = s.toFloat();
if (goc == 0) {
Serial.println("Lenh khong hop le");
return;
}
buoc = lroundf(goc * BUOC_MOI_VONG / 360.0f);
}
Serial.printf("Quay %ld nua buoc, mat khoang %.1f s\n", buoc, labs(buoc) * msMoiBuoc / 1000.0);
diBuoc(buoc);
inViTri();
}

NUA_BUOC[8][4]là bảng tám pha ở mục 1. Tăng chỉ sốphathì quay một chiều, giảm thì quay chiều kia.lroundf(goc * 4076 / 360)đổi góc ra số nửa bước và làm tròn. Ví dụ 90° là 1019 nửa bước.viTriđếm tổng số bước đã đi, nên lệnh0chỉ cần quay ngược đúng bấy nhiêu bước.nhaCuon()ngắt điện cả bốn cuộn sau mỗi lần quay. Động cơ đỡ nóng, đỡ tốn điện, còn trục vẫn được ma sát hộp số giữ lại.
5. Chương trình 2: kim giây đồng hồ mô hình
Gắn một que nhẹ lên trục làm kim, đặt kim chỉ số 12 rồi cho chạy. Mỗi giây kim nhích một vạch, 60 giây quay đúng một vòng.
/*
PYWORLD.VN - Kim giây đồng hồ mô hình: động cơ 28BYJ-48 nhích một vạch mỗi giây (ESP32 + ULN2003)
Nối dây như chương trình BUOC_28BYJ48_QUAY_GOC
Mỗi giây tính lại vị trí đích từ số giây để sai số làm tròn không cộng dồn qua từng vòng
*/
const uint8_t CHAN[4] = {19, 18, 17, 16};
const uint8_t NUA_BUOC[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}
};
const long BUOC_MOI_VONG = 4076;
long viTri = 0;
int pha = 0;
int giay = 0;
unsigned long tTruoc = 0;
void diToi(long dich) {
while (viTri != dich) {
int huong = (dich > viTri) ? 1 : -1;
pha = (pha + huong + 8) % 8;
for (int i = 0; i < 4; i++) digitalWrite(CHAN[i], NUA_BUOC[pha][i]);
viTri += huong;
delay(2);
}
for (int i = 0; i < 4; i++) digitalWrite(CHAN[i], LOW);
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < 4; i++) pinMode(CHAN[i], OUTPUT);
Serial.println("Kim giay: dat kim chi so 12 roi cho chay");
tTruoc = millis();
}
void loop() {
if (millis() - tTruoc < 1000) return;
tTruoc += 1000;
giay++;
long dich = (long)giay * BUOC_MOI_VONG / 60; // vị trí của vạch thứ giay tính từ số 12
diToi(dich);
if (giay == 60) { // tròn một vòng: đặt lại mốc
giay = 0;
viTri = 0;
}
Serial.printf("Giay %2d -> vi tri %4ld nua buoc\n", giay, viTri);
}
4076 không chia hết cho 60 (khoảng 67,93 nửa bước mỗi giây). Nếu mỗi giây cứ đi 68 bước, sau mỗi vòng kim sẽ vượt 4 bước. Chương trình tính lại vị trí đích theo số giây đã qua: giay × 4076 / 60. Như vậy sai số làm tròn không cộng dồn, kim luôn về đúng số 12.
6. Tốc độ và lực của 28BYJ-48
- Với 2 ms mỗi nửa bước, trục ra mất khoảng 8,2 giây cho một vòng (khoảng 7,4 vòng/phút).
- Giảm xuống 1 ms thì nhanh gấp đôi nhưng động cơ dễ trượt bước, nhất là khi có tải. Trượt bước nghĩa là cuộn dây đã đổi pha mà rôto chưa kịp theo, nên vị trí đếm được không còn đúng.
- Động cơ này yếu và chậm. Cơ cấu nặng, cần nhanh hoặc cần chính xác thì dùng động cơ bước NEMA 17 với driver A4988 (bài sau trong loạt).
7. Gợi ý dùng trong giờ học STEM
Mục tiêu: học sinh hiểu điều khiển vòng hở (đếm bước, không cần cảm biến), tỉ số truyền của hộp số, và cách xử lý sai số làm tròn.
- Kiểm chứng tỉ số truyền. Đánh dấu vị trí trục, lần lượt cho quay 4096 và 4076 nửa bước, đo độ lệch so với vạch ban đầu. Con số nào đúng hơn? Lặp lại 10 vòng để thấy rõ sai lệch.
- Tìm tốc độ lớn nhất. Gắn một tải nhẹ (đĩa bìa có kẹp giấy), giảm dần
vcho tới khi động cơ bắt đầu trượt bước. Lập bảng tốc độ theo tải. - Làm đồng hồ. Dùng chương trình 2, vẽ mặt đồng hồ bằng bìa. Mở rộng: thêm động cơ thứ hai làm kim phút, hoặc lấy giờ từ Internet qua NTP như bài đồng hồ ESP32 NTP.
- Bàn xoay chụp ảnh 360°. Mỗi lần quay 15°, dừng 2 giây để điện thoại chụp, được 24 ảnh quanh một vật.
Hoạt động gắn với tỉ số và số đo góc trong Toán, chuyển động quay và bánh răng trong Công nghệ, Vật lý, và lập trình trong Tin học.
8. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| LED trên ULN2003 nháy nhưng trục không quay, chỉ rung | Sai thứ tự IN1–IN4, hoặc bước quá nhanh | Kiểm tra lại bốn dây theo bảng; tăng v lên 3–5 ms |
| Quay ngược chiều mong muốn | Chiều quay phụ thuộc cách lắp | Đổi dấu góc trong lệnh, hoặc đảo thứ tự mảng CHAN |
| Sau vài vòng lệch dần vị trí | Dùng 4096 thay vì 4076; có trượt bước | Dùng 4076; giảm tốc độ, giảm tải |
| Động cơ nóng | Cuộn dây được cấp điện liên tục | Gọi nhaCuon() sau mỗi lần quay như trong chương trình |
| ESP32 khởi động lại | Nguồn USB yếu khi chạy nhiều động cơ | Dùng nguồn 5 V riêng, GND chung |
9. Xem thêm
- Làm đồng hồ Internet với ESP32, NTP và màn hình OLED
- Lưu dữ liệu vào flash của ESP32 bằng Preferences (ghi nhớ vị trí động cơ khi mất điện)
- PWM trên ESP32
Cùng loạt: ESP32 điều khiển động cơ DC với L298N, servo SG90 và động cơ bước NEMA 17 với A4988.
Xem thêm