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() {}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.
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.
PlatformIO:
lib_deps = violet10b/HWStepGen@^1.1.0Arduino IDE: Sketch > Include Library > Add .ZIP Library, or clone into
your libraries/ folder. Requires the ESP32 board package (2.x or 3.x).
#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.
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.
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(); }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/.
- BasicSingleMotor — one motor, constant speed, reversing
- MultipleMotors — three axes, independent speeds, group stop
- SpeedRamp — non-blocking acceleration with easing
- Direction changes halt the pulse train for
dirSetupTimeUs(default 5 µs) before resuming, but do not decelerate first. Reverse from low speed or useRamp. - 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, andStepperConfig::ledcChannelpins a channel explicitly. See docs/hardware.md.
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.
Large portions of the original code were rewritten using Fable 5 in Claude Code.
MIT, see LICENSE.