← Course overview
Week 5 of 12 · Motion & Kinematics

Stepper Motors, Rotary Cams & Precision Homing

Understand why open-loop motors have zero memory of their physical angle, how eccentric rotary cam lobes actuate microswitches without mechanical shock, and how to write a 3-phase precision homing algorithm.

~5 hrs Bench Rig + Python Kinematics

Concept 01 A Motor With Zero Memory (Open-Loop Actuators)

When your Raspberry Pi boots up, it has no idea where the inspector wheel or sorting chute is pointing. Stepper motors are open-loop actuators: they execute discrete step pulses (200 steps/rev, 1.8° per step) blindly, without internal positional feedback.

The Power-On Blindness Problem

Standard DC motors, continuous rotation servos, and stepper motors lack absolute encoders. If the machine loses power, reboots, or is manually rotated by hand while turned off, the software's internal step counter resets to 0, completely desynchronized from physical reality.

Cumulative Step Drift & Missed Steps

During a 1,000-candy sorting run, high rotational acceleration ($I = m \cdot r^2$) or a candy friction drag can cause the stepper to skip 1–2 microsteps. Without an absolute datum, small errors accumulate until pocket nests misalign with the sensor, jamming candies and ruining classification.

The Engineering Solution: Absolute Datum Homing Every precision mechatronic system (from 3D printers and CNC mills to industrial pick-and-place robots) solves this by defining a physical Home Switch Datum ($0.00^\circ$). On startup and after recovery cycles, the motor slowly searches for this physical switch, hits the trigger, and resets the software coordinate system to true zero.

Concept 02 Why a Cam-Shaped Part? (`cam.stl` vs Hard Stops)

How do you mechanically trip a limit switch on a mechanism that needs to spin continuously 360 degrees? Why not use a hard physical stop pin or optical interrupter?

❌ Why Hard Stops Fail in Continuous Sorting

A hard metal/plastic stop pin physically blocks the motor. When the motor hits the pin, it violently stalls, draws maximum current, vibrates loudly, skips steps, and induces fatigue fractures in 3D-printed teeth and brackets. Worst of all: it prevents continuous forward rotation—the wheel would have to reverse 360° every single cycle.

✅ The Rotary Eccentric Cam Solution (`cam.stl`)

A radial cam lobe rotates smoothly with the shaft. For 330° of rotation, the microswitch roller rides on the smaller base cylinder without touching. Over a 30° sweep, an eccentric raised lobe gently ramps upward by 3.5mm, depressing the roller lever smoothly without impact, and then drops back down. The wheel can spin forward forever in one direction!

Rotation ω Eccentric Lobe (+3.5mm Lift) R_base = 24mm VMA327 / Omron Microswitch Tangential Contact Electrical Logic: • Base circle → HIGH (3.3V) • Lobe trips → LOW (GND)
Figure 5.1: Radial cam lobe (`cam.stl`) engaging the microswitch roller arm without mechanical shock.

Concept 03 Switch Hysteresis & The 3-Phase Homing Algorithm

Why can't you just rotate the motor fast until the switch clicks and stop instantly? The answer is switch hysteresis and motor deceleration lag.

What is Switch Hysteresis? Mechanical microswitches do not click ON and OFF at the exact same physical position. Due to the internal snap-action spring:
  • Operating Position ($OP$): The switch trips to LOW when depressed at position $X_1$.
  • Release Position ($RP$): The switch only releases back to HIGH when the cam backs off to position $X_2$ (where $X_2 \ne X_1$). The difference $\Delta = |X_1 - X_2|$ is called the mechanical hysteresis ($\approx 0.3\text{ mm}$ or $2\text{–}3$ full motor steps).
If you home at high speed, motor inertia causes overshoot. If you don't release the switch slowly, your zero point will drift by several steps depending on approach velocity!

The 3-Phase Precision Homing Algorithm

Phase 1: Coarse Fast Seek

Step forward at normal speed (~100 steps/sec) until GPIO.input(SWITCH_PIN) == LOW. This quickly finds the cam lobe from any unknown starting angle (up to 360° away).

Phase 2: Fine Slow Back-Off

Reverse the motor very slowly in single microsteps (~10 steps/sec) until GPIO.input(SWITCH_PIN) == HIGH. The exact transition edge where the switch spring releases defines the true repeatable physical zero.

Phase 3: Calibrated Datum Offset Move

The microswitch trigger point is physically located a few degrees offset from the optical sensor shroud. Once the true zero is registered in Phase 2, step forward by a fixed calibrated constant (e.g. OFFSET_STEPS = 14) so that Nest Pocket #1 stops perfectly centered directly underneath the AS7341 spectral sensor.

Kit What you need this week

  • Adafruit Motor HAT stacked on Raspberry Pi
  • One NEMA-17 stepper motor (1.8° / 200 steps/rev)
  • One VMA327 / Omron roller microswitch
  • 12V / 2A external DC power supply for the Motor HAT
  • 3D printed test parts: cam.stl & plate_stepper.stl
  • M3 × 4.5mm heat-set insert + M3 grub screw for D-shaft clamp

Wiring Stepper Motor & Switch Schematic

Raspberry Pi GPIO header I2C (SDA/SCL) Adafruit Motor HAT I2C Addr: 0x60 Stepper 1 (Coils) NEMA-17 Motor 4 coil leads 12V 2A PSU Motor Power Terminal Limit Switch GPIO20 (pin 38) Common → GND
Figure 5.2: Motor HAT power distribution and GPIO20 pull-up switch wiring.

