NEMA17 + A4988 and AccelStepper: Acceleration, Braking and Why Steppers Stall
A stepper cannot start at full speed: the rotor cannot keep up with the field and misses steps. The AccelStepper library builds a trapezoidal speed profile. We set up the A4988, microstepping and acceleration, and control the motion with serial commands.
About the project
The NEMA17 is the standard stepper of 3D printers and CNC machines: 200 steps per revolution (1.8°), 0.3–0.5 N·m of torque. An A4988 driver runs it: one pulse on STEP is one step, and the level on DIR is the direction. But if you just start sending pulses at 1000 Hz, the motor hums and does not move: the rotor has inertia and cannot keep up with the rotating field. It needs acceleration, and the AccelStepper library does it for you.
What you need
- ESP32 DevKit
- NEMA17 motor (e.g. 17HS4401) + A4988 driver (or a DRV8825)
- A 12 V ≥ 2 A supply and a 100 µF capacitor on VMOT
- The AccelStepper library
Wiring
| A4988 | Connection |
|---|---|
| VMOT / GND | +12 V / GND (a 100 µF capacitor right at the pins!) |
| VDD / GND | ESP32 3V3 / GND |
| STEP | GPIO26 |
| DIR | GPIO27 |
| EN | GPIO25 (active LOW) |
| RESET ↔ SLEEP | joined with a jumper |
| 1A 1B 2A 2B | the motor windings |
| MS1–MS3 | microstepping (see below) |
All grounds are shared. Never unplug the motor from a powered driver: the voltage spike kills the A4988.
The current is set with the trimmer before the first run: Vref = I × 8 × Rs. For a typical clone with R100 (0.1 Ω) sense resistors and a 1.2 A motor, that is ≈ 0.8 × 1.2 × 0.85 ≈ 0.8 V. We take 85 % of the rating so the driver does not overheat.
Microstepping
The MS1–MS3 jumpers divide the full step: 1/2, 1/4, 1/8, 1/16. At 1/16 the motor makes 3200 microsteps per revolution and runs quieter and smoother, but the same speed needs 16 times as many pulses.
Why acceleration is needed
A motor can only "catch" a limited starting rate (the pull-in rate, a few hundred steps per second from standstill). To reach 1000–2000 steps/s, the speed must be ramped up gradually. Rush it, and the rotor falls a step behind and the motor stalls: it hums, vibrates and stops, and the position in the program no longer matches reality.
AccelStepper computes the interval to every next step so that the speed grows linearly (a trapezoidal profile): acceleration → cruising at maxSpeed → deceleration to the stopping point.
Code
// NEMA17 + A4988 with AccelStepper: accelerate/brake, shuttle back and forth, serial commands
#include <AccelStepper.h>
const int STEP_PIN = 26, DIR_PIN = 27, EN_PIN = 25;
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
long target = 800; // 4 revolutions at full step (200 steps per revolution)
bool shuttle = true; // go back and forth until a command arrives
void setup() {
Serial.begin(115200);
stepper.setEnablePin(EN_PIN);
stepper.setPinsInverted(false, false, true); // the A4988 EN pin is active LOW
stepper.enableOutputs();
stepper.setMaxSpeed(800); // steps/s
stepper.setAcceleration(1600); // steps/s²
stepper.moveTo(target);
Serial.println("Commands: go <steps>, speed <steps/s>, accel <steps/s2>, stop");
}
void loop() {
if (shuttle && stepper.distanceToGo() == 0) { // arrived: turn around
target = -target;
stepper.moveTo(target);
}
stepper.run(); // call as often as possible: it steps only when a step is due
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd.startsWith("go ")) { shuttle = false; stepper.moveTo(cmd.substring(3).toInt()); }
else if (cmd.startsWith("speed ")) stepper.setMaxSpeed(cmd.substring(6).toFloat());
else if (cmd.startsWith("accel ")) stepper.setAcceleration(cmd.substring(6).toFloat());
else if (cmd == "stop") { shuttle = false; stepper.stop(); } // a smooth stop
Serial.printf("position %ld, target %ld\n", stepper.currentPosition(), stepper.targetPosition());
}
}
AccelStepper rules
run()must be called continuously. Nodelay()inloop(): a 100 ms delay makes the motor miss hundreds of steps. Split long work into chunks withmillis().moveTo()is an absolute position,move()a relative one.stop()decelerates with the set acceleration; it does not stop instantly.runSpeed()+setSpeed()give constant speed with no ramp, e.g. for a conveyor. Starting that way only works at a low speed.runToPosition()blocks the program until it arrives. Handy for simple scenarios.- The same AccelStepper also drives a 28BYJ-48 on a ULN2003:
AccelStepper stepper(AccelStepper::HALF4WIRE, IN1, IN3, IN2, IN4);. Note the pin order.
Experiments in the simulator
- Send
accel 100000: the ramp becomes almost instant, and the simulator shows the motor stalling and losing steps. - Set "heavy load" in the motor properties: the maximum speed and acceleration go down.
- Try 1/16 microstepping with
speed 12800to see the same rotation speed with 16 times as many pulses.