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.
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.
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.
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.
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?
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.
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!
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.
LOW when depressed at position $X_1$.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).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).
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.
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.
cam.stl & plate_stepper.stlMount 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.
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).
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.")
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()
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()
plate_stepper.stl and slide the switch closer to the cam.
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.
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.