Build Step by Step Implementation

Step 1

Assemble the bench test rig

Mount the NEMA-17 stepper into plate_stepper.stl. Slide cam.stl onto the 5.0mm D-shaft and tighten the M3 grub screw against the flat edge. Bolt the roller microswitch to the standoff posts on the bracket so the roller rests lightly against the cam base circle.

Step 2

Wire the Motor HAT & switch

Connect the stepper's 4 wires to Motor Port 1 (M1/M2). Wire the switch: COM (Common) pin to Pi GND (Pin 39), and NO (Normally Open) pin to GPIO20 (Pin 38).

Step 3

Test single-step motion

Verify motor coil wiring with a simple test script:

from adafruit_motorkit import MotorKit
from adafruit_motor import stepper
import time

kit = MotorKit(address=0x60)

# Move 50 steps forward (90 degrees on a 200-step motor)
print("Moving 50 steps forward...")
for _ in range(50):
    kit.stepper1.onestep(direction=stepper.FORWARD, style=stepper.SINGLE)
    time.sleep(0.01)

# ALWAYS release coils when stopped to prevent motor overheating!
kit.stepper1.release()
print("Motion complete. Motor released.")
Step 4

Write the 3-phase precision homing routine

Implement the complete robust homing sequence with coarse seek, fine back-off hysteresis release, and datum offset alignment:

import RPi.GPIO as GPIO
import time
from adafruit_motorkit import MotorKit
from adafruit_motor import stepper

SWITCH_PIN = 20
STEPS_PER_REV = 200
OFFSET_TO_POCKET_1 = 14  # Calibrated steps from switch release to Pocket #1 center

GPIO.setmode(GPIO.BCM)
GPIO.setup(SWITCH_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
kit = MotorKit(address=0x60)

def precision_home_wheel():
    print("--- Phase 1: Fast Coarse Search ---")
    max_search_steps = STEPS_PER_REV + 50  # Safety timeout (1.25 revolutions max)
    steps_taken = 0
    
    # Rotate forward until cam lobe trips switch (active LOW)
    while GPIO.input(SWITCH_PIN) == GPIO.HIGH:
        kit.stepper1.onestep(direction=stepper.FORWARD, style=stepper.SINGLE)
        steps_taken += 1
        time.sleep(0.008)  # Fast seek speed
        if steps_taken > max_search_steps:
            kit.stepper1.release()
            raise RuntimeError("Homing Error: Cam switch not detected within 250 steps! Check switch alignment.")

    print(f"Cam detected after {steps_taken} steps.")
    time.sleep(0.1)

    print("--- Phase 2: Fine Back-Off (Hysteresis Elimination) ---")
    # Reverse slowly in single steps until switch spring releases (reads HIGH)
    release_steps = 0
    while GPIO.input(SWITCH_PIN) == GPIO.LOW:
        kit.stepper1.onestep(direction=stepper.BACKWARD, style=stepper.SINGLE)
        release_steps += 1
        time.sleep(0.03)  # Slow precision speed

    print(f"Switch released after backing off {release_steps} steps. Physical 0.0° established.")
    time.sleep(0.1)

    print("--- Phase 3: Calibrated Datum Offset Move ---")
    # Step forward to align Pocket #1 directly under the optical shroud
    for _ in range(OFFSET_TO_POCKET_1):
        kit.stepper1.onestep(direction=stepper.FORWARD, style=stepper.SINGLE)
        time.sleep(0.01)

    kit.stepper1.release()
    print("✓ Wheel successfully homed and aligned at Pocket #1.")

# Run homing test
if __name__ == "__main__":
    try:
        precision_home_wheel()
    finally:
        kit.stepper1.release()
        GPIO.cleanup()
Step 5

Index between pockets (90° moves)

Now that physical zero is established, index exactly 90° (50 steps) per candy:

def index_next_pocket():
    """Index exactly 90 degrees (50 steps on a 200-step motor)"""
    print("Indexing to next pocket...")
    for _ in range(50):
        kit.stepper1.onestep(direction=stepper.FORWARD, style=stepper.SINGLE)
        time.sleep(0.008)
    kit.stepper1.release()

Diagnostics If It Breaks: Homing Troubleshooting

Error: "Cam switch not detected within 250 steps" Check mechanical alignment: Is the cam lobe physically pressing the roller switch arm deep enough to hear an audible "click"? If the roller is too far from the shaft center, loosen the switch mounting screws on plate_stepper.stl and slide the switch closer to the cam.
Homed angle varies by 2–3 degrees each run You are skipping Phase 2 (the slow back-off release). Hitting a switch at high speed and stopping immediately causes mechanical bounce and deceleration overshoot. Always home on the release edge at slow speed.
Motor vibrates loudly and skips steps during indexing The grub screw on cam.stl or wheel.stl is slipping on the D-shaft flat, or your step delay is too fast (<0.004s). Ensure the grub screw is tightened securely with an M3 hex key and your step delay is ≥0.008s.
Motor gets burning hot after 5 minutes Always call kit.stepper1.release() whenever the motor stops moving. If you leave coils energised while idle, the motor continuously consumes 1.5A of holding current, heating the motor to >70°C.
Week 5 Checkpoint You can place the cam at any random starting angle, run your Python script, and watch the motor automatically seek home, eliminate hysteresis, and stop consistently at the exact calibrated datum angle within ±0.2° repeatability.
Next week: Week 6 — Building the Inspector Wheel & Optical Shroud: Combining CAD, 3D printing, and your homing routine into the complete inspection subassembly.