Skip to content

Add digitalio for steppers #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 135 additions & 65 deletions adafruit_motor/stepper.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,21 @@
"""Step a fraction of a step by partially activating two neighboring coils. Step size is determined
by ``microsteps`` constructor argument."""

_SINGLE_STEPS = bytes([0b0010, 0b0100, 0b0001, 0b1000])

_DOUBLE_STEPS = bytes([0b1010, 0b0110, 0b0101, 0b1001])

_INTERLEAVE_STEPS = bytes(
[0b1010, 0b0010, 0b0110, 0b0100, 0b0101, 0b0001, 0b1001, 0b1000]
)


class StepperMotor:
"""A bipolar stepper motor or four coil unipolar motor.
"""A bipolar stepper motor or four coil unipolar motor. The use of microstepping requires
pins that can output PWM. For non-microstepping, can set microsteps to None and use
digital out pins.

**PWM**

:param ~pulseio.PWMOut ain1: `pulseio.PWMOut`-compatible output connected to the driver for
the first coil (unipolar) or first input to first coil (bipolar).
Expand All @@ -70,56 +82,97 @@ class StepperMotor:
:param ~pulseio.PWMOut bin2: `pulseio.PWMOut`-compatible output connected to the driver for
the fourth coil (unipolar) or second input to second coil (bipolar).
:param int microsteps: Number of microsteps between full steps. Must be at least 2 and even.

**Digital Out**

:param ~digitalio.DigitalInOut ain1: `digitalio.DigitalInOut`-compatible output connected to
the driver for the first coil (unipolar) or first input to first coil (bipolar).
:param ~digitalio.DigitalInOut ain2: `digitalio.DigitalInOut`-compatible output connected to
the driver for the third coil (unipolar) or second input to first coil (bipolar).
:param ~digitalio.DigitalInOut bin1: `digitalio.DigitalInOut`-compatible output connected to
the driver for the second coil (unipolar) or first input to second coil (bipolar).
:param ~digitalio.DigitalInOut bin2: `digitalio.DigitalInOut`-compatible output connected to
the driver for the fourth coil (unipolar) or second input to second coil (bipolar).
:param microsteps: set to `None`
"""

def __init__(self, ain1, ain2, bin1, bin2, *, microsteps=16):
self._coil = (ain2, bin1, ain1, bin2)

# set a safe pwm freq for each output
for i in range(4):
if self._coil[i].frequency < 1500:
self._coil[i].frequency = 2000

if microsteps is None:
#
# Digital IO Pins
#
self._steps = None
self._coil = (ain1, ain2, bin1, bin2)
else:
#
# PWM Pins
#
# set a safe pwm freq for each output
self._coil = (ain2, bin1, ain1, bin2)
for i in range(4):
if self._coil[i].frequency < 1500:
self._coil[i].frequency = 2000
if microsteps < 2:
raise ValueError("Microsteps must be at least 2")
if microsteps % 2 == 1:
raise ValueError("Microsteps must be even")
self._curve = [
int(round(0xFFFF * math.sin(math.pi / (2 * microsteps) * i)))
for i in range(microsteps + 1)
]
self._current_microstep = 0
if microsteps < 2:
raise ValueError("Microsteps must be at least 2")
if microsteps % 2 == 1:
raise ValueError("Microsteps must be even")
self._microsteps = microsteps
self._curve = [
int(round(0xFFFF * math.sin(math.pi / (2 * microsteps) * i)))
for i in range(microsteps + 1)
]
self._update_coils()

def _update_coils(self, *, microstepping=False):
duty_cycles = [0, 0, 0, 0]
trailing_coil = (self._current_microstep // self._microsteps) % 4
leading_coil = (trailing_coil + 1) % 4
microstep = self._current_microstep % self._microsteps
duty_cycles[leading_coil] = self._curve[microstep]
duty_cycles[trailing_coil] = self._curve[self._microsteps - microstep]

# This ensures DOUBLE steps use full torque. Without it, we'd use partial torque from the
# microstepping curve (0xb504).
if not microstepping and (
duty_cycles[leading_coil] == duty_cycles[trailing_coil]
and duty_cycles[leading_coil] > 0
):
duty_cycles[leading_coil] = 0xFFFF
duty_cycles[trailing_coil] = 0xFFFF

# Energize coils as appropriate:
for i in range(4):
self._coil[i].duty_cycle = duty_cycles[i]
if self._microsteps is None:
#
# Digital IO Pins
#
# Get coil activation sequence
if self._steps is None:
steps = 0b0000
else:
steps = self._steps[self._current_microstep % len(self._steps)]
# Energize coils as appropriate:
for i, coil in enumerate(self._coil):
coil.value = (steps >> i) & 0x01
else:
#
# PWM Pins
#
duty_cycles = [0, 0, 0, 0]
trailing_coil = (self._current_microstep // self._microsteps) % 4
leading_coil = (trailing_coil + 1) % 4
microstep = self._current_microstep % self._microsteps
duty_cycles[leading_coil] = self._curve[microstep]
duty_cycles[trailing_coil] = self._curve[self._microsteps - microstep]

# This ensures DOUBLE steps use full torque. Without it, we'd use
# partial torque from the microstepping curve (0xb504).
if not microstepping and (
duty_cycles[leading_coil] == duty_cycles[trailing_coil]
and duty_cycles[leading_coil] > 0
):
duty_cycles[leading_coil] = 0xFFFF
duty_cycles[trailing_coil] = 0xFFFF

# Energize coils as appropriate:
for i in range(4):
self._coil[i].duty_cycle = duty_cycles[i]

def release(self):
"""Releases all the coils so the motor can free spin, also won't use any power"""
# De-energize coils:
for i in range(4):
self._coil[i].duty_cycle = 0

