Learning Goals 5 min
Cluster C ends with a finished, useful product. Today you build a small motorised turntable that spins a phone, a model, or a glass on cue — with controllable speed and direction. The mechanical build is dead simple; the cleverness is in the software. By the end of this lesson you will be able to:
- Mount a cardboard/wooden disc to a 28BYJ-48's output shaft and balance a small load on top without binding the bearings.
- Implement a finished "turntable" sketch with four operating modes: continuous slow spin, indexed stops (e.g. four 90° positions for a product photo shoot), reverse / forward toggling, and home-on-button.
- Add a button + LED user interface so the platform is usable as a stand-alone product (no Serial Monitor needed during demo).
Warm-Up 10 min
Bring out: 28BYJ-48 + ULN2003 (already wired), your cardboard/wooden disc, double-sided tape, and a small "cargo" — a phone, a 3D-printed model, a chess piece, a small mug. Nothing heavier than ~300 g (the 28BYJ-48's 0.3 kg·cm holding torque limit).
Plan before you glue
- Mount the stepper to a small base (cardboard box or scrap wood) so the output shaft points straight up.
- Tape the disc centred on the shaft. If the disc is heavy or unbalanced, the motor will lurch — keep the cargo centred too.
- Plan the user interface: how does someone start the spin? Stop it? Change the mode? We'll wire one button (Start/Stop), but you can scale to more.
One question
Your platform sits on a desk. A phone (180 g) is centred on it, and you ask it to spin at 10 RPM. Will the stepper manage?
Reveal
Yes, comfortably. The 28BYJ-48's holding torque is ~300 g·cm. With the phone centred (zero distance from the axis), the load on the motor is essentially just friction in the bearings — the phone's weight is supported by the platform/disc, not by the motor. The motor only has to overcome friction + inertia, which is a tiny fraction of its torque budget. If the phone were off-centre, the motor would also have to fight the torque from its weight × off-centre distance — which is when 300 g·cm starts to matter.
New Concept · Mode-based control + indexing 20 min
Four operating modes
A polished turntable supports more than "always spinning":
| Mode | Behaviour | Use case |
|---|---|---|
| CONT_FWD | Spin forward continuously at 8 RPM. | Window display, slow drink-display. |
| CONT_REV | Spin reverse continuously at 8 RPM. | Same but for left-handed photos. |
| INDEX | Stop at 0°, 90°, 180°, 270° — wait at each for 3 s, then advance. | 360° product photography (4 angles per rotation). |
| HOME | Rotate to the 0° position and stop. | End-of-session reset. |
Why mode-based and not just a long if-else chain
A mode-based design means each operating mode lives in its own small function. The main loop simply calls the active mode's function each iteration. Adding a new mode (e.g. "random advance every 5 s") is a 5-minute change. This is the same architecture pattern you met in L02-26 (Smart Bin Lid) as a state machine — modes are a softer version with fewer formal transitions.
enum Mode { IDLE, CONT_FWD, CONT_REV, INDEX, HOME };
Mode mode = IDLE;
void runIdle() { /* coils off, do nothing */ }
void runContFwd() { motor.step( SMALL_CHUNK); }
void runContRev() { motor.step(-SMALL_CHUNK); }
void runIndex() { /* see §4 */ }
void runHome() { /* see §4 */ }
void loop() {
// 1. Read inputs (button) - updates mode if pressed
// 2. Drive output based on current mode
switch (mode) {
case IDLE: runIdle(); break;
case CONT_FWD: runContFwd(); break;
case CONT_REV: runContRev(); break;
case INDEX: runIndex(); break;
case HOME: runHome(); break;
}
}"Continuous" in small chunks
Because stepper.step() blocks, calling step(2048) would freeze the loop for 5+ seconds — no chance to read the button until it finishes. Instead, in CONT modes, call step(SMALL_CHUNK) repeatedly. SMALL_CHUNK = 32 (about 5.6° at 12 RPM = ~80 ms per chunk) gives a responsive feel — the user's button press is detected within ~80 ms, fast enough to feel instant.
Indexing — knowing where 0° is
The 28BYJ-48 has no internal "I am at 0°" switch. We track position in software:
long currentPosition = 0; // in steps from boot
void stepAndTrack(int n) {
motor.step(n);
currentPosition += n;
}For HOME mode, compute how many steps back to 0:
void runHome() {
int needed = -(int)(currentPosition % STEPS_PER_REV);
// pick the shorter direction
if (needed > STEPS_PER_REV / 2) needed -= STEPS_PER_REV;
if (needed < -STEPS_PER_REV / 2) needed += STEPS_PER_REV;
stepAndTrack(needed);
mode = IDLE;
}For INDEX mode, advance to the next quarter-revolution boundary, pause, repeat.
Worked Example · The finished turntable 25 min
Step 1 — mechanical build
- Hot-glue the 28BYJ-48 to a small wooden base or cardboard box so the shaft sticks straight up.
- Centre the disc on the shaft. The disc should rotate freely without scraping the motor body.
- Tape (or velcro) the ULN2003 board to the base, away from the disc's rotation arc.
- Place the cargo (phone, model) on the centre of the disc.
Step 2 — electrical additions for the UI
Beyond the existing stepper wiring:
| Component | UNO pin |
|---|---|
| Mode button (push) | D2 → button → GND, with INPUT_PULLUP |
| Mode indicator LED | D6 → 220 Ω → LED → GND |
Step 3 — the sketch
// L03-14 · Rotating Display Platform
// Modes cycle on each button press: IDLE -> CONT_FWD -> INDEX -> HOME -> IDLE
// LED on D6 blinks the mode number (1, 2, 3, 4 short flashes per second).
#include <Stepper.h>
const int STEPS_PER_REV = 2048;
Stepper motor(STEPS_PER_REV, 8, 10, 9, 11);
const int COIL_PINS[4] = {8, 9, 10, 11};
const int BTN_PIN = 2;
const int LED_PIN = 6;
const int SMALL_CHUNK = 32; // ~5.6 deg, ~80 ms at 12 RPM
const int INDEX_QUARTER = STEPS_PER_REV / 4;
const unsigned long INDEX_PAUSE_MS = 3000;
enum Mode { IDLE, CONT_FWD, INDEX, HOME };
Mode mode = IDLE;
long currentPosition = 0;
unsigned long indexPauseUntil = 0;
int indexQuarter = 0;
bool lastBtn = HIGH;
unsigned long lastBtnEdge = 0;
const unsigned long DEBOUNCE_MS = 50;
void releaseCoils() {
for (int i = 0; i < 4; i++) digitalWrite(COIL_PINS[i], LOW);
}
void stepAndTrack(int n) {
motor.step(n);
currentPosition += n;
}
void runIdle() {
releaseCoils();
}
void runContFwd() {
stepAndTrack(SMALL_CHUNK);
}
void runIndex() {
unsigned long now = millis();
if (now < indexPauseUntil) {
releaseCoils();
return;
}
stepAndTrack(INDEX_QUARTER);
indexQuarter = (indexQuarter + 1) % 4;
indexPauseUntil = millis() + INDEX_PAUSE_MS;
}
void runHome() {
long offset = currentPosition % STEPS_PER_REV;
if (offset < 0) offset += STEPS_PER_REV;
int needed = -(int)offset;
if (needed < -STEPS_PER_REV / 2) needed += STEPS_PER_REV;
if (needed != 0) stepAndTrack(needed);
currentPosition = 0;
mode = IDLE;
}
// LED blinks the mode number, 1..4 flashes then a pause
unsigned long lastBlinkChange = 0;
int blinkPhase = 0;
void runIndicator() {
int targetFlashes = (int)mode + 1;
unsigned long now = millis();
unsigned long interval = (blinkPhase % 2 == 0) ? 100 : 200;
int totalPhases = targetFlashes * 2 + 1; // flashes + final long off
if (blinkPhase == totalPhases - 1) interval = 700;
if (now - lastBlinkChange >= interval) {
lastBlinkChange = now;
blinkPhase = (blinkPhase + 1) % totalPhases;
digitalWrite(LED_PIN, (blinkPhase % 2 == 0 && blinkPhase < targetFlashes * 2) ? HIGH : LOW);
}
}
void handleButton() {
bool b = digitalRead(BTN_PIN);
if (b != lastBtn && (millis() - lastBtnEdge) > DEBOUNCE_MS) {
lastBtnEdge = millis();
if (b == LOW) { // press, not release
mode = (Mode)(((int)mode + 1) % 4);
Serial.print("# Mode -> "); Serial.println((int)mode);
indexPauseUntil = 0;
blinkPhase = 0;
}
lastBtn = b;
}
}
void setup() {
Serial.begin(9600);
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
motor.setSpeed(12);
releaseCoils();
Serial.println("# Rotating Display Platform armed.");
}
void loop() {
handleButton();
runIndicator();
switch (mode) {
case IDLE: runIdle(); break;
case CONT_FWD: runContFwd(); break;
case INDEX: runIndex(); break;
case HOME: runHome(); break;
}
}Step 4 — upload and demo
- On power-up: IDLE mode, LED flashes once per cycle.
- Press button: CONT_FWD — platform spins forward slowly. LED flashes twice.
- Press again: INDEX — platform jumps 90° clockwise, waits 3 s, jumps again, etc. LED flashes three times.
- Press again: HOME — platform rotates to nearest 0° (via shortest path) and stops. LED briefly flashes four times before returning to IDLE.
- Press again: cycle restarts.
Step 5 — what to watch for
- The platform should turn smoothly with no audible "stalling" sound. If it stalls, your cargo is too heavy or too off-centre.
- Between INDEX pauses, the platform should hold still. If you can spin it by hand easily, that's expected (coils released). For a heavy load that drifts, comment out the
releaseCoils()call inrunIndex(). - HOME should always return to the same physical orientation, no matter how many turns the platform has done in CONT_FWD.
Try It Yourself 15 min
Goal: Add a CONT_REV mode (continuous reverse) between CONT_FWD and INDEX. Five modes instead of four.
Hint
Add to the enum, add a runContRev() that calls stepAndTrack(-SMALL_CHUNK), add a case to the switch, change the modulo in handleButton from 4 to 5. Don't forget to update the LED indicator if you want 5 flashes for the new mode.
Goal: Replace the "cycle through modes" button with three buttons: START, REVERSE, HOME. START toggles CONT_FWD ↔ IDLE; REVERSE flips direction while in continuous mode; HOME jumps straight to homing.
Hint
This is now a richer UI but simpler conceptually — each button maps to one action, not a cycling state machine. Track contDir = +SMALL_CHUNK or -SMALL_CHUNK as a separate variable. REVERSE just flips the sign.
Goal: Add a potentiometer to set the INDEX step size from the table — choose 4, 6, 8, or 12 indexed positions per revolution. So "6 positions" gives stops every 60°.
Hint
int pot = analogRead(A0);
int positions;
if (pot < 256) positions = 4;
else if (pot < 512) positions = 6;
else if (pot < 768) positions = 8;
else positions = 12;
int stepSize = STEPS_PER_REV / positions;This makes the platform far more useful for product photography: 8 positions = standard turntable; 12 = high-res shoot; 4 = quick preview.
Mini-Challenge · Ship the turntable 10 min
- Centre your cargo on the disc and shoot a short video of all four modes: IDLE → CONT_FWD → INDEX → HOME, with the button presses visible.
- Take 4 photos using the INDEX mode — one at each 90° position. This is the "product photo shoot" demo. Crop and align them so they look like a 4-frame turntable strip.
- Tape down all wiring so the unit looks like a finished product, not a breadboard mess. Hide the breadboard inside the wooden base if you can.
- Label the button with a printed or hand-written sticker: "PRESS: IDLE / SPIN / INDEX / HOME". Users shouldn't need a manual.
Ship-ready test: hand it to a classmate. Can they:
- Make it spin without instruction?
- Stop it and home it without instruction?
- Use it to take a 4-angle product photo of their pencil case in 30 seconds?
If yes, you've shipped Cluster C's capstone. Cluster D (UART, I²C, SPI) starts tomorrow — we move from motors to communication protocols, the way chips talk to each other.
Recap 5 min
A rotating display platform = stepper + disc + button + a mode-based state machine. The headline trick: because step() blocks, "continuous spin" is implemented as a tight stream of small step(SMALL_CHUNK) calls so the loop can still poll the button. Position-tracking in software (no encoder needed) gives us indexed stops and a working HOME mode. Mode cycling on a single button — IDLE → CONT_FWD → INDEX → HOME → IDLE — is enough UI for a polished demo. The architecture is "input → mode → action" — same shape we used for the smart-bin lid (L02-26) and the 2-wheel rover (L03-10), and it'll come back for the BLE car (L03-28). Cluster C done — next cluster opens with the question: how do chips talk to each other?
- Operating mode
- A named state of the application that determines its current behaviour (IDLE, INDEX, etc.). Simpler than a full state machine because transitions can be ad-hoc.
- Indexing
- Advancing in fixed angular chunks — e.g. 90° per index — so the platform stops at predictable positions. Standard for product photography turntables and CNC tool changers.
- Position tracking
- A software variable that records how many steps the motor has been commanded since boot. Lets you compute "how do I get home from here?".
- HOME pose
- The reference orientation (0°) that the system can always return to. Sets the convention for "forward", "left", etc.
- Coil release
- Writing LOW to all stepper control pins so the coils aren't energised. Saves power; loses holding torque.
- SMALL_CHUNK
- A small step count used inside the main loop so each
step()call returns quickly enough for the loop to also poll inputs. Trades a tiny per-iteration overhead for responsiveness. - UI indicator
- An output (LED, buzzer, screen) that shows the user what mode the device is in. Even one LED with mode-count blinks beats a silent device with no feedback.
- Ship-ready
- The polished "product" threshold: another person can use the device without verbal instructions. The L2/L3 capstone projects all aim for this.
Homework 5 min
- Finish the build. Take photos and (ideally) a 30-second video of the four modes.
- Save the sketch as
turntable.ino. - Read ahead to ARD-L03-15 (UART Deep Dive). Tomorrow we leave motors behind for a while and start on communication — first the UART/serial we've been quietly using since L01-24.
Bring back next class:
- Your finished turntable + photos/video.
- Your saved sketch.
- Two UNOs if you have access (we'll wire them "UART-to-UART" in §4 of L03-15). One UNO is fine if you don't — we'll loopback instead.