Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add Stepper Motor PWM-Counter driver. #720

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions micropython/drivers/motion/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.mp4
*.png
*.jpg
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
## Stepper motor PWM-Counter driver

This MicroPython software driver is designed to control stepper motor using STEP/DIR hardware driver.

![stepper_motor_driver](https://github.com/IhorNehrutsa/micropython-lib/assets/70886343/27933a08-7225-4931-a1ee-8e0042d0b822)

* The driver signal "STEP" (aka "PULSE") is intended for clock pulses. In one pulse, the motor rotor turns one step. The higher the frequency of pulses, the higher the speed of rotation of the rotor.

* The driver signal "DIR" is intended to select the direction of rotation of the engine ("1" - in one direction, "0" - in the other direction).

![forward_reverse](https://github.com/IhorNehrutsa/micropython-lib/assets/70886343/f1986469-6fca-4d10-a6f2-262020a19946)

### Hardware

As an example of a STEP / DIR hardware driver:

* [TMC2209](https://wiki.fysetc.com/Silent2209) module, TB6612,

* [TB6560-V2](https://mypractic.com/stepper-motor-driver-tb6560-v2-description-characteristics-recommendations-for-use) module,

* [TB6600](https://mytectutor.com/tb6600-stepper-motor-driver-with-arduino) based driver,

* DM860H, DM556 etc.

### Software

The main feature of this driver is that the generation and counting of pulses are performed by hardware, which frees up time in the main loop. PWM will start pulses and Counter will stop pulses in irq handler.

The PWM unit creates STEP pulses and sets the motor speed.

The GPIO unit controls the DIR pin, the direction of rotation of the motor.

The Counter unit counts pulses, that is, the actual position of the stepper motor.

![stepper_motor_pwm_counter](https://github.com/IhorNehrutsa/micropython-lib/assets/70886343/4e6cf4b9-b198-4fa6-8bcc-51d873bf74ce)

In general case MicroPython ports need 4 pins: PWM STEP output, GPIO DIR output, Counter STEP input, Counter DIR input (red wires in the image).

The ESP32 port allows to connect Counter inputs to the same outputs inside the MCU(green wires in the picture), so 2 pins are needed.

This driver requires PR's:

[esp32/PWM: Reduce inconsitencies between ports. #10854](https://github.com/micropython/micropython/pull/10854)

[ESP32: Add Quadrature Encoder and Pulse Counter classes. #8766](https://github.com/micropython/micropython/pull/8766)

Constructor
-----------

class:: StepperMotorPwmCounter(pin_step, pin_dir, freq, reverse)

Construct and return a new StepperMotorPwmCounter object using the following parameters:

- *pin_step* is the entity on which the PWM is output, which is usually a
:ref:`machine.Pin <machine.Pin>` object, but a port may allow other values, like integers.
- *freq* should be an integer which sets the frequency in Hz for the
PWM cycle i.e. motor speed.
- *reverse* reverse the motor direction if the value is True

Properties
----------

property:: StepperMotorPwmCounter.freq

Get/set the current frequency of the STEP/PWM output.

property:: StepperMotorPwmCounter.steps_counter

Get current steps position form the Counter.

property:: StepperMotorPwmCounter.steps_target

Get/set the steps target.

Methods
-------

method:: StepperMotorPwmCounter.deinit()

Disable the PWM output.

method:: StepperMotorPwmCounter.go()

Call it in the main loop to move the motor to the steps_target position.

method:: StepperMotorPwmCounter.is_ready()

Return True if steps_target is achieved.

Tested on ESP32.

**Simple example is:**
```
# stepper_motor_pwm_counter_test1.py

from time import sleep

from stepper_motor_pwm_counter import StepperMotorPwmCounter

try:
motor = StepperMotorPwmCounter(26, 23, freq=10_000)
print(motor)

motor.steps_target = 8192
while True:
if not motor.is_ready():
motor.go()
print(f'motor.steps_target={motor.steps_target}, motor.steps_counter={motor.steps_counter}, motor.is_ready()={motor.is_ready()}')
else:
print()
print(f'motor.steps_target={motor.steps_target}, motor.steps_counter={motor.steps_counter}, motor.is_ready()={motor.is_ready()}')
print('SET steps_target', -motor.steps_target)
print('sleep(1)')
print()
sleep(1)
motor.steps_target = -motor.steps_target
motor.go()

sleep(0.1)

except Exception as e:
print(e)
raise e
finally:
try:
motor.deinit()
except:
pass
```

**Output is:**
```
StepMotorPWMCounter(pin_step=Pin(26), pin_dir=Pin(23), freq=10000, reverse=0,
pwm=PWM(Pin(26), freq=10000, duty_u16=0),
counter=Counter(0, src=Pin(26), direction=Pin(23), edge=Counter.RISING, filter_ns=0))
motor.steps_target=8192, motor.steps_counter=2, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=1025, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=2048, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=3071, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=4094, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=5117, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=6139, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=7162, motor.is_ready()=False
motor.steps_target=8192, motor.steps_counter=8185, motor.is_ready()=False
irq_handler: steps_over_run=6, counter.get_value()=8204

motor.steps_target=8192, motor.steps_counter=8204, motor.is_ready()=True
SET steps_target -8192
sleep(1)

motor.steps_target=-8192, motor.steps_counter=7200, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=6176, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=5153, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=4130, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=3107, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=2084, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=1061, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=37, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-986, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-2009, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-3032, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-4054, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-5077, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-6100, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-7123, motor.is_ready()=False
motor.steps_target=-8192, motor.steps_counter=-8146, motor.is_ready()=False
irq_handler: steps_over_run=4, counter.get_value()=-8188
motor.steps_target=-8192, motor.steps_counter=-8189, motor.is_ready()=False

motor.steps_target=-8192, motor.steps_counter=-9209, motor.is_ready()=True
SET steps_target 8192
sleep(1)

motor.steps_target=8192, motor.steps_counter=-8205, motor.is_ready()=False
Traceback (most recent call last):
File "<stdin>", line 25, in <module>
KeyboardInterrupt:
```

**Example with motor speed acceleration/deceleration:**
```
# stepper_motor_pwm_counter_test2.py

from time import sleep

from stepper_motor_pwm_counter import StepperMotorPwmCounter


try:
motor = StepperMotorPwmCounter(26, 23)
print(motor)

f_min = 3_000
f_max = 50_000
df = 1_000
motor.freq = f_min
motor_steps_start = motor.steps_counter
motor.steps_target = 8192 * 10
while True:
if not motor.is_ready():
motor.go()
else:
print()
print(f'motor.steps_target={motor.steps_target}, motor.steps_counter={motor.steps_counter}, motor.is_ready()={motor.is_ready()}')
print('SET steps_target', -motor.steps_target)
print('sleep(1)')
print()
sleep(1)
motor_steps_start = motor.steps_target
motor.steps_target = -motor.steps_target
motor.go()


m = min(abs(motor.steps_counter - motor_steps_start), abs(motor.steps_target - motor.steps_counter))
motor.freq = min(f_min + df * m // 1000, f_max)

sleep(0.1)

except Exception as e:
print(e)
raise e
finally:
try:
motor.deinit()
except:
pass

```
[Motor speed acceleration/deceleration video](https://drive.google.com/file/d/1HOkmqnaepOOmt4XUEJzPtQJNCVQrRUXs/view?usp=drive_link)
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from utime import ticks_diff, ticks_us, ticks_ms
from machine import Pin, PWM, Counter


class StepperMotorPwmCounter:
def __init__(self, pin_step, pin_dir, freq=5_000, reverse=0, counter=None, pwm=None):
if isinstance(pin_step, Pin):
self.pin_step = pin_step
else:
self.pin_step = Pin(pin_step, Pin.OUT)

if isinstance(pin_dir, Pin):
self.pin_dir = pin_dir
else:
self.pin_dir = Pin(pin_dir, Pin.OUT, value=0)

self.freq = freq

self.reverse = reverse # reverse the direction of movement of the motor

if isinstance(counter, Counter):
self._counter = counter
else:
self._counter = Counter(-1, src=pin_step, direction=pin_dir)

self._steps_target = 0
self._match = 0
self._steps_over_run = 0

self._direction = 0 # the current direction of movement of the motor (-1 - movement in the negative direction, 0 - motionless, 1 - movement in the positive direction)

# must be after Counter() initialization!
if isinstance(pwm, PWM):
self._pwm = pwm
else:
self._pwm = PWM(pin_step, freq=self._freq, duty_u16=0) # 0%

def __repr__(self):
return f"StepMotorPWMCounter(pin_step={self.pin_step}, pin_dir={self.pin_dir}, freq={self._freq}, reverse={self._reverse}, pwm={self._pwm}, counter={self._counter})"

def deinit(self):
try:
self._pwm.deinit()
except:
pass
try:
self._counter.irq(handler=None)
except:
pass
try:
self._counter.deinit()
except:
pass

# -----------------------------------------------------------------------
@property
def reverse(self):
return self._reverse

@reverse.setter
def reverse(self, reverse: int):
self._reverse = 1 if bool(reverse) else 0

# -----------------------------------------------------------------------
@property
def freq(self):
return self._freq

@freq.setter
def freq(self, freq):
self._freq = freq if freq > 0 else 1 # pulse frequency in Hz

# -----------------------------------------------------------------------
@property
def direction(self) -> int:
return self._direction

@direction.setter
def direction(self, delta: int):
if delta > 0:
self._direction = 1
self.pin_dir(1 ^ self._reverse)
elif delta < 0:
self._direction = -1
self.pin_dir(0 ^ self._reverse)
else:
self._direction = 0
# print(f'Set direction:{delta} to {self._direction}')

# -----------------------------------------------------------------------
@property
def steps_counter(self) -> int:
return self._counter.get_value()

# -----------------------------------------------------------------------
@property
def steps_target(self) -> int:
return self._steps_target

@steps_target.setter
def steps_target(self, steps_target):
# Set the target position that will be achieved in the main loop
if self._steps_target != steps_target:
self._steps_target = steps_target

delta = self._steps_target - self._counter.get_value()
if delta > 0:
self._match = self._steps_target - self._steps_over_run # * 2
self._counter.irq(
handler=self.irq_handler, trigger=Counter.IRQ_MATCH1, value=self._match
)
elif delta < 0:
self._match = self._steps_target + self._steps_over_run # * 2
self._counter.irq(
handler=self.irq_handler, trigger=Counter.IRQ_MATCH1, value=self._match
)

# -----------------------------------------------------------------------
def irq_handler(self, obj):
self.stop_pulses()
self._steps_over_run = (
self._steps_over_run + abs(self._counter.get_value() - self._match)
) // 2
print(
f" irq_handler: steps_over_run={self._steps_over_run}, counter.get_value()={self._counter.get_value()}"
)

def start_pulses(self):
self._pwm.freq(self._freq)
self._pwm.duty_u16(32768)

def stop_pulses(self):
self._pwm.duty_u16(0)

def stop(self):
self.stop_pulses()
self._steps_target = self._counter.get_value()

def go(self):
delta = self._steps_target - self._counter.get_value()
if delta > 0:
self.direction = 1
self.start_pulses()
elif delta < 0:
self.direction = -1
self.start_pulses()
else:
self.stop_pulses()
# print(f" go: delta={delta}, steps_target={self._steps_target}, match={self._match}, counter.get_value()={self._counter.get_value()}, direction={self._direction}, steps_over_run={self._steps_over_run}, freq={self._freq}")

def is_ready(self) -> bool:
delta = self._steps_target - self._counter.get_value()
# print(f" is_ready: delta={delta}, counter.get_value()={self._counter.get_value()}, steps_target={self._steps_target}, direction={self._direction}")
if self._direction > 0:
if delta <= 0:
self.stop_pulses()
return True
elif self._direction < 0:
if delta >= 0:
self.stop_pulses()
return True
else:
return delta == 0
return False
Loading