Complete guide: Choosing an industrial controller
To control a stepper motor with an ESP32 PLC, connect a stepper driver such as the TB6600 between the PLC and the motor. One PWM output of the PLC sends the step pulses (PUL), a second output sets the direction (DIR) and a third one enables the driver (ENA). The pulse frequency sets the speed: rpm = frequency × 60 / (200 × microsteps) for a 1.8° motor. On the ESP32 PLC 21, 42 and 58 the Q0.x outputs come from a PCA9685 chip, so the step frequency goes from 24 Hz to 1525 Hz: 3.6 to 228.9 rpm with 2 microsteps. The PLC does not count the pulses, so the position is open loop and comes from the time the pulses run.
This guide shows the wiring, the driver settings and two Arduino sketches for the setup we used in our lab, a 17HS16-2004S NEMA 17 motor and a TB6600 microstep driver: one moves the motor at a constant speed and the other one adds an acceleration ramp.
What speed can an ESP32 PLC give a stepper motor?
The speed depends on the microstep setting of the driver and on the frequency range of the PWM output. On the ESP32 PLC 21, 42 and 58, Q0.0 to Q0.7 are outputs of a PCA9685 PWM chip at I2C address 0x40, with a 25 MHz clock and a 12-bit counter. The Industrial Shields ESP32 boards package (version 2.7.1) accepts frequencies from 24 Hz to 1525 Hz on these outputs (1526 Hz is rejected by the core; a request for 1525 Hz gives the highest frequency the chip makes, 1525.9 Hz). With a 200-step motor:
| Microsteps | Pulses per turn | Lowest speed (24 Hz) | Highest speed (1525.9 Hz) |
|---|---|---|---|
| 1 (full step) | 200 | 7.2 rpm | 457.8 rpm |
| 2 | 400 | 3.6 rpm | 228.9 rpm |
| 4 | 800 | 1.8 rpm | 114.4 rpm |
| 8 | 1600 | 0.9 rpm | 57.2 rpm |
| 16 | 3200 | 0.45 rpm | 28.6 rpm |
| 32 | 6400 | 0.22 rpm | 14.3 rpm |
Two details of the PCA9685 change how you write the program:
- The frequency moves in steps. The chip can only make 25 MHz / (4096 × (prescaler + 1)), with a whole prescaler. Near the top of the range the steps are large: 1525.9, 1220.7, 1017.3, 871.9 and 762.9 Hz are consecutive values. If you ask for 333 Hz you get 339 Hz, so the motor turns 1.8 % faster than requested. The sketches below calculate the real frequency and use it to time the movement.
- The frequency is the same for the whole chip. Changing it for Q0.0 changes it for every PWM output of that PCA9685. Use the other outputs of the chip as on/off signals, like DIR and ENA here, or for loads that do not depend on the frequency.
On the ESP32 PLC 14, Q0.0 to Q0.3 are direct pins of the ESP32 with its own LEDC PWM, so the 24 to 1525 Hz limit of the PCA9685 does not apply to them. The sketches in this post are written for the PCA9685 outputs.
How a stepper motor and its driver work
A stepper motor turns in fixed angular steps instead of turning continuously. The 17HS16-2004S has a step angle of 1.8°, which is 200 steps per turn. Each pulse that the driver receives moves the motor one step, or one fraction of a step in microstep mode, so the number of pulses sets the position and the pulse frequency sets the speed.
The PLC cannot power the motor coils directly. The driver receives three logic signals and switches the coil current:
- PUL (pulse): one pulse per step or microstep.
- DIR (direction): clockwise or counterclockwise.
- ENA (enable): with the inputs wired as in this guide, a high signal disables the driver and frees the motor, and a low signal enables it.
NEMA 17 17HS16-2004S motor specifications
| Specification | Value |
|---|---|
| Type | Bipolar |
| Holding torque | 0.45 Nm |
| Rated current | 2 A per phase |
| Phase resistance | 1.1 Ω |
| Inductance | 2.6 mH |
| Step angle | 1.8° (200 steps per turn) |
TB6600 driver settings
The driver has six DIP switches. S1 to S3 select the microsteps and S4 to S6 the current. The table printed on the driver used in this test gives these combinations:
| Microsteps | Pulses per turn | S1 | S2 | S3 |
|---|---|---|---|---|
| 1 | 200 | ON | ON | OFF |
| 2 (2/A) | 400 | ON | OFF | ON |
| 4 | 800 | ON | OFF | OFF |
| 8 | 1600 | OFF | ON | OFF |
| 16 | 3200 | OFF | OFF | ON |
| 32 | 6400 | OFF | OFF | OFF |
For the 2 A motor, set the current to 2.0 A (2.2 A peak): S4 ON, S5 OFF, S6 OFF. The sketches use 2 microsteps. Driver versions can print a different table, so always follow the label of your driver.
Wiring the ESP32 PLC to the TB6600 driver
| From | To | Function |
|---|---|---|
| Q0.0 | PUL+ | Step pulses |
| Q0.1 | DIR+ | Direction |
| Q0.2 | ENA+ | Enable |
| PLC GND | PUL-, DIR-, ENA- | Common of the signals |
| Motor blue / red | A- / A+ | Coil A |
| Motor black / green | B- / B+ | Coil B |
| 24 Vdc power supply | PLC Vdc and driver VCC | Power |
The outputs of the ESP32 PLC take their voltage from QVdc. In this setup QVdc is connected to 5 V and COM(-) to GND, so the signals reach the driver at 5 V. The same 24 Vdc supply feeds the PLC and the driver. Check the colours of your motor cable in its datasheet: they change between manufacturers.

