Skip to content

Repository files navigation

HWStepGen

CI PlatformIO Registry Version License: MIT

Stepper motor speed control for the ESP32 family, with the STEP pulses generated by the LEDC peripheral instead of a software timer. Once a speed is set the CPU is no longer involved: loop() can block, WiFi can saturate a core, flash writes can suspend interrupts, and the pulse train keeps its rate.

#include <HWStepGen.h>

hwsg::Stepper motor;

void setup() {
  hwsg::StepperConfig config;
  config.stepPin = 25;
  config.dirPin  = 26;
  config.stepsPerRev = 200;
  config.microsteps  = 16;

  motor.begin(config);
  motor.setSpeed(120.0f);
}

void loop() {}

Scope

HWStepGen controls how fast a motor turns and in which direction, with optional smooth acceleration. It does not do positioning: there is no moveTo() and no step counting, because the LEDC peripheral emits a free-running square wave and keeps no record of how many pulses it produced.

Good fit: conveyors, pumps, spindles, fans, turntables — anything that runs at a speed. For exact step counts (CNC axes, 3D printers) use FastAccelStepper; see docs/architecture.md for what positioning would take on this hardware and how a backend for it could be added here.

Supported hardware

Every ESP32 variant with an LEDC peripheral, on Arduino core 2.x and 3.x. Chip capabilities are read from the SDK's soc_caps.h at compile time, so new variants work without a library update.

SoC Max motors Step rate Speed at 200 steps x 16 microsteps
ESP32 8 76 Hz - 78 kHz 1.4 - 1465 rpm
ESP32-S2, S3, P4 4 38 Hz - 39 kHz 0.7 - 732 rpm
ESP32-C3, C6 3 38 Hz - 39 kHz 0.7 - 732 rpm
ESP32-H2 3 31 Hz - 31 kHz 0.6 - 586 rpm

The motor limit is one LEDC timer per axis: the Arduino core pairs two channels onto every timer, and channels sharing a timer are forced to share a frequency. The library allocates around this automatically; hwsg::Stepper::availableChannels() reports what is left at runtime.

setSpeed() returns Error::FrequencyOutOfRange for speeds outside the achievable range, and minRpm() / maxRpm() report the range for your gearing. Details in docs/hardware.md.

Any step/dir driver works: A4988, DRV8825, TMC2208/2209/2130 in step-dir mode, TB6600, etc.

Installation

PlatformIO:

lib_deps = violet10b/HWStepGen@^1.1.0

Arduino IDE: Sketch > Include Library > Add .ZIP Library, or clone into your libraries/ folder. Requires the ESP32 board package (2.x or 3.x).

Usage

#include <HWStepGen.h>

hwsg::Stepper motor;

void setup() {
  Serial.begin(115200);

  hwsg::StepperConfig config;
  config.stepPin     = 25;
  config.dirPin      = 26;
  config.enablePin   = 27;    // optional
  config.stepsPerRev = 200;   // 1.8 degree motor
  config.microsteps  = 16;    // match the driver's MS jumpers

  hwsg::Error error = motor.begin(config);
  if (error != hwsg::Error::Ok) {
    Serial.printf("begin failed: %s\n", hwsg::toString(error));
    return;
  }

  motor.setSpeed(60.0f);      // negative reverses
}

void loop() {}

begin() never half-succeeds: on any error the GPIOs and the LEDC channel are released and the driver is left disabled.

Acceleration

A stepper commanded straight to a high speed stalls. hwsg::Ramp walks the speed towards a target over time without blocking:

hwsg::Ramp ramp;

void setup() {
  motor.begin(config);
  ramp.attach(motor);
  ramp.setAcceleration(300.0f);              // average rpm per second
  ramp.setEasing(hwsg::Easing::SmoothStep);
  ramp.setTargetSpeed(240.0f);
}

void loop() {
  ramp.update();   // every 5-20 ms is plenty
}

Linear is a constant-acceleration trapezoidal profile. SmoothStep and Sinusoidal taper the acceleration to zero at both ends, which is what takes most of the ringing out of a belt drive. SmootherStep additionally reaches zero jerk at the ends, for a higher peak. setAcceleration() takes the average over the ramp; peak acceleration is 1.5x for SmoothStep, 1.875x for SmootherStep and pi/2 for Sinusoidal. Reversing through zero is safe — the DIR flip happens at a standstill.

Several motors

hwsg::Stepper axisX, axisY;
hwsg::StepperBank axes;

void setup() {
  axisX.begin(xConfig);
  axisY.begin(yConfig);
  axes.add(axisX);
  axes.add(axisY);
}

void onEndstopHit() { axes.stopAll(); }

API summary

Stepper::begin(config) claim GPIOs and an LEDC channel; motor stays stopped
Stepper::end() stop, release everything, park STEP low
Stepper::setSpeed(rpm) signed speed; the sign selects direction
Stepper::setStepFrequency(hz) same, in steps per second
Stepper::stop() halt pulses, keep holding torque
Stepper::setDirection(dir) explicit direction change
Stepper::setEnabled(bool) drive the ENABLE pin
Stepper::actualSpeedRpm() speed after timer quantisation
Stepper::minRpm() / maxRpm() achievable range for this gearing
Ramp::setTargetSpeed(rpm) / update() non-blocking acceleration
StepperBank::stopAll() stop every axis

Fallible calls return hwsg::Error; hwsg::toString(error) names it for logging. The library allocates no memory, throws no exceptions, and blocks only for the driver's DIR setup time (microseconds). Reference documentation is in the headers under src/.

Examples

Notes

  • Direction changes halt the pulse train for dirSetupTimeUs (default 5 µs) before resuming, but do not decelerate first. Reverse from low speed or use Ramp.
  • The Arduino core hard-codes 10-bit duty resolution inside ledcWriteTone() on both core 2.x and 3.x, which fixes the frequency range above. If you need slower motion, raise the microstepping.
  • The step rate is quantised by the LEDC timer divider. actualStepFrequencyHz() reports what the hardware produces; requests more than 5 % off are rejected.
  • analogWrite(), servo libraries and LED dimmers use the same LEDC channels, and on core 2.x neither side can see the other's reservations. Channels are allocated top-down to stay clear of them; checkTimer() detects a takeover after the fact, and StepperConfig::ledcChannel pins a channel explicitly. See docs/hardware.md.

Contributing

See CONTRIBUTING.md. The step generator sits behind a small internal interface (docs/architecture.md), so alternative backends (RMT, MCPWM) can be added without touching the public API.

Credits

Large portions of the original code were rewritten using Fable 5 in Claude Code.

License

MIT, see LICENSE.

About

Stepper motor speed control for the ESP32 family. STEP pulses are generated by the LEDC peripheral, so the pulse rate is unaffected by CPU load. Supports ESP32, S2, S3, C3, C6, H2 and P4 on Arduino core 2.x and 3.x.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages