Adafruit_CircuitPython_STSPIN/adafruit_stspin.py
2025-06-16 10:05:58 -04:00

420 lines
14 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_stspin`
================================================================================
CircuitPython driver for the Adafruit STSPIN220 Stepper Motor Driver Breakout Board
* Author(s): Liz Clark
Implementation Notes
--------------------
**Hardware:**
* `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
"""
import time
from digitalio import DigitalInOut, Direction, Pull
from micropython import const
try:
from typing import Literal, Optional
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_STSPIN.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 STSPIN:
"""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
start_time = time.monotonic()
target_time = start_time
for i in range(steps_left):
target_time = start_time + (i * self._step_delay)
now = time.monotonic()
wait_time = target_time - now
if wait_time > 0:
time.sleep(wait_time)
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
self._last_step_time = time.monotonic()
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)