diff --git a/README.rst b/README.rst index 2c3c496..116ead7 100644 --- a/README.rst +++ b/README.rst @@ -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 ============= diff --git a/adafruit_stspin.py b/adafruit_stspin.py index 34abdba..037ee32 100644 --- a/adafruit_stspin.py +++ b/adafruit_stspin.py @@ -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 `_" +* `Adafruit STSPIN220 Stepper Motor Driver Breakout Board `_ **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) diff --git a/docs/conf.py b/docs/conf.py index d3aa98c..9202b8f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 diff --git a/docs/examples.rst b/docs/examples.rst index 66f71d3..92cf4ea 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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: diff --git a/examples/stspin_microsteps.py b/examples/stspin_microsteps.py new file mode 100644 index 0000000..febe8ab --- /dev/null +++ b/examples/stspin_microsteps.py @@ -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) \ No newline at end of file diff --git a/examples/stspin_simpletest.py b/examples/stspin_simpletest.py index 42772ff..8518e25 100644 --- a/examples/stspin_simpletest.py +++ b/examples/stspin_simpletest.py @@ -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)