Stepper Motors
Driving stepper motors from an ESP32 for precise cockpit gauge needles, VOR indicators, and motorised controls.
A stepper motor moves in discrete steps rather than a continuous sweep. Each pulse to the driver advances the motor by one step, so position is tracked by counting steps — no position feedback sensor is needed for open-loop control.
| Feature | Stepper motor | Servo |
|---|---|---|
| Position accuracy | Precise (open-loop) | Good (closed-loop internally) |
| Holding torque | Excellent (energised) | Good |
| Speed | Moderate | Fast |
| Wiring complexity | 4–6 wires + driver board | 3 wires |
| Power consumption | Always drawing current when holding | Only when moving under load |
The 28BYJ-48 is the most common starter stepper motor. It runs on 5 V, has an internal gear reduction of ~64:1, and comes bundled with a ULN2003A driver board that accepts 3.3 V logic directly from the ESP32.
| Specification | Value |
|---|---|
| Supply voltage | 5 V DC |
| Phase resistance | ~200 Ω |
| Steps per revolution (half-step) | 4 096 (64 elec. steps × ~64 gear ratio) |
| No-load speed | ~15 RPM at 5 V |
| Stall torque | ~300–350 g·cm |
// 28BYJ-48 with ULN2003 — Library Manager → "AccelStepper" by Mike McCauley
#include <AccelStepper.h>
// HALF4WIRE (8-step) mode: IN1=23, IN3=21, IN2=22, IN4=19
// Note: AccelStepper expects (pin1, pin2, pin3, pin4) = (IN1, IN3, IN2, IN4)
AccelStepper stepper(AccelStepper::HALF4WIRE, 23, 21, 22, 19);
// 28BYJ-48: 64 electrical steps × gear ratio ≈ 64 = 4096 steps/rev (half-step)
const int STEPS_PER_REV = 4096;
void setup() {
stepper.setMaxSpeed(1000); // steps/second
stepper.setAcceleration(500); // steps/second²
}
void loop() {
stepper.moveTo(STEPS_PER_REV); // one full clockwise revolution
while (stepper.distanceToGo() != 0) {
stepper.run();
}
delay(500);
stepper.moveTo(0); // return to home position
while (stepper.distanceToGo() != 0) {
stepper.run();
}
delay(500);
}NEMA 17 is the industry-standard bipolar stepper motor for 3D printers and CNC machines. It delivers significantly more torque than the 28BYJ-48 and supports up to 1/16 microstepping for smooth motion. The A4988 driver requires a separate high-voltage motor supply (8–35 V) and a 3.3 V logic supply.
| A4988 Pin | Connect to | Notes |
|---|---|---|
| STEP | GPIO 23 | One pulse = one step |
| DIR | GPIO 22 | HIGH / LOW sets rotation direction |
| EN | GPIO 21 | Active LOW — drive LOW to enable, HIGH to coast |
| RST, SLP | 3.3 V (VDD) | Tie together and pull HIGH to keep driver active |
| VDD | 3.3 V | Logic supply from ESP32 |
| VMOT | 8–35 V external supply | Place 100 µF cap across VMOT / GND |
| MS1, MS2, MS3 | GND (full step) or see table | 000=full, 100=half, 010=¼, 110=⅛, 111=1/16 |
| 1A, 1B / 2A, 2B | Motor coil A / Motor coil B | 4-wire bipolar motor |
// NEMA 17 with A4988 driver — STEP / DIR interface
#include <AccelStepper.h>
const int STEP_PIN = 23;
const int DIR_PIN = 22;
const int EN_PIN = 21; // active LOW — pull LOW to enable
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
// NEMA 17 standard: 200 full steps/rev (1.8° per step)
// With 1/16 microstepping on A4988: 3200 steps/rev
const int STEPS_PER_REV = 200;
void setup() {
pinMode(EN_PIN, OUTPUT);
digitalWrite(EN_PIN, LOW); // enable the driver
stepper.setMaxSpeed(800);
stepper.setAcceleration(400);
}
void loop() {
stepper.moveTo(STEPS_PER_REV);
while (stepper.distanceToGo() != 0) { stepper.run(); }
delay(500);
stepper.moveTo(0);
while (stepper.distanceToGo() != 0) { stepper.run(); }
delay(500);
}A gauge stepper with a stop, such as the X27.168, finds its zero by driving onto that stop at every power-up. A compass card, a heading or ADF card, or an altimeter hand turns all the way round and has no stop, so it uses a stepper without one (an X27.589, for example). In CockpitConnect, set the Gauge Stepper's rotation to Continuous and wire an Index Sensor that the card passes once per turn. At every power-up the controller turns the card forward until the sensor triggers, then takes the shorter way round to the simulator's value (359° → 1° is 2° forward). If the sensor hasn't triggered within 1¼ turns, the controller stops the motor and the gauge shows as failed in the Flow Builder. It never guesses where the card is. On four coil pins the coils are switched off too; a driver chip keeps the motor energised.
| Sensor | Wiring | Setting |
|---|---|---|
| A3144 Hall sensor + a small magnet on the card | VCC to 5 V (it needs at least 4.5 V), GND to GND, OUT to the index pin. The output is open-collector and only pulls low, so the ESP32's internal pull-up to 3.3 V keeps the pin safe. | Sensor pulls high: off |
| Optical slot sensor + a flag on the card | Power the module from 3.3 V so its output can never exceed the pin's 3.3 V. An open-collector output that pulls low when the flag passes works as it is. | Off if it pulls low, on if it drives the pin high |
Measuring the index angle. The index angle is where the card is when it passes the sensor, read off the face. The easiest option is to mount the sensor where north passes and leave the angle at 0. Otherwise, turn the card by hand until the sensor triggers and enter the heading that sits under the lubber line. Then check it with the Pin Test's 0–360° slider: send the card to 90° and it should show east.
An X27.589 driven directly takes 1 080 steps a turn, the default. On a driver chip, enter the steps per turn yourself. An altimeter hand works the same way: map the altitude × 0.36 to the needle angle and the controller winds the hand round as many times as it takes.
| Application | Motor choice | Notes |
|---|---|---|
| Compass / heading card, altimeter hand | X27.589 (no stop) | Continuous rotation, homed on an Index Sensor (see above) |
| Altimeter drum counter | 28BYJ-48 | Drives rotating digit drums via shaft coupler |
| VOR / ILS needle | 28BYJ-48 / NEMA 17 | Map deviation value (−2.5 to +2.5 dots) to steps |
| Throttle / mixture lever | NEMA 17 | Requires feedback force; consider holding-torque spec |
| Motorised trim wheel | NEMA 17 | Continuously tracks simulator trim dataref |