|
| 1 | +# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries |
| 2 | +# SPDX-FileCopyrightText: Copyright (c) 2023 Jose D. Montoya |
| 3 | +# |
| 4 | +# SPDX-License-Identifier: MIT |
| 5 | + |
| 6 | +""" |
| 7 | +`stepper` |
| 8 | +==================================================== |
| 9 | +
|
| 10 | +MicroPython Helper for controlling stepper motors with microstepping support. |
| 11 | +
|
| 12 | +
|
| 13 | +* Author(s): Tony DiCola, Scott Shawcroft, Jose D. Montoya |
| 14 | +""" |
| 15 | + |
| 16 | +import math |
| 17 | +from micropython import const |
| 18 | + |
| 19 | + |
| 20 | +# Constants that specify the direction and style of steps. |
| 21 | +FORWARD = const(1) |
| 22 | +"""Step forward""" |
| 23 | +BACKWARD = const(2) |
| 24 | +""""Step backward""" |
| 25 | +SINGLE = const(1) |
| 26 | +"""Step so that each step only activates a single coil""" |
| 27 | +DOUBLE = const(2) |
| 28 | +"""Step so that each step only activates two coils to produce more torque.""" |
| 29 | +INTERLEAVE = const(3) |
| 30 | +"""Step half a step to alternate between single coil and double coil steps.""" |
| 31 | +MICROSTEP = const(4) |
| 32 | +"""Step a fraction of a step by partially activating two neighboring coils. Step size is determined |
| 33 | + by ``microsteps`` constructor argument.""" |
| 34 | + |
| 35 | +_SINGLE_STEPS = bytes([0b0010, 0b0100, 0b0001, 0b1000]) |
| 36 | + |
| 37 | +_DOUBLE_STEPS = bytes([0b1010, 0b0110, 0b0101, 0b1001]) |
| 38 | + |
| 39 | +_INTERLEAVE_STEPS = bytes( |
| 40 | + [0b1010, 0b0010, 0b0110, 0b0100, 0b0101, 0b0001, 0b1001, 0b1000] |
| 41 | +) |
| 42 | + |
| 43 | + |
| 44 | +class StepperMotor: |
| 45 | + """A bipolar stepper motor or four coil unipolar motor. The use of microstepping requires |
| 46 | + pins that can output PWM. For non-microstepping, can set microsteps to None and use |
| 47 | + digital out pins. |
| 48 | +
|
| 49 | +
|
| 50 | + :param ain1: `machine.PWM` or `machine.Pin`-compatible output connected to the driver for |
| 51 | + the first coil (unipolar) or first input to first coil (bipolar). |
| 52 | + :param ain2: `machine.PWM` or `machine.Pin`-compatible output connected to the driver for |
| 53 | + the third coil (unipolar) or second input to first coil (bipolar). |
| 54 | + :param bin1: `machine.PWM` or `machine.Pin`-compatible output connected to the driver for |
| 55 | + the second coil (unipolar) or second input to second coil (bipolar). |
| 56 | + :param bin2: `machine.PWM` or `machine.Pin`-compatible output connected to the driver for |
| 57 | + the fourth coil (unipolar) or second input to second coil (bipolar). |
| 58 | + :param int microsteps: Number of microsteps between full steps. Must be at least 2 and even. |
| 59 | +
|
| 60 | + """ |
| 61 | + |
| 62 | + def __init__(self, ain1, ain2, bin1, bin2, *, microsteps=16) -> None: |
| 63 | + if microsteps is None: |
| 64 | + # Digital IO Pins |
| 65 | + self._steps = None |
| 66 | + self._coil = (ain1, ain2, bin1, bin2) |
| 67 | + else: |
| 68 | + # PWM Pins set a safe pwm freq for each output |
| 69 | + self._coil = (ain2, bin1, ain1, bin2) |
| 70 | + for i in range(4): |
| 71 | + if self._coil[i].freq() < 1500: |
| 72 | + try: |
| 73 | + self._coil[i].frequency = 2000 |
| 74 | + except AttributeError as err: |
| 75 | + raise ValueError( |
| 76 | + "PWMOut outputs must either be set to at least " |
| 77 | + "1500 Hz or allow variable frequency." |
| 78 | + ) from err |
| 79 | + if microsteps < 2: |
| 80 | + raise ValueError("Microsteps must be at least 2") |
| 81 | + if microsteps % 2 == 1: |
| 82 | + raise ValueError("Microsteps must be even") |
| 83 | + self._curve = [ |
| 84 | + int(round(0xFFFF * math.sin(math.pi / (2 * microsteps) * i))) |
| 85 | + for i in range(microsteps + 1) |
| 86 | + ] |
| 87 | + self._current_microstep = 0 |
| 88 | + self._microsteps = microsteps |
| 89 | + self._update_coils() |
| 90 | + |
| 91 | + def _update_coils(self, *, microstepping: bool = False) -> None: |
| 92 | + if self._microsteps is None: |
| 93 | + # Digital IO Pins Get coil activation sequence |
| 94 | + if self._steps is None: |
| 95 | + steps = 0b0000 |
| 96 | + else: |
| 97 | + steps = self._steps[self._current_microstep % len(self._steps)] |
| 98 | + # Energize coils as appropriate: |
| 99 | + for i, coil in enumerate(self._coil): |
| 100 | + coil.value((steps >> i) & 0x01) |
| 101 | + else: |
| 102 | + # PWM Pins |
| 103 | + duty_cycles = [0, 0, 0, 0] |
| 104 | + trailing_coil = (self._current_microstep // self._microsteps) % 4 |
| 105 | + leading_coil = (trailing_coil + 1) % 4 |
| 106 | + microstep = self._current_microstep % self._microsteps |
| 107 | + duty_cycles[leading_coil] = self._curve[microstep] |
| 108 | + duty_cycles[trailing_coil] = self._curve[self._microsteps - microstep] |
| 109 | + |
| 110 | + # This ensures DOUBLE steps use full torque. Without it, we'd use |
| 111 | + # partial torque from the microstepping curve (0xb504). |
| 112 | + if not microstepping and ( |
| 113 | + duty_cycles[leading_coil] == duty_cycles[trailing_coil] |
| 114 | + and duty_cycles[leading_coil] > 0 |
| 115 | + ): |
| 116 | + duty_cycles[leading_coil] = 0xFFFF |
| 117 | + duty_cycles[trailing_coil] = 0xFFFF |
| 118 | + |
| 119 | + # Energize coils as appropriate: |
| 120 | + for i in range(4): |
| 121 | + self._coil[i].duty_u16(duty_cycles[i]) |
| 122 | + |
| 123 | + def release(self) -> None: |
| 124 | + """Releases all the coils so the motor can free spin, also won't use any power""" |
| 125 | + # De-energize coils: |
| 126 | + for coil in self._coil: |
| 127 | + if self._microsteps is None: |
| 128 | + coil.value(0) |
| 129 | + else: |
| 130 | + coil.duty_u16(0) |
| 131 | + |
| 132 | + def onestep(self, *, direction: int = FORWARD, style: int = SINGLE) -> None: |
| 133 | + """Performs one step of a particular style. The actual rotation amount will vary by style. |
| 134 | + `SINGLE` and `DOUBLE` will normal cause a full step rotation. `INTERLEAVE` will normally |
| 135 | + do a half step rotation. `MICROSTEP` will perform the smallest configured step. |
| 136 | +
|
| 137 | + When step styles are mixed, subsequent `SINGLE`, `DOUBLE` or `INTERLEAVE` steps may be |
| 138 | + less than normal in order to align to the desired style's pattern. |
| 139 | +
|
| 140 | + :param int direction: Either `FORWARD` or `BACKWARD` |
| 141 | + :param int style: `SINGLE`, `DOUBLE`, `INTERLEAVE`""" |
| 142 | + if self._microsteps is None: |
| 143 | + # Digital IO Pins |
| 144 | + step_size = 1 |
| 145 | + if style == SINGLE: |
| 146 | + self._steps = _SINGLE_STEPS |
| 147 | + elif style == DOUBLE: |
| 148 | + self._steps = _DOUBLE_STEPS |
| 149 | + elif style == INTERLEAVE: |
| 150 | + self._steps = _INTERLEAVE_STEPS |
| 151 | + else: |
| 152 | + raise ValueError("Unsupported step style.") |
| 153 | + else: |
| 154 | + # PWM Pins Adjust current steps based on the direction and type of step. |
| 155 | + step_size = 0 |
| 156 | + if style == MICROSTEP: |
| 157 | + step_size = 1 |
| 158 | + else: |
| 159 | + half_step = self._microsteps // 2 |
| 160 | + full_step = self._microsteps |
| 161 | + # Its possible the previous steps were MICROSTEPS so first align |
| 162 | + # with the interleave pattern. |
| 163 | + additional_microsteps = self._current_microstep % half_step |
| 164 | + if additional_microsteps != 0: |
| 165 | + # We set _current_microstep directly because our step size varies |
| 166 | + # depending on the direction. |
| 167 | + if direction == FORWARD: |
| 168 | + self._current_microstep += half_step - additional_microsteps |
| 169 | + else: |
| 170 | + self._current_microstep -= additional_microsteps |
| 171 | + step_size = 0 |
| 172 | + elif style == INTERLEAVE: |
| 173 | + step_size = half_step |
| 174 | + |
| 175 | + current_interleave = self._current_microstep // half_step |
| 176 | + if (style == SINGLE and current_interleave % 2 == 1) or ( |
| 177 | + style == DOUBLE and current_interleave % 2 == 0 |
| 178 | + ): |
| 179 | + step_size = half_step |
| 180 | + elif style in (SINGLE, DOUBLE): |
| 181 | + step_size = full_step |
| 182 | + |
| 183 | + if direction == FORWARD: |
| 184 | + self._current_microstep += step_size |
| 185 | + else: |
| 186 | + self._current_microstep -= step_size |
| 187 | + |
| 188 | + # Now that we know our target microstep we can determine how to energize the four coils. |
| 189 | + self._update_coils(microstepping=style == MICROSTEP) |
| 190 | + |
| 191 | + return self._current_microstep |
0 commit comments