def onestep(self, *, direction=FORWARD, style=SINGLE):
for coil in self._coil:
if self._microsteps is None:
coil.value = 0
else:
coil.duty_cycle = 0

def onestep(
self, *, direction=FORWARD, style=SINGLE
): # pylint: disable=too-many-branches
"""Performs one step of a particular style. The actual rotation amount will vary by style.
`SINGLE` and `DOUBLE` will normal cause a full step rotation. `INTERLEAVE` will normally
do a half step rotation. `MICROSTEP` will perform the smallest configured step.
Expand All @@ -129,34 +182,51 @@ def onestep(self, *, direction=FORWARD, style=SINGLE):

:param int direction: Either `FORWARD` or `BACKWARD`
:param int style: `SINGLE`, `DOUBLE`, `INTERLEAVE`"""
# Adjust current steps based on the direction and type of step.
step_size = 0
if style == MICROSTEP:
if self._microsteps is None:
#
# Digital IO Pins
#
step_size = 1
else:
half_step = self._microsteps // 2
full_step = self._microsteps
# Its possible the previous steps were MICROSTEPS so first align with the interleave
# pattern.
additional_microsteps = self._current_microstep % half_step
if additional_microsteps != 0:
# We set _current_microstep directly because our step size varies depending on the
# direction.
if direction == FORWARD:
self._current_microstep += half_step - additional_microsteps
else:
self._current_microstep -= additional_microsteps
step_size = 0
if style == SINGLE:
self._steps = _SINGLE_STEPS
elif style == DOUBLE:
self._steps = _DOUBLE_STEPS
elif style == INTERLEAVE:
step_size = half_step

current_interleave = self._current_microstep // half_step
if (style == SINGLE and current_interleave % 2 == 1) or (
style == DOUBLE and current_interleave % 2 == 0
):
step_size = half_step
elif style in (SINGLE, DOUBLE):
step_size = full_step
self._steps = _INTERLEAVE_STEPS
else:
raise ValueError("Unsupported step style.")
else:
#
# PWM Pins
#
# Adjust current steps based on the direction and type of step.
step_size = 0
if style == MICROSTEP:
step_size = 1
else:
half_step = self._microsteps // 2
full_step = self._microsteps
# Its possible the previous steps were MICROSTEPS so first align
# with the interleave pattern.
additional_microsteps = self._current_microstep % half_step
if additional_microsteps != 0:
# We set _current_microstep directly because our step size varies
# depending on the direction.
if direction == FORWARD:
self._current_microstep += half_step - additional_microsteps
else:
self._current_microstep -= additional_microsteps
step_size = 0
elif style == INTERLEAVE:
step_size = half_step

current_interleave = self._current_microstep // half_step
if (style == SINGLE and current_interleave % 2 == 1) or (
style == DOUBLE and current_interleave % 2 == 0
):
step_size = half_step
elif style in (SINGLE, DOUBLE):
step_size = full_step

if direction == FORWARD:
self._current_microstep += step_size
Expand Down
48 changes: 48 additions & 0 deletions examples/motor_servo_digitalio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Use this example for digital pin control of an H-bridge driver
# like a TB6612 or L298N.

import time
import board
import digitalio
from adafruit_motor import stepper

DELAY = 0.01
STEPS = 200

coils = (
digitalio.DigitalInOut(board.D19), # A1
digitalio.DigitalInOut(board.D26), # A2
digitalio.DigitalInOut(board.D20), # B1
digitalio.DigitalInOut(board.D21), # B2
)

for coil in coils:
coil.direction = digitalio.Direction.OUTPUT

motor = stepper.StepperMotor(coils[0], coils[1], coils[2], coils[3], microsteps=None)

for step in range(STEPS):
motor.onestep()
time.sleep(DELAY)

for step in range(STEPS):
motor.onestep(direction=stepper.BACKWARD)
time.sleep(DELAY)

for step in range(STEPS):
motor.onestep(style=stepper.DOUBLE)
time.sleep(DELAY)

for step in range(STEPS):
motor.onestep(direction=stepper.BACKWARD, style=stepper.DOUBLE)
time.sleep(DELAY)

for step in range(STEPS):
motor.onestep(style=stepper.INTERLEAVE)
time.sleep(DELAY)

for step in range(STEPS):
motor.onestep(direction=stepper.BACKWARD, style=stepper.INTERLEAVE)
time.sleep(DELAY)

motor.release()