Overview
This is a DIY motorized turntable designed primarily for 3D scanning, but it can also be used as a product display turntable.
The project was designed in Fusion 360 and built as a weekend project for my personal needs. It works reliably for me, and I’m sharing it in case others find it useful or want to improve on it.
Electronics & Control
The turntable is powered by:
-
ESP32
-
28BYJ-48 stepper motor
-
ULN2003 driver
-
Rotary encoder for:
Despite the small and inexpensive motor, the turntable is capable of rotating surprisingly heavy objects thanks to the large bearing-supported structure.
➡ Arduino (.ino) code and wiring diagram are included below.
Mechanical Design
-
Large plate diameter: 430 mm
-
The plate is split into 4 sections so it can be printed on smaller printers and then glued together.
-
Uses 608Z bearings (x5) for smooth rotation.
-
Detachable legs for easy storage and transport.
Printing Notes
Parts Needed
Future Improvements
This design works well for my use, but it’s not perfect. I may improve it in the future, and I invite anyone to modify, remix, or improve it as they see fit.
If you build it or change something—please share it!
Code and Readme:
#include
const uint8_t stepPins[4] = {21, 19, 18, 5}; const uint8_t encoderCLK = 15; const uint8_t encoderDT = 2; const uint8_t encoderSW = 4; const uint8_t stepSequence[8][4] = { {1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 1}, {0, 0, 0, 1}, {1, 0, 0, 1} }; volatile int16_t encoderDelta = 0; int8_t currentStep = 0; int16_t speedSetting = 0; int16_t currentSpeed = 0; const int16_t speedIncrement = 2; // Encoder detent multiplier const int16_t maxSpeedSetting = 160; // Clamp for stability const uint32_t maxStepInterval = 4500; // Slowest (µs) const uint32_t minStepInterval = 900; // Fastest (µs) const uint8_t accelStep = 1; // Acceleration step const uint32_t debounceMs = 50; bool paused = false; bool lastButtonState = HIGH; uint32_t lastButtonChange = 0; uint32_t lastStepMicros = 0; void IRAM_ATTR handleEncoder() { static uint8_t lastState = 0; uint8_t state = (digitalRead(encoderCLK) << 1) | digitalRead(encoderDT); uint8_t transition = (lastState << 2) | state; switch (transition) { case 0b0001: case 0b0111: case 0b1110: case 0b1000: encoderDelta++; break; case 0b0010: case 0b0100: case 0b1101: case 0b1011: encoderDelta--; break; default: break; } lastState = state; } void applyStep(int8_t stepIndex) { for (uint8_t i = 0; i < 4; i++) { digitalWrite(stepPins[i], stepSequence[stepIndex][i]); } } void stepMotor(int direction) { currentStep = (currentStep + direction + 8) % 8; applyStep(currentStep); } void setup() { for (uint8_t pin : stepPins) { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); } pinMode(encoderCLK, INPUT_PULLUP); pinMode(encoderDT, INPUT_PULLUP); pinMode(encoderSW, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(encoderCLK), handleEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(encoderDT), handleEncoder, CHANGE); } void loop() { int16_t delta; noInterrupts(); delta = encoderDelta; encoderDelta = 0; interrupts(); const bool buttonState = digitalRead(encoderSW); const uint32_t nowMs = millis(); if (buttonState != lastButtonState && (nowMs - lastButtonChange) > debounceMs) { lastButtonChange = nowMs; lastButtonState = buttonState; if (buttonState == LOW) { paused = !paused; if (paused) { currentSpeed = 0; } } } if (delta != 0) { speedSetting += delta * speedIncrement; speedSetting = constrain(speedSetting, -maxSpeedSetting, maxSpeedSetting); } if (paused) { lastStepMicros = micros(); return; } if (currentSpeed != speedSetting) { if (currentSpeed < speedSetting) currentSpeed += accelStep; else if (currentSpeed > speedSetting) currentSpeed -= accelStep; } if (currentSpeed != 0) { const int8_t direction = (currentSpeed > 0) ? 1 : -1; const uint32_t speedLevel = abs(currentSpeed); const uint32_t interval = constrain( map(speedLevel, 1, maxSpeedSetting, maxStepInterval, minStepInterval), minStepInterval, maxStepInterval ); const uint32_t now = micros(); if (now - lastStepMicros >= interval) { lastStepMicros = now; stepMotor(direction); } } } # ESP32 Turntable Controller Simple ESP32 sketch that uses a rotary encoder to adjust the speed (and direction) of a 28BYJ-48 stepper motor through a ULN2003 driver board. Pressing the encoder button pauses/resumes motion. ## Wiring ### Power - ESP32 VIN (5 V from USB) → ULN2003 VCC - ESP32 GND → ULN2003 GND → Rotary encoder GND (common ground) - Rotary encoder + → ESP32 3.3 V ### Stepper (via ULN2003 board) | ULN2003 pin | ESP32 GPIO | |-------------|------------| | IN1 | 21 | | IN2 | 19 | | IN3 | 18 | | IN4 | 5 | | Motor header| 28BYJ-48 | ### Rotary Encoder | Encoder pin | ESP32 GPIO | Notes | |-------------|------------|-----------------------| | CLK | 15 | Interrupt input | | DT | 2 | Interrupt input | | SW | 4 | Button (pause/resume) | | + | 3.3 V | Use INTERNAL PULLUP | | GND | GND | Share ground | ## Behavior - Turning the encoder clockwise increases speed in the forward direction. - Turning counter-clockwise decreases speed, eventually reversing direction. - Encoder button toggles pause/resume. ## Notes - Keep the ULN2003 board powered from 5 V; the 28BYJ-48 needs more than 3.3 V. - Ensure the ESP32, ULN2003, and encoder share ground. - If you need higher top speed, consider a stronger stepper (e.g., NEMA 17 with DRV8825).