initial library and examples

This commit is contained in:
Liz 2025-06-13 16:23:33 -04:00
parent 18d73e6f0c
commit 9d2c40df66
6 changed files with 562 additions and 13 deletions

View file

@ -92,7 +92,20 @@ Usage Example
.. code-block:: python
import adafruit_stspin
import time
import adafruit_stspin220
import board
STEPS_PER_REVOLUTION = 200
STEP_PIN = board.D5
DIR_PIN = board.D6
motor = adafruit_stspin220.STSPIN220(STEP_PIN, DIR_PIN, STEPS_PER_REVOLUTION)
motor.speed = 60
total_microsteps = STEPS_PER_REVOLUTION * motor.microsteps_per_step
motor.step(total_microsteps)
Documentation
=============

View file

@ -1,4 +1,3 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
@ -16,22 +15,403 @@ Implementation Notes
**Hardware:**
.. todo:: Add links to any specific hardware product page(s), or category page(s).
Use unordered list & hyperlink rST inline format: "* `Link Text <url>`_"
* `Adafruit STSPIN220 Stepper Motor Driver Breakout Board <https://www.adafruit.com/product/6353>`_
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://circuitpython.org/downloads
.. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies
based on the library's use of either.
# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
"""
# imports
import time
from digitalio import DigitalInOut, Direction, Pull
from micropython import const
try:
from typing import Literal, Optional
except ImportError:
from typing_extensions import Literal
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_STSPIN.git"
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_STSPIN220.git"
# Timing characteristics
TOFF_MIN_US = const(9) # Minimum OFF time with ROFF=10kΩ (μs)
TOFF_MAX_US = const(125) # Maximum OFF time with ROFF=160kΩ (μs)
STCK_MIN_PULSE_NS = const(100) # Minimum STCK pulse width (ns)
DIR_SETUP_TIME_NS = const(100) # DIR input setup time (ns)
DIR_HOLD_TIME_NS = const(100) # DIR input hold time (ns)
STCK_MAX_FREQ_MHZ = const(1) # Maximum STCK frequency (MHz)
class Modes:
"""Valid microstepping modes for STSPIN220.
Each mode value encodes the state of MODE1-MODE4 pins:
- Bits 0-1: MODE1, MODE2 (physical pins if connected)
- Bits 2-3: MODE3, MODE4 (multiplexed with STEP/DIR during reset)
"""
STEP_FULL = const(0b0000) # Full step mode
STEP_1_2 = const(0b0101) # 1/2 step mode
STEP_1_4 = const(0b1010) # 1/4 step mode
STEP_1_8 = const(0b0111) # 1/8 step mode
STEP_1_16 = const(0b1111) # 1/16 step mode (default)
STEP_1_32 = const(0b0010) # 1/32 step mode
STEP_1_64 = const(0b1011) # 1/64 step mode
STEP_1_128 = const(0b0001) # 1/128 step mode
STEP_1_256 = const(0b0011) # 1/256 step mode
_MICROSTEPS = {
STEP_FULL: 1,
STEP_1_2: 2,
STEP_1_4: 4,
STEP_1_8: 8,
STEP_1_16: 16,
STEP_1_32: 32,
STEP_1_64: 64,
STEP_1_128: 128,
STEP_1_256: 256,
}
@classmethod
def microsteps(cls, mode: int) -> int:
"""Get number of microsteps for a given mode.
:param int mode: Step mode constant
:return: Number of microsteps per full step
:rtype: int
"""
return cls._MICROSTEPS.get(mode, 16) # Default to 16
@classmethod
def is_valid(cls, mode: int) -> bool:
"""Check if a mode value is valid.
:param int mode: Mode value to check
:return: True if valid mode
:rtype: bool
"""
return mode in cls._MICROSTEPS
class STSPIN220:
"""Driver for the STSPIN220 Low Voltage Stepper Motor Driver.
:param ~microcontroller.Pin step_pin: The pin connected to STEP (step clock) input
:param ~microcontroller.Pin dir_pin: The pin connected to DIR (direction) input
:param int steps_per_revolution: Number of steps per full motor revolution
:param ~microcontroller.Pin mode1_pin: The pin connected to MODE1 input (optional)
:param ~microcontroller.Pin mode2_pin: The pin connected to MODE2 input (optional)
:param ~microcontroller.Pin en_fault_pin: The pin connected to EN/FAULT pin (optional)
:param ~microcontroller.Pin stby_reset_pin: The pin connected to STBY/RESET pin (optional)
"""
def __init__( # noqa: PLR0913, PLR0917
self,
step_pin,
dir_pin,
steps_per_revolution: int = 200,
mode1_pin=None,
mode2_pin=None,
en_fault_pin=None,
stby_reset_pin=None,
) -> None:
# Initialize pins
self._step_pin = DigitalInOut(step_pin)
self._step_pin.direction = Direction.OUTPUT
self._step_pin.value = True
self._dir_pin = DigitalInOut(dir_pin)
self._dir_pin.direction = Direction.OUTPUT
self._dir_pin.value = True
# Optional pins
self._mode1_pin = None
self._mode2_pin = None
if mode1_pin is not None:
self._mode1_pin = DigitalInOut(mode1_pin)
self._mode1_pin.direction = Direction.OUTPUT
self._mode1_pin.value = True
if mode2_pin is not None:
self._mode2_pin = DigitalInOut(mode2_pin)
self._mode2_pin.direction = Direction.OUTPUT
self._mode2_pin.value = True
self._en_fault_pin = None
if en_fault_pin is not None:
self._en_fault_pin = DigitalInOut(en_fault_pin)
self._en_fault_pin.direction = Direction.INPUT
self._en_fault_pin.pull = Pull.UP
self._stby_reset_pin = None
if stby_reset_pin is not None:
self._stby_reset_pin = DigitalInOut(stby_reset_pin)
self._stby_reset_pin.direction = Direction.OUTPUT
self._stby_reset_pin.value = True
# Motor parameters
self._steps_per_revolution = steps_per_revolution
self._step_delay = 0.001 # Default 1ms between steps
self._step_number = 0
self._last_step_time = 0
self._step_mode = Modes.STEP_1_16 # Default to 1/16 microstepping
self._enabled = True
# Set initial step mode if mode pins are available
if self._mode1_pin and self._mode2_pin:
mode_bits = self._step_mode
self._mode1_pin.value = bool(mode_bits & 0x01)
self._mode2_pin.value = bool(mode_bits & 0x02)
@property
def speed(self) -> float:
"""Motor speed in revolutions per minute (RPM).
:return: Current speed in RPM
:rtype: float
"""
if self._step_delay >= 1.0:
return 0.0
microsteps = self.microsteps_per_step
steps_per_second = 1.0 / self._step_delay
steps_per_minute = steps_per_second * 60.0
rpm = steps_per_minute / (self._steps_per_revolution * microsteps)
return rpm
@speed.setter
def speed(self, rpm: float) -> None:
"""Set motor speed in revolutions per minute (RPM).
:param float rpm: Desired speed in RPM (must be positive)
"""
if rpm <= 0:
self._step_delay = 1.0
else:
# Account for microstepping
microsteps = self.microsteps_per_step
steps_per_minute = rpm * self._steps_per_revolution * microsteps
steps_per_second = steps_per_minute / 60.0
self._step_delay = 1.0 / steps_per_second
# Enforce minimum step delay (1 MHz max frequency = 1 μs minimum)
if self._step_delay < 0.000001:
self._step_delay = 0.000001
@property
def step_mode(self) -> int:
"""Current microstepping mode.
:return: Current step mode constant from Modes class
:rtype: int
"""
return self._step_mode
@step_mode.setter
def step_mode(self, mode: int) -> None:
"""Set the microstepping mode.
:param int mode: Step mode constant from Modes class (e.g., Modes.STEP_1_16)
:raises ValueError: If mode is invalid or cannot be set with available pins
"""
if not Modes.is_valid(mode):
raise ValueError(f"Invalid step mode: {mode}")
if self._stby_reset_pin is None:
raise ValueError("Cannot set step mode without STBY/RESET pin")
mode_bits = mode
# Check if we can set this mode with available pins
if (self._mode1_pin is None) or (self._mode2_pin is None):
# If mode1/mode2 pins not available, only allow modes where those bits are high
if (mode_bits & 0x01) == 0 or (mode_bits & 0x02) == 0:
raise ValueError(
"Cannot set mode requiring low MODE1/MODE2 without those pins connected"
)
# Put device into standby/reset
self._stby_reset_pin.value = False
time.sleep(0.001) # 1 ms
# Set all available mode pins (MODE1, MODE2, STEP/MODE3, DIR/MODE4)
if self._mode1_pin is not None:
self._mode1_pin.value = bool(mode_bits & 0x01)
if self._mode2_pin is not None:
self._mode2_pin.value = bool(mode_bits & 0x02)
self._step_pin.value = bool(mode_bits & 0x04)
self._dir_pin.value = bool(mode_bits & 0x08)
# Come out of standby to latch the mode
self._stby_reset_pin.value = True
self._step_mode = mode
@property
def microsteps_per_step(self) -> int:
"""Number of microsteps per full step for current mode.
:return: Microsteps per full step (1, 2, 4, 8, 16, 32, 64, 128, or 256)
:rtype: int
"""
return Modes.microsteps(self._step_mode)
@property
def fault(self) -> bool:
"""Check if a fault condition exists.
:return: True if fault detected, False if normal operation
:rtype: bool
"""
if self._en_fault_pin is None:
return False
return not self._en_fault_pin.value
@property
def position(self) -> int:
"""Current motor position in steps (0 to steps_per_revolution-1).
:return: Current position in steps
:rtype: int
"""
return self._step_number
def step(self, steps: int) -> None:
"""Move the motor a specified number of steps.
:param int steps: Number of steps to move (positive = forward, negative = reverse)
"""
steps_left = abs(steps)
self._dir_pin.value = steps > 0
time.sleep(0.000001) # 1 μs setup time
while steps_left > 0:
now = time.monotonic()
if (now - self._last_step_time) >= self._step_delay:
self._step()
if steps > 0:
self._step_number += 1
if self._step_number >= self._steps_per_revolution:
self._step_number = 0
elif self._step_number == 0:
self._step_number = self._steps_per_revolution - 1
else:
self._step_number -= 1
steps_left -= 1
self._last_step_time = now
def _step(self) -> None:
"""Perform a single step pulse."""
self._step_pin.value = False
time.sleep(0.000001) # 1 μs pulse width
self._step_pin.value = True
def step_blocking(self, steps: int, delay_seconds: float = 0.001) -> None:
"""Move the motor with blocking delay between steps.
:param int steps: Number of steps to move (positive = forward, negative = reverse)
:param float delay_seconds: Delay between steps in seconds
"""
steps_left = abs(steps)
self._dir_pin.value = steps > 0
time.sleep(0.000001) # 1 μs setup time
for _ in range(steps_left):
self._step()
time.sleep(delay_seconds)
@property
def enabled(self) -> bool:
"""Motor power stage enable state.
:return: True if enabled, False if disabled
:rtype: bool
"""
if self._en_fault_pin is None:
return True # If no enable pin, assume enabled
return self._enabled
@enabled.setter
def enabled(self, state: bool) -> None:
"""Enable or disable the motor power stage.
:param bool state: True to enable, False to disable
"""
if self._en_fault_pin is None:
return
# Ensure pin is configured as output
if self._en_fault_pin.direction != Direction.OUTPUT:
self._en_fault_pin.direction = Direction.OUTPUT
# Set pin high to enable, low to disable
self._en_fault_pin.value = state
self._enabled = state
@property
def standby(self) -> bool:
"""Device standby state.
:return: True if in standby mode, False if active
:rtype: bool
"""
if self._stby_reset_pin is None:
return False # If no standby pin, assume active
return not self._stby_reset_pin.value # Low = standby, High = active
@standby.setter
def standby(self, state: bool) -> None:
"""Put the device into standby mode or wake it up.
:param bool state: True to enter standby (ultra-low power), False to wake up
"""
if self._stby_reset_pin is None:
return
if state:
# Going into standby/reset - pull pin low
self._stby_reset_pin.value = False
else:
# Coming out of standby/reset - pull pin high
self._stby_reset_pin.value = True
# After waking up, we need to restore the step mode
# by reconfiguring the MODE pins
if hasattr(self, "_step_mode"):
# Re-apply the current step mode
self.step_mode = self._step_mode
def clear_fault(self) -> None:
"""Clear fault condition by toggling enable pin."""
if self._en_fault_pin is None:
return
# Ensure we're in output mode to control the pin
self._en_fault_pin.direction = Direction.OUTPUT
# Toggle the pin low then high to clear the fault
self._en_fault_pin.value = False
time.sleep(0.001) # 1 ms
self._en_fault_pin.value = True
self._enabled = True
def reset(self) -> None:
"""Reset the device by toggling the STBY/RESET pin."""
if self._stby_reset_pin is None:
return
self.standby(True)
time.sleep(0.001) # 1 ms
self.standby(False)

View file

@ -25,7 +25,7 @@ extensions = [
# Uncomment the below if you use native CircuitPython modules such as
# digitalio, micropython and busio. List the modules you use. Without it, the
# autodoc module docs will fail to generate with a warning.
# autodoc_mock_imports = ["digitalio", "busio"]
autodoc_mock_imports = ["digitalio"]
autodoc_preserve_defaults = True

View file

@ -6,3 +6,12 @@ Ensure your device works with this simple test.
.. literalinclude:: ../examples/stspin_simpletest.py
:caption: examples/stspin_simpletest.py
:linenos:
Microstep mode test
--------------------
Cycle through setting the different microstep modes
.. literalinclude:: ../examples/stspin_microsteps.py
:caption: examples/stspin_microsteps.py
:linenos:

View file

@ -0,0 +1,113 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Microstepping mode test for the Adafruit STSPIN220 stepper motor driver.
"""
import time
import board
import adafruit_stspin220
# Define the number of steps per revolution for your stepper motor
# Most steppers are 200 steps per revolution (1.8 degrees per step)
STEPS_PER_REVOLUTION = 200
STEP_PIN = board.D5 # Step clock pin
DIR_PIN = board.D6 # Direction pin
MODE1_PIN = board.D9 # Mode 1 pin (REQUIRED for mode switching)
MODE2_PIN = board.D10 # Mode 2 pin (REQUIRED for mode switching)
EN_FAULT_PIN = board.D11 # Enable/Fault pin (optional)
STBY_RESET_PIN = board.D12 # Standby/Reset pin (REQUIRED for mode switching)
print("Initializing STSPIN220...")
motor = adafruit_stspin220.STSPIN220(
STEP_PIN,
DIR_PIN,
STEPS_PER_REVOLUTION,
mode1_pin=MODE1_PIN,
mode2_pin=MODE2_PIN,
en_fault_pin=EN_FAULT_PIN,
stby_reset_pin=STBY_RESET_PIN
)
print("Adafruit STSPIN220 Microstepping Mode Test")
print("=" * 50)
# Define all available modes with their names for display
MODES = [
(adafruit_stspin220.Modes.STEP_FULL, "Full Step"),
(adafruit_stspin220.Modes.STEP_1_2, "1/2 Step"),
(adafruit_stspin220.Modes.STEP_1_4, "1/4 Step"),
(adafruit_stspin220.Modes.STEP_1_8, "1/8 Step"),
(adafruit_stspin220.Modes.STEP_1_16, "1/16 Step"),
(adafruit_stspin220.Modes.STEP_1_32, "1/32 Step"),
(adafruit_stspin220.Modes.STEP_1_64, "1/64 Step"),
(adafruit_stspin220.Modes.STEP_1_128, "1/128 Step"),
(adafruit_stspin220.Modes.STEP_1_256, "1/256 Step"),
]
BASE_RPM = 30 # Base speed for full step mode
print(f"Base speed: {BASE_RPM} RPM (for full step mode)")
print("Speed will be adjusted for each mode to maintain similar rotation time")
print("=" * 50)
time.sleep(2.0)
while True:
for mode, mode_name in MODES:
print(f"\nTesting {mode_name} mode...")
try:
# Set the microstepping mode
motor.step_mode = mode
# Get the number of microsteps for this mode
microsteps = motor.microsteps_per_step
# Adjust speed to maintain similar rotation time across all modes
# More microsteps = need higher RPM to maintain same angular velocity
adjusted_rpm = BASE_RPM * (microsteps / 1.0) # Normalized to full step
motor.speed = adjusted_rpm
# Calculate total steps needed for one full revolution
total_steps = STEPS_PER_REVOLUTION * microsteps
print(f" Microsteps per full step: {microsteps}")
print(f" Adjusted speed: {adjusted_rpm:.1f} RPM")
print(f" Steps for full revolution: {total_steps}")
# Check for any faults before moving
if motor.fault:
print(" WARNING: Fault detected! Clearing...")
motor.clear_fault()
time.sleep(0.1)
# Perform one full revolution forward
print(f" Rotating forward 360°...")
start_time = time.monotonic()
motor.step(total_steps)
rotation_time = time.monotonic() - start_time
print(f" Rotation completed in {rotation_time:.2f} seconds")
# Brief pause to see the position
time.sleep(0.5)
# Return to starting position
print(f" Returning to start position...")
motor.step(-total_steps)
print(f" {mode_name} test complete!")
except ValueError as e:
print(f" ERROR: Could not set {mode_name} mode - {e}")
print(" Make sure MODE1, MODE2, and STBY/RESET pins are connected!")
# Pause between modes
time.sleep(1.0)
print("\n" + "=" * 50)
print("All modes tested! Starting next cycle in 3 seconds...")
print("=" * 50)
time.sleep(3.0)

View file

@ -1,4 +1,38 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
# SPDX-License-Identifier: MIT
"""
Basic example for the Adafruit STSPIN220 stepper motor driver library.
"""
import time
import adafruit_stspin220
import board
STEPS_PER_REVOLUTION = 200
STEP_PIN = board.D5
DIR_PIN = board.D6
# Create stepper object with full pin configuration
# Defaults to 1/16 microsteps
motor = adafruit_stspin220.STSPIN220(STEP_PIN, DIR_PIN, STEPS_PER_REVOLUTION)
# Set the speed to 60 RPM
motor.speed = 60
print(f"Microstepping mode set to 1/{motor.microsteps_per_step} at {motor.speed} RPM")
while True:
# Calculate total microsteps for one full revolution
total_microsteps = STEPS_PER_REVOLUTION * motor.microsteps_per_step
print(f"Stepping forward one revolution ({total_microsteps} microsteps)...")
motor.step(total_microsteps)
time.sleep(1.0)
print(f"Stepping backward one revolution ({total_microsteps} microsteps)...")
motor.step(-total_microsteps)
time.sleep(1.0)