Skip to content

Instantly share code, notes, and snippets.

@nickovs
Last active March 11, 2025 14:45
Show Gist options
  • Save nickovs/23928a8591735b00b00654fa628302d5 to your computer and use it in GitHub Desktop.
Save nickovs/23928a8591735b00b00654fa628302d5 to your computer and use it in GitHub Desktop.
A stepper motor driver for MicroPython
# stepper.py
# A micropython driver for 4-phase, unipolar stepper motors such as
# the 28BYJ-48
# Relesed to the Public Domain by Nicko van Someren, 2020
# The constructor for the Stepper class takes as arguments the four
# pins for driving the motor phases, in phase order, and optionally a
# timer. The pins can be passed as pin numbers or machine.Pin objects
# and the timer can be a machine.Timer object or a timer index. Note
# that if two stepper motors use the same timer then they will not be
# able to run at the same time.
#
# The run() method takes a number of steps and an optional delay (in
# seconds) between driving the steps (the default is 1ms). A negative
# step count will drive the motor in the oposite direction to a
# positive count. The count represents "half steps" since the driver
# alternates driving single coils and driving pairs of adjacent coils.
# Calls to run() return immediately; the motor runs on a timer in the
# background. Calling run() again before the previous command has
# finished adds the new count to the old count, so the destination
# position is the sum of the requests; the delay is set to the new
# value if stepper is not already at its final location.
#
# The stop() method will stop the rotation of the motor. It returns
# the number of un-taken steps that would be needed to perform the
# outstanding requests from previous calls to run().
#
# The is_running property returns true if the motor is running,
# i.e. stop() would return a non-zero value, and false otherwise.
import machine
import time
# When the following number is sampled at four consecutive
# even-numbered bits it will have two bits set, but sampling at four
# consecutive odd-numbered bits will only yield one bit set.
_WAVE_MAGIC = 0b0000011100000111
class Stepper:
def __init__(self, A, B, C, D, T=1):
if not isinstance(T, machine.Timer):
T = machine.Timer(T)
self._timer = T
l = []
for p in (A, B, C, D):
if not isinstance(p, machine.Pin):
p = machine.Pin(p, machine.Pin.OUT)
l.append(p)
self._pins = l
self._phase = 0
self._stop()
self._run_remaining = 0
def _stop(self):
[p.off() for p in self._pins]
# Note: This is called on an interrupt on some platforms, so it must not use the heap
def _callback(self, t):
if self._run_remaining != 0:
direction = 1 if self._run_remaining > 0 else -1
self._phase = (self._phase + direction) % 8
wave = _WAVE_MAGIC >> self._phase
for i in range(4):
self._pins[i].value((wave >> (i*2)) & 1)
self._run_remaining -= direction
else:
self._timer.deinit()
self._stop()
def run(self, count, delay=0.001):
tick_hz=1000000
period = int(delay*tick_hz)
if period < 500:
period = 500
self._run_remaining += count
if self._run_remaining != 0:
self._timer.init(period=period, tick_hz=tick_hz,
mode=machine.Timer.PERIODIC, callback=self._callback)
else:
self._timer.deinit()
self._stop()
def stop(self):
remaining = self._run_remaining
self._run_remaining = 0
self._timer.deinit()
self._stop()
return remaining
@property
def is_running(self):
return self._run_remaining != 0
@omirete
Copy link

omirete commented Nov 14, 2023

Thanks a lot for this! It works really well.

May I ask how I would go about calculating the exact value for the count variable so I get exactly a full revolution?

@nickovs
Copy link
Author

nickovs commented Nov 14, 2023

@omirete The answer to that is dependent on the type of stepper motor that you have but it will typically be 2 times the nominal number of steps for the motor. For some sorts of winding it could be 1 times or 4 times, but it's pretty easy to tell those apart just by trying. Of course this for one rotation of the motor's shaft; if you have a gearbox then you need to multiply this by the division ratio of the gearbox. Thus if you have a 5:1 step-down gearbox then you need to multiply the count by 5, since you will need 5 revolutions of the motor shaft to get one revolution of the gearbox output.

@11mbs
Copy link

11mbs commented Mar 11, 2025

This is a beautiful and useful piece of code! Thank you for sharing it!

Is there a quick way to make the motor operate in Full Step?

@nickovs
Copy link
Author

nickovs commented Mar 11, 2025

@11mbs The code as written will always carry out the motion using half-stepping, although you can always choose to pass in even values for the count.

The easiest way to change the code to only drive in full-step would be to change line 64 to read self._phase = (self._phase + direction*2) % 8 and line 53 to self._phase = 1. This works because the 'odd' phases are the ones where only one winding is powered; starting at 1 and moving by an even amount will keep you there.

One could also update this code to add a fullstep parameter to the constructor, use it to set a step size attribute in the object to either 1 or 2 and use this to multiply the direction at run time. This would be a worthwhile change but I'm travelling right now and don't have access to a stepper motor, and I'm hesitant to post changes to the published code without testing them, so I'm going to leave that task to you for the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment