AccelStepper: tăng tốc, giảm tốc mượt cho động cơ bước NEMA 17, A4988, ESP32, về gốc, hàm hay dùng
Dùng thư viện AccelStepper cho động cơ bước NEMA 17 và A4988 trên ESP32: vì sao cần tăng tốc, cách thư viện tính khoảng cách bước, nối dây, về gốc bằng công tắc, hàm hay dùng, lỗi thường gặp.
AccelStepper là thư viện Arduino điều khiển động cơ bước có tăng tốc, giảm tốc: thay vì phát xung đều từ đầu tới cuối (dễ mất bước lúc khởi động, giật mạnh lúc dừng), thư viện tăng dần tốc độ, chạy đều, rồi giảm dần để dừng đúng vị trí. Bài này giải thích cách AccelStepper tính tốc độ (kèm mô phỏng đúng thuật toán của thư viện), cách nối NEMA 17 với driver A4988 và ESP32, chương trình chạy qua lại có về gốc bằng công tắc hành trình và nhận lệnh qua Serial, bảng hàm hay dùng và các lỗi thường gặp.
1. Vì sao cần tăng tốc, giảm tốc
Động cơ bước có mô-men giảm khi tốc độ tăng, và phải thắng quán tính của tải khi khởi động. Phát ngay xung ở tốc độ cao thì rô-to không theo kịp: động cơ rung, kêu, đứng yên hoặc mất bước. Dừng đột ngột ở tốc độ cao thì quán tính kéo tải trượt thêm vài bước. Tăng tốc, giảm tốc dần cho phép chạy nhanh hơn nhiều mà không mất bước. Cấu tạo động cơ bước, vi bước và cách chỉnh dòng driver có ở bài driver A4988, DRV8825, TMC2209.
2. Nối dây

- STEP → GPIO17, DIR → GPIO16, EN → GPIO21 (mức thấp là cấp điện cho cuộn dây).
- MS1, MS2, MS3 nối 3,3 V: vi bước 1/16, một vòng 200 × 16 = 3200 bước.
- VMOT, GND nối nguồn 12 V riêng, tụ 100 µF sát chân; GND nguồn 12 V nối chung GND ESP32.
- Công tắc hành trình thường mở giữa GPIO4 và GND, dùng điện trở kéo lên trong chip.
Cách chỉnh dòng A4988 bằng biến trở, nối dây động cơ có ở bài ESP32 điều khiển NEMA 17 với A4988.
3. AccelStepper tính tốc độ thế nào

Thư viện không tính tốc độ theo thời gian mà tính khoảng cách giữa hai bước: bước đầu tiên cách c0 = 0,676 × √(2/a) giây, mỗi bước sau ngắn lại theo công thức cn = cn−1 − 2·cn−1 / (4n + 1), cho tới khi bằng khoảng cách ứng với tốc độ tối đa. Khi số bước còn lại bằng số bước cần để dừng (v² / 2a), thư viện đảo dấu n để giảm tốc. Script dưới đây làm đúng các bước đó để vẽ đồ thị và tính thời gian chạy, chạy bằng Python 3:
# PYWORLD.VN - Mô phỏng cách AccelStepper tính khoảng cách giữa các bước khi tăng tốc, chạy đều, giảm tốc (thuật toán của David Austin)
# PYWORLD.VN - c0 = 0,676 × √(2/a) × 10⁶ µs; bước sau: cn = cn−1 − 2·cn−1 / (4n + 1); chạy đều khi cn chạm 10⁶ / tốc độ tối đa
# PYWORLD.VN - Chạy: python mo_phong_accelstepper.py [số bước] [tốc độ tối đa] [gia tốc] (mặc định 9600 3200 6400)
"""Theo đúng computeNewSpeed() của AccelStepper khi chạy một quãng từ đứng yên tới đích (không đổi đích giữa chừng)."""
import math
import sys
def mo_phong(so_buoc: int, toc_do_max: float, gia_toc: float) -> list[tuple[float, float]]:
"""Trả về danh sách (thời điểm bước, tốc độ bước/giây) cho từng bước."""
c0 = 0.676 * math.sqrt(2.0 / gia_toc) * 1e6
cmin = 1e6 / toc_do_max
vi_tri, n, cn, toc_do, t, ds = 0, 0, 0.0, 0.0, 0.0, []
def tinh(): # computeNewSpeed() khi chỉ chạy một chiều tới đích
nonlocal n, cn, toc_do
con_lai = so_buoc - vi_tri
buoc_dung = int(toc_do * toc_do / (2.0 * gia_toc))
if con_lai == 0 and buoc_dung <= 1:
toc_do, n = 0.0, 0
return 0.0
if n > 0 and buoc_dung >= con_lai:
n = -buoc_dung # bắt đầu giảm tốc
if n == 0:
cn = c0
else:
cn = max(cn - 2.0 * cn / (4.0 * n + 1), cmin)
n += 1
toc_do = 1e6 / cn
return cn
khoang = tinh()
while khoang > 0:
t += khoang / 1e6
vi_tri += 1
ds.append((t, toc_do))
khoang = tinh()
return ds
if __name__ == "__main__":
so_buoc, vmax, a = (int(sys.argv[1]), float(sys.argv[2]), float(sys.argv[3])) if len(sys.argv) > 3 else (9600, 3200.0, 6400.0)
ds = mo_phong(so_buoc, vmax, a)
t_tang = vmax / a
ly_thuyet = (so_buoc / vmax + t_tang) if so_buoc >= vmax * vmax / a else 2 * math.sqrt(so_buoc / a)
print(f"{so_buoc} buoc, toc do toi da {vmax:g} buoc/s, gia toc {a:g} buoc/s2")
print(f"Thoi gian mo phong {ds[-1][0]:.3f} s; hinh thang ly thuyet {ly_thuyet:.3f} s")
print(f"Toc do lon nhat dat {max(v for _, v in ds):.0f} buoc/s; buoc dau cach {ds[0][0] * 1000:.2f} ms")

4. Chương trình ESP32
Cài thư viện AccelStepper của Mike McCauley trong Library Manager của Arduino IDE.
// PYWORLD.VN - Động cơ bước NEMA 17 + A4988 với thư viện AccelStepper trên ESP32: tăng tốc, giảm tốc mượt, về gốc bằng công tắc hành trình
// PYWORLD.VN - Chạy qua lại 0 <-> 3 vòng; lệnh Serial: g<số bước> tới vị trí, v<bước/giây> tốc độ tối đa, a<bước/giây²> gia tốc, s dừng êm
// PYWORLD.VN - A4988: STEP -> GPIO17, DIR -> GPIO16, EN -> GPIO21 (mức thấp là bật); MS1-MS3 nối 3,3 V (1/16 vi bước); công tắc gốc GPIO4 xuống GND
#include <AccelStepper.h>
const int CHAN_STEP = 17, CHAN_DIR = 16, CHAN_EN = 21, CHAN_CONG_TAC = 4;
const long BUOC_MOI_VONG = 200L * 16; // động cơ 1,8°, vi bước 1/16
AccelStepper dongCo(AccelStepper::DRIVER, CHAN_STEP, CHAN_DIR);
long dich = 3 * BUOC_MOI_VONG;
uint32_t lucToi = 0;
bool dangNghi = false;
void veGoc() {
Serial.println("Ve goc: chay cham ve phia cong tac");
dongCo.setSpeed(-800); // tốc độ không đổi, không tăng tốc
while (digitalRead(CHAN_CONG_TAC) == HIGH) dongCo.runSpeed();
dongCo.setCurrentPosition(0); // vị trí công tắc là 0 (lệnh này cũng đặt tốc độ về 0)
dongCo.runToNewPosition(160); // lùi ra khỏi công tắc 1/20 vòng, rồi lấy đó làm gốc
dongCo.setCurrentPosition(0);
Serial.println("Da ve goc");
}
void xuLyLenh() {
if (!Serial.available()) return;
String lenh = Serial.readStringUntil('\n');
lenh.trim();
if (lenh.length() == 0) return;
float so = lenh.substring(1).toFloat();
switch (lenh[0]) {
case 'g': dich = (long)so; dongCo.moveTo(dich); dangNghi = false; break;
case 'v': if (so > 0) dongCo.setMaxSpeed(so); break;
case 'a': if (so > 0) dongCo.setAcceleration(so); break;
case 's': dongCo.stop(); break; // giảm tốc theo gia tốc đã đặt rồi dừng, không dừng gắt
}
Serial.printf("Lenh %s: vi tri %ld, dich %ld\n", lenh.c_str(), dongCo.currentPosition(), dongCo.targetPosition());
}
void setup() {
Serial.begin(115200);
pinMode(CHAN_CONG_TAC, INPUT_PULLUP);
dongCo.setEnablePin(CHAN_EN);
dongCo.setPinsInverted(false, false, true); // chân EN của A4988 tích cực mức thấp
dongCo.enableOutputs();
dongCo.setMinPulseWidth(2); // µs; A4988 cần xung STEP tối thiểu 1 µs
dongCo.setMaxSpeed(3200); // bước/giây = 1 vòng/giây
dongCo.setAcceleration(6400); // bước/giây²: đạt tốc độ tối đa sau 0,5 giây
veGoc();
dongCo.moveTo(dich);
}
void loop() {
xuLyLenh();
dongCo.run(); // gọi càng thường xuyên càng tốt, không dùng delay()
if (dongCo.distanceToGo() == 0 && !dangNghi) {
dangNghi = true;
lucToi = millis();
Serial.printf("Toi vi tri %ld\n", dongCo.currentPosition());
}
if (dangNghi && millis() - lucToi >= 1000 && (dich == 0 || dich == 3 * BUOC_MOI_VONG)) {
dich = (dich == 0) ? 3 * BUOC_MOI_VONG : 0; // chạy qua lại sau 1 giây nghỉ
dongCo.moveTo(dich);
dangNghi = false;
}
}
- Không dùng
delay()trongloop():run()chỉ đi một bước khi đến lúc, phải được gọi liên tục. Một lệnhdelay(100)là động cơ khựng lại. - In Serial ít thôi: in mỗi vòng lặp làm chậm
run(), động cơ chạy không đều. Chương trình chỉ in khi tới vị trí hoặc khi nhận lệnh. - Về gốc: chạy chậm tốc độ không đổi (
runSpeed) về phía công tắc, gặp công tắc thì đặt vị trí 0, lùi ra một đoạn rồi lấy làm gốc. stop()dừng êm theo gia tốc đã đặt; cần dừng khẩn thì cắt chân EN bằng phần cứng.- Lệnh Serial:
g9600tới vị trí 9600,v6400đổi tốc độ tối đa,a3200đổi gia tốc,sdừng.

Động cơ 28BYJ-48 với mạch ULN2003 cũng dùng được AccelStepper với kiểu nửa bước: AccelStepper dc(AccelStepper::HALF4WIRE, IN1, IN3, IN2, IN4) (chú ý thứ tự IN1, IN3, IN2, IN4); xem bài 28BYJ-48 với ULN2003.
5. Lỗi hay gặp
| Hiện tượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Động cơ không quay, chỉ rung | Thiếu setMaxSpeed (mặc định rất chậm) hoặc thiếu setAcceleration | Đặt cả hai trước moveTo |
| Quay chậm hơn tốc độ đã đặt | run() không được gọi đủ nhanh: delay, in Serial nhiều | Bỏ delay, giảm in; việc nặng chuyển sang nhân thứ hai của ESP32 |
| Mất bước khi khởi động | Gia tốc quá lớn, dòng driver thấp | Giảm gia tốc; chỉnh dòng A4988 |
| Chạy ngược chiều | Đảo cặp cuộn dây hoặc quy ước chiều | setPinsInverted(true, false, true) hoặc đảo một cặp dây |
| Driver nóng khi đứng yên | Cuộn dây giữ dòng liên tục | disableOutputs() khi không cần giữ vị trí (mất mô-men giữ) |
| Về gốc đi luôn không dừng | Công tắc nối sai, chiều về gốc ngược | Đọc thử digitalRead của công tắc; đổi dấu tốc độ về gốc |
6. Xem thêm
- Động cơ bước cho máy CNC: NEMA 17, NEMA 23, mô-men giữ, vi bước
- Tự làm máy vẽ 2 trục bằng ESP32 và động cơ bước
Sản phẩm dùng trong bài: ESP32 DevKit V1 30 chân.
Bài cùng cụm chủ đề: động cơ bước kêu, rung, mất bước; điều khiển 16 servo bằng PCA9685.
Xem thêm