diff --git a/adafruit_motor/stepper.py b/adafruit_motor/stepper.py old mode 100644 new mode 100755 index 5873cec..5bcd35f --- a/adafruit_motor/stepper.py +++ b/adafruit_motor/stepper.py @@ -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). @@ -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. @@ -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 diff --git a/examples/motor_servo_digitalio.py b/examples/motor_servo_digitalio.py new file mode 100755 index 0000000..7668767 --- /dev/null +++ b/examples/motor_servo_digitalio.py @@ -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()