Arduino sketch: constant speed
Install the Industrial Shields ESP32 boards package in the Arduino IDE and select your ESP32 PLC model. The package already includes analogWriteSetFrequency() and analogWrite() for the PCA9685 outputs, so no extra library is needed. The first version of this post used the Adafruit PWM Servo Driver library on the same chip; with the current package it is not necessary.
The function moveRevs() receives the turns (negative to turn the other way) and the speed in rpm. It sets the frequency, starts the pulses with a 50 % duty cycle (2048 of 4095), waits the time that the real frequency needs to send the pulses and stops them.
// Stepper motor at constant speed with an ESP32 PLC and a TB6600 driver
// Q0.0 -> PUL+, Q0.1 -> DIR+, Q0.2 -> ENA+, GND -> PUL-, DIR-, ENA-
#define STEP_PIN Q0_0
#define DIR_PIN Q0_1
#define ENA_PIN Q0_2
#define MOTOR_STEPS 200 // 1.8 degree motor
#define MICROSTEPS 2 // DIP switches of the driver
#define STEPS_REV (MOTOR_STEPS * MICROSTEPS) // 400 pulses per turn
// Q0.x outputs come from a PCA9685 (25 MHz clock, 12-bit).
// The chip can only make the frequencies 25 MHz / (4096 x (prescaler + 1)).
// This returns the real frequency that analogWriteSetFrequency() will set.
float realFrequency(uint32_t hz) {
uint32_t prescaler = (((25000000UL << 4) / (4096UL * hz)) >> 4) - 1;
return 25000000.0 / (4096.0 * (prescaler + 1));
}
// Turns the motor 'revs' turns (negative = other direction) at 'rpm'.
// Returns the number of pulses sent, calculated from the real frequency.
float moveRevs(float revs, float rpm) {
uint32_t hz = (uint32_t)(rpm * STEPS_REV / 60.0 + 0.5);
if (hz < 24 || hz > 1525) {
Serial.println("Speed out of range for this microstep setting");
return 0;
}
float realHz = realFrequency(hz);
unsigned long runMs = (unsigned long)(fabs(revs) * STEPS_REV * 1000.0 / realHz + 0.5);
analogWriteSetFrequency(STEP_PIN, hz);
digitalWrite(DIR_PIN, revs >= 0 ? HIGH : LOW);
digitalWrite(ENA_PIN, LOW); // LOW = driver enabled
analogWrite(STEP_PIN, 2048); // 50 % duty: pulses start
delay(runMs);
analogWrite(STEP_PIN, 0); // pulses stop
digitalWrite(ENA_PIN, HIGH); // HIGH = driver disabled
Serial.print("Requested Hz: "); Serial.print(hz);
Serial.print(" real Hz: "); Serial.print(realHz);
Serial.print(" time ms: "); Serial.println(runMs);
return runMs * realHz / 1000.0;
}
void setup() {
Serial.begin(115200);
pinMode(DIR_PIN, OUTPUT);
pinMode(ENA_PIN, OUTPUT);
digitalWrite(ENA_PIN, HIGH);
}
void loop() {
moveRevs(1.5, 50); // 1.5 turns forward at 50 rpm
delay(500);
moveRevs(-1.5, 50); // and back
delay(500);
}
With 2 microsteps, 50 rpm asks for 333 Hz. The PCA9685 gives 339.08 Hz, so 1.5 turns take 1769 ms instead of 1800 ms.
Arduino sketch: acceleration ramp
A load with inertia can lose steps if the motor starts or stops at full speed. This sketch follows a trapezoidal profile: every 30 ms it calculates the speed allowed by the acceleration from the start and by the braking to the end, and it updates the frequency. It counts the pulses sent from the real frequency of each period, so it stops when the target is reached.
// Stepper motor with acceleration ramp, ESP32 PLC + TB6600 driver
// Same wiring as the constant speed example
#define STEP_PIN Q0_0
#define DIR_PIN Q0_1
#define ENA_PIN Q0_2
#define MOTOR_STEPS 200
#define MICROSTEPS 2
#define STEPS_REV (MOTOR_STEPS * MICROSTEPS)
#define MIN_HZ 24 // lowest PCA9685 frequency
#define MAX_HZ 1525 // highest PCA9685 frequency
#define UPDATE_MS 30 // speed update period
float realFrequency(uint32_t hz) {
uint32_t prescaler = (((25000000UL << 4) / (4096UL * hz)) >> 4) - 1;
return 25000000.0 / (4096.0 * (prescaler + 1));
}
// Moves 'revs' turns with a trapezoidal profile.
// maxRpm: cruise speed. accel: acceleration in pulses per second squared.
void moveRamp(float revs, float maxRpm, float accel) {
float target = fabs(revs) * STEPS_REV; // pulses to send
float maxHz = maxRpm * STEPS_REV / 60.0;
if (maxHz > MAX_HZ) maxHz = MAX_HZ;
if (maxHz < MIN_HZ) maxHz = MIN_HZ;
float done = 0; // pulses sent so far
uint32_t lastHz = 0;
float realHz = 0;
digitalWrite(DIR_PIN, revs >= 0 ? HIGH : LOW);
digitalWrite(ENA_PIN, LOW);
while (done < target) {
float left = target - done;
// speed allowed by the acceleration (from start) and the braking (to end)
float vUp = sqrt(2.0 * accel * done);
float vDown = sqrt(2.0 * accel * left);
float v = min(maxHz, min(vUp, vDown));
if (v < MIN_HZ) v = MIN_HZ;
uint32_t hz = (uint32_t)(v + 0.5);
if (hz != lastHz) {
analogWriteSetFrequency(STEP_PIN, hz);
if (lastHz == 0) analogWrite(STEP_PIN, 2048);
lastHz = hz;
realHz = realFrequency(hz);
}
// last period: wait only the time the remaining pulses need
unsigned long waitMs = UPDATE_MS;
float pulses = realHz * UPDATE_MS / 1000.0;
if (pulses > left) {
waitMs = (unsigned long)(left * 1000.0 / realHz + 0.5);
pulses = left;
}
delay(waitMs);
done += pulses;
}
analogWrite(STEP_PIN, 0);
digitalWrite(ENA_PIN, HIGH);
}
void setup() {
Serial.begin(115200);
pinMode(DIR_PIN, OUTPUT);
pinMode(ENA_PIN, OUTPUT);
digitalWrite(ENA_PIN, HIGH);
}
void loop() {
moveRamp(2, 120, 2000); // 2 turns, up to 120 rpm, 2000 pulses/s2
delay(500);
moveRamp(-2, 120, 2000);
delay(500);
}
Each frequency change is an I2C write to the PCA9685, so the speed changes in steps every 30 ms and not pulse by pulse. With a high acceleration the steps are large and the movement is less smooth. Lower the acceleration or update more often if the motor vibrates or loses steps.
What we checked and what you should check
Both sketches compile to an object file without warnings against the Industrial Shields ESP32 package 2.7.1, for the ESP32 PLC 21 and the ESP32 PLC 19R (compiler test only, not a full build with the Arduino IDE). We also ran their timing logic on a PC with simulated outputs: 1.5 turns at 50 rpm give 599.8 of 600 pulses, and a ramp of 2 turns gives 800.0 of 800 pulses. These new versions have not been tested on a motor yet, so check them on your machine at low speed first.
- The position is open loop. A pulse can be lost when the PWM starts or stops, and the error adds up over many moves. Use a home sensor on an input of the PLC to reset the position at the start of each cycle.
- If the motor vibrates without turning, lower the speed or the acceleration, or raise the current of the driver up to the rated current of the motor.
- If you need more speed, use fewer microsteps: the frequency limit stays the same and each pulse moves the motor further.
FAQ about stepper motors and the ESP32 PLC
Can an ESP32 PLC drive a stepper motor without a driver?
No. The outputs of the PLC give logic signals. A driver such as the TB6600 switches the current of the motor coils and makes the microsteps.
Which outputs of the ESP32 PLC can send the step pulses?
Any PWM output. On the ESP32 PLC 21, 42 and 58, Q0.0 to Q0.7 are PCA9685 outputs from 24 Hz to 1525 Hz. Use the other outputs of the same chip for DIR, ENA or on/off loads, because the frequency is shared.
How do I calculate the speed of the motor?
rpm = frequency × 60 / pulses per turn. With 2 microsteps a 200-step motor needs 400 pulses per turn, so 1000 Hz is 150 rpm. Remember that the PCA9685 rounds the frequency up to the next value that it can make.
Does the ESP32 PLC know the position of the motor?
Not with these sketches. They calculate the position from time and frequency. For a known position, add a home sensor, or use an encoder input if the machine needs a closed loop.
Can I use the Adafruit PWM Servo Driver library?
It controls the same PCA9685, but the Industrial Shields package already initializes the chip and has analogWriteSetFrequency(). Using one interface for the chip avoids two pieces of code writing different settings to it.
More about the ESP32 PLC
- ESP32 PLC: PWM outputs test
- Choosing an industrial controller
- Industrial applications: 50 real examples by sector
- Safe solenoid valve sequencing on an ESP32 PLC with a state machine
See all the models in the ESP32 PLC family.
Take it further
Stepper motors in real machines: a bottle labelling conveyor, a laser research rig and the driver for your own prototype.
Case study
Label bottles in motion, while the line speed changes.
An encoder, a photocell and two stepper motors on one Arduino based PLC.
Read the case study
Success story
A laser shows the grain. A PLC turns the wood.
A PLC turns a wood sample in 3.6° steps with a stepper motor, 100 positions per turn, without losing a step.
Read the success story
Product
Stepper motor controller, 4 A, 9 to 42 V
A driver for 2 and 4 phase stepper motors, to pair with the outputs of a PLC.
See the product



