Skip to content

Instantly share code, notes, and snippets.

@kopp
Created June 21, 2020 21:22
Show Gist options
  • Save kopp/2642945fb9988fe21fcf0e68da2925a6 to your computer and use it in GitHub Desktop.
Save kopp/2642945fb9988fe21fcf0e68da2925a6 to your computer and use it in GitHub Desktop.
A python class that allows to control a two-wheel robot with two AC motors on the Calliope Mini.
from calliope_mini import pin28, pin29, pin30
from utime import ticks_us, ticks_add
# Note: as the file is large and has long comments, use pyminifier before uploading to Calliope mini.
# Note: Some abstration is missing here and code is duplicated.
# This happens with a reason:
# MicroPython has only a very limited stack size and thus number of nested
# function calls is low.
# Because of that, code is repeated in different functions
# instead of factoring it out, because calling the factored out function
# reduces the available number of nested function calls for the "client" using
# this code.
class TwoAcMotors:
"""
This class assumes, that the motors are wired as follows:
- left motor is connected to Motor A pin and GND
- right motor is connected to Motor B pin and GND
This means, that both motors can only go forwards.
Implementation:
When the robot should go straight forwards, each period is split evenly
between the left and the right motor.
|--------------- one period ------------------------|
|---- left on ------------|---- left off -----------|
|---- right off ----------|---- right on -----------|
When the robot should turn, the time is not divided evenly any more but
with a given factor, e.g. turn left with factor 0.7 means that the right
motor is on for 70% of the period and the left for only 30%.
|--------------- one period ------------------------|
|-- left on ---|----- left off ---------------------|
|-- right off -|----- right on ---------------------|
The speed is determined by what "on" means.
The pin controlling each motor will be written with an analog value which
is proportional to the speed (1.0 -> maximum analog value, 0.0 -> off).
"""
MOTOR1_ON = 1
MOTOR2_ON = 2
OBSOLETE = 3 # mark the current state as obsolete i.e. needing change
def __init__(self, default_speed=1):
if not 0.0 <= default_speed <= 1.0:
raise ValueError("Speed values must be in [0, 1].")
self.default_speed = default_speed
self.period_duration_us = 16384 # multiple of 256 (analog period)
self._is_active = False
self._motors_analog_value = None
self._timestamp_us_period_begin = None
self._duration_us_first_motor = None
self._timestamp_us_period_end = None
pin29.set_analog_period_microseconds(256)
pin30.set_analog_period_microseconds(256)
def start_to_move_forwards(self, speed=None):
"""
Move forward with given speed.
Note: if speed is too low, it may happen that the robot does not move at all.
:param speed float: Speed with which to move -- see ``set_default_speed``.
"""
if speed is None:
speed = self.default_speed
if not 0.0 <= speed <= 1.0:
raise ValueError("Speed values must be in [0, 1].")
now = ticks_us()
self._timestamp_us_period_begin = now
self._duration_us_first_motor = int(self.period_duration_us / 2)
self._timestamp_us_period_end = ticks_add(self._timestamp_us_period_begin, self.period_duration_us)
self._motors_analog_value = int(1024 * speed)
self._motors_current_state = TwoAcMotors.OBSOLETE
self._is_active = True
def stop(self):
"""
Stop the motors directly -- no further call to ``act()`` necessary.
"""
self._is_active = False
pin28.write_digital(0)
pin29.write_digital(0)
pin30.write_digital(0)
def start_curve(self, direction, turn_factor=1.0, speed=None):
"""
Start to turn the robot.
Note: if speeds are low, it may happen that for a small turn factor,
the wheel in turning direction is not turned at all.
:param direction str: either 'left' or 'right'
:param turn_factor float: Ratio of the robot's ability to do the turn (1 -> maximal, 0 -> no turn at all)
:param speed float: Speed with which to move -- see ``set_default_speed``.
"""
if not 0.0 <= turn_factor <= 1.0:
raise ValueError("Please use a value in [0, 1] as turn factor.")
if speed is None:
speed = self.default_speed
else:
if not 0.0 <= speed <= 1.0:
raise ValueError("Speed values must be in [0, 1].")
if direction == "left":
self._duration_us_first_motor = int(self.period_duration_us * ((1.0 - turn_factor) / (2.0 - turn_factor)))
elif direction == "right":
self._duration_us_first_motor = int(self.period_duration_us / (2.0 - turn_factor))
else:
raise ValueError("Please use only 'left' or 'right' as direction.")
now = ticks_us()
self._timestamp_us_period_begin = now
self._timestamp_us_period_end = ticks_add(self._timestamp_us_period_begin, self.period_duration_us)
self._motors_analog_value = int(1024 * speed)
self._motors_current_state = TwoAcMotors.OBSOLETE
self._is_active = True
def act(self):
"""
This method must be called in a fast loop.
Only when it is called, actual commands are executed and sent to the motors.
"""
if self._is_active:
now = ticks_us()
pin28.write_digital(1)
# handle end of period
if now >= self._timestamp_us_period_end:
self._timestamp_us_period_begin = self._timestamp_us_period_end
self._timestamp_us_period_end = ticks_add(self._timestamp_us_period_begin, self.period_duration_us)
# check, in which part of the period (motor 1 or 2)
timestamp_switch_motors = ticks_add(self._timestamp_us_period_begin, self._duration_us_first_motor)
if now < timestamp_switch_motors: # TODO: comparison is mostly wrong in case of overflow
motors_desired_state = TwoAcMotors.MOTOR1_ON
else:
motors_desired_state = TwoAcMotors.MOTOR2_ON
# set the pins if they need change
if motors_desired_state != self._motors_current_state:
if motors_desired_state == TwoAcMotors.MOTOR1_ON:
self._motors_current_state = TwoAcMotors.MOTOR1_ON
if self._motors_analog_value == 1024:
pin29.write_digital(1)
else:
pin29.write_analog(self._motors_analog_value)
pin30.write_digital(0)
elif motors_desired_state == TwoAcMotors.MOTOR2_ON:
self._motors_current_state = TwoAcMotors.MOTOR2_ON
pin29.write_digital(0)
if self._motors_analog_value == 1024:
pin30.write_digital(1)
else:
pin30.write_analog(self._motors_analog_value)
else:
raise ValueError("Unknown desired motor state {}".format(motors_desired_state))
def set_period_duration(self, duration_in_us):
"""
This class implements a software PWM.
Use this function to set the period duration.
A larger period means, that it is possible to use/define the speed commands more precisely.
The commands can only be executed as fractions of the period duration
-- if the period duration is 20, only speeds 0/20, 1/20, 2/20, ...,
20/20 can be actually actuated.
On the other hand, a longer period means, that the robot drives
longer with only one wheel on, so the small S-curves that it drives
during the two parts of a period are more visible.
If the values are too small, the software simply breaks, because it
has no time to call multiple ``act()`` calls within a period.
See the documentation of the class for more info on the period.
Only use this if you know, what you do.
"""
self.period_duration_us = duration_in_us
def set_default_speed(self, speed):
"""
Set the default speed -- this speed is used if you use one of the
moovement-functions without parameter for speed.
The speed is a floating point value in the range 0..1.
0.0 means to not move at all and 1.0 means to move as fast as possible.
"""
if 0.0 <= speed <= 1.0:
self.default_speed = speed
else:
raise ValueError("Speed values must be in [0, 1].")
# the following is for demo only.
class Timer:
def __init__(self, duration_ms, autostart=True):
self.duration_us = 1000 * duration_ms
self._is_expired = False
if autostart:
self._timestamp_us_end = ticks_add(ticks_us(), self.duration_us)
def start(self):
self._is_expired = False
self._timestamp_us_end = ticks_add(ticks_us(), self.duration_us)
def rewind(self, new_duration_ms, autostart=True):
self.duration_us = 1000 * new_duration_ms
self._is_expired = False
if autostart:
self._timestamp_us_end = ticks_add(ticks_us(), self.duration_us)
def is_expired(self):
now = ticks_us()
if now >= self._timestamp_us_end: # TODO: breaks in case of tick overflow
self._is_expired = True
return self._is_expired
if __name__ == "__main__":
motors = TwoAcMotors()
timer = Timer(1000)
print("go straight")
timer.start()
motors.start_to_move_forwards()
while not timer.is_expired():
motors.act()
print("rotate left")
timer.start()
motors.start_curve("left")
while not timer.is_expired():
motors.act()
print("pause")
motors.stop()
timer.rewind(500)
while not timer.is_expired():
pass
print("rotate right slower")
timer.rewind(1500)
motors.start_curve("right", speed=0.7)
while not timer.is_expired():
motors.act()
print("pause")
motors.stop()
timer.rewind(500)
while not timer.is_expired():
pass
print("curve left")
timer.rewind(1500)
motors.start_curve("left", turn_factor=0.6)
while not timer.is_expired():
motors.act()
print("straight ...")
timer.rewind(1000)
motors.start_to_move_forwards()
while not timer.is_expired():
motors.act()
print(" ... small curve right ...")
timer.rewind(300)
motors.start_curve("right", turn_factor=0.3)
while not timer.is_expired():
motors.act()
print(" ... and straight again")
timer.rewind(1000)
motors.start_to_move_forwards()
while not timer.is_expired():
motors.act()
motors.stop()
from calliope_mini import display, Image
display.show(Image.HEART)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment