← All projects
Automation Intermediate

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.

Difficulty
Intermediate
Time
1 h
Category
Automation
Updated

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. No delay() in loop(): a 100 ms delay makes the motor miss hundreds of steps. Split long work into chunks with millis().
  • 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 12800 to see the same rotation speed with 16 times as many pulses.