Skip to content

Commit

Permalink
Some big motor + encoder examples
Browse files Browse the repository at this point in the history
  • Loading branch information
ZodiusInfuser committed Nov 2, 2023
1 parent 5d4b613 commit 5e395cf
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 0 deletions.
57 changes: 57 additions & 0 deletions examples/modules/big_motor/single_motor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import math
from pimoroni_yukon import Yukon
from pimoroni_yukon import SLOT1 as SLOT
from pimoroni_yukon.modules import BigMotorModule
from pimoroni_yukon.timing import ticks_ms, ticks_add

"""
How to drive a single motor from a Big Motor + Encoder Module connected to Slot1.
A wave pattern will be played on the attached motor.
"""

# Constants
SPEED = 0.005 # How much to advance the motor phase offset by each update
UPDATES = 50 # How many times to update the motors per second
SPEED_EXTENT = 1.0 # How far from zero to drive the motors
CURRENT_LIMIT = 0.5 # The maximum current (in amps) the motors will be driven with

# Variables
yukon = Yukon() # Create a new Yukon object
module = BigMotorModule() # Create a BigMotorModule object
phase_offset = 0 # The offset used to animate the motors

# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt)
try:
yukon.register_with_slot(module, SLOT) # Register the BigMotorModule object with the slot
yukon.verify_and_initialise() # Verify that a BigMotorModule is attached to Yukon, and initialise it
yukon.enable_main_output() # Turn on power to the module slots

module.enable() # Enable the motor driver on the BigMotorModule

current_time = ticks_ms() # Record the start time of the program loop

# Loop until the BOOT/USER button is pressed
while not yukon.is_boot_pressed():

# Give the motor a new speed
phase = phase_offset * math.pi * 2
speed = math.sin(phase) * SPEED_EXTENT
module.motor.speed(speed)

# Advance the phase offset, wrapping if it exceeds 1.0
phase_offset += SPEED
if phase_offset >= 1.0:
phase_offset -= 1.0

print(f"Phase = {phase_offset}")

# Advance the current time by a number of seconds
current_time = ticks_add(current_time, int(1000 / UPDATES))

# Monitor sensors until the current time is reached, recording the min, max, and average for each
# This approach accounts for the updating of the rainbows taking a non-zero amount of time to complete
yukon.monitor_until_ms(current_time)

finally:
# Put the board back into a safe state, regardless of how the program may have ended
yukon.reset()
105 changes: 105 additions & 0 deletions examples/modules/big_motor/tuning/motor_profiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from pimoroni import NORMAL_DIR # , REVERSED_DIR
from pimoroni_yukon import Yukon
from pimoroni_yukon import SLOT1 as SLOT
from pimoroni_yukon.modules import BigMotorModule

"""
A program that profiles the speed of a motor across its PWM duty cycle range using the attached encoder for feedback.
Note that the returned readings will only be valid for a single input voltage.
"""

# Constants
GEAR_RATIO = 30 # The gear ratio of the motor
ENCODER_CPR = 12 # The number of counts a single encoder shaft revolution will produce
MOTOR_CPR = GEAR_RATIO * ENCODER_CPR # The number of counts a single motor shaft revolution will produce

DIRECTION = NORMAL_DIR # The direction to spin the motor in. NORMAL_DIR (0), REVERSED_DIR (1)
SPEED_SCALE = 3.4 # The scaling to apply to the motor's speed. Set this to the maximum measured speed
ZERO_POINT = 0.0 # The duty cycle that corresponds with zero speed when plotting the motor's speed as a straight line
DEAD_ZONE = 0.0 # The duty cycle below which the motor's friction prevents it from moving

DUTY_STEPS = 100 # How many duty cycle steps to sample the speed of
SETTLE_TIME = 0.1 # How long to wait after changing motor duty cycle
CAPTURE_TIME = 0.2 # How long to capture the motor's speed at each step

# Variables
yukon = Yukon() # Create a new Yukon object
module = BigMotorModule(counts_per_rev=MOTOR_CPR) # Create a BigMotorModule object

# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt)
try:
yukon.register_with_slot(module, SLOT) # Register the BigMotorModule object with the slot
yukon.verify_and_initialise() # Verify that a BigMotorModule is attached to Yukon, and initialise it
yukon.enable_main_output() # Turn on power to the module slots

# Set the motor's speed scale, zeropoint, and deadzone
module.motor.speed_scale(SPEED_SCALE)
module.motor.zeropoint(ZERO_POINT)
module.motor.deadzone(DEAD_ZONE)

# Set the motor and encoder's direction
module.motor.direction(DIRECTION)
module.encoder.direction(DIRECTION)

# Function that performs a single profiling step
def profile_at_duty(motor, encoder, duty):
# Set the motor to a new duty cycle and wait for it to settle
if DIRECTION == 1:
motor.duty(0.0 - duty)
else:
motor.duty(duty)
yukon.monitored_sleep(SETTLE_TIME)

# Perform a dummy capture to clear the encoder
encoder.capture()

# Wait for the capture time to pass
yukon.monitored_sleep(CAPTURE_TIME)

# Perform a capture and read the measured speed
capture = encoder.capture()
measured_speed = capture.revolutions_per_second

# These are some alternate speed measurements from the encoder
# measured_speed = capture.revolutions_per_minute
# measured_speed = capture.degrees_per_second
# measured_speed = capture.radians_per_second

# Print out the expected and measured speeds, as well as their difference
print("Duty =", motor.duty(), end=", ")
print("Expected =", motor.speed(), end=", ")
print("Measured =", measured_speed, end=", ")
print("Diff =", motor.speed() - measured_speed)

module.enable() # Enable the motor driver on the BigMotorModule
module.motor.enable() # Enable the motor to get started

print("Profiler Starting...")

# Profile from 0% up to one step below 100%
for i in range(DUTY_STEPS):
profile_at_duty(module.motor, module.encoder, i / DUTY_STEPS)

# Profile from 100% down to one step above 0%
for i in range(DUTY_STEPS):
profile_at_duty(module.motor, module.encoder, (DUTY_STEPS - i) / DUTY_STEPS)

# Profile from 0% down to one step above -100%
for i in range(DUTY_STEPS):
profile_at_duty(module.motor, module.encoder, -i / DUTY_STEPS)

# Profile from -100% up to one step below 0%
for i in range(DUTY_STEPS):
profile_at_duty(module.motor, module.encoder, -(DUTY_STEPS - i) / DUTY_STEPS)

# Profile 0% again
profile_at_duty(module.motor, module.encoder, 0)

print("Profiler Finished...")

# Disable the motor now the profiler has finished
module.motor.disable()

finally:
# Put the board back into a safe state, regardless of how the program may have ended
yukon.reset()
109 changes: 109 additions & 0 deletions examples/modules/big_motor/tuning/position_tuning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from pimoroni import PID, NORMAL_DIR # , REVERSED_DIR
from pimoroni_yukon import Yukon
from pimoroni_yukon import SLOT1 as SLOT
from pimoroni_yukon.modules import BigMotorModule
from pimoroni_yukon.timing import ticks_ms, ticks_add

"""
A program to aid in the discovery and tuning of motor PID values for position control.
It does this by commanding the motor to move repeatedly between two setpoint angles and
plots the measured response.
"""

# Constants
GEAR_RATIO = 30 # The gear ratio of the motor
ENCODER_CPR = 12 # The number of counts a single encoder shaft revolution will produce
MOTOR_CPR = GEAR_RATIO * ENCODER_CPR # The number of counts a single motor shaft revolution will produce

DIRECTION = NORMAL_DIR # The direction to spin the motor in. NORMAL_DIR (0), REVERSED_DIR (1)
SPEED_SCALE = 3.4 # The scaling to apply to the motor's speed to match its real-world speed

UPDATES = 100 # How many times to update the motor per second
UPDATE_RATE = 1 / UPDATES
PRINT_WINDOW = 0.25 # The time (in seconds) after a new setpoint, to display print out motor values
MOVEMENT_WINDOW = 2.0 # The time (in seconds) between each new setpoint being set
PRINT_DIVIDER = 1 # How many of the updates should be printed (i.e. 2 would be every other update)

# Multipliers for the different printed values, so they appear nicely on the Thonny plotter
SPD_PRINT_SCALE = 10 # Driving Speed multipler

POSITION_EXTENT = 90 # How far from zero to move the motor, in degrees

# PID values
POS_KP = 0.14 # Position proportional (P) gain
POS_KI = 0.0 # Position integral (I) gain
POS_KD = 0.0022 # Position derivative (D) gain


# Variables
yukon = Yukon() # Create a new Yukon object
module = BigMotorModule(counts_per_rev=MOTOR_CPR) # Create a BigMotorModule object
update = 0
print_count = 0

# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt)
try:
yukon.register_with_slot(module, SLOT) # Register the BigMotorModule object with the slot
yukon.verify_and_initialise() # Verify that a BigMotorModule is attached to Yukon, and initialise it
yukon.enable_main_output() # Turn on power to the module slots

module.motor.speed_scale(SPEED_SCALE) # Set the motor's speed scale

# Set the motor and encoder's direction
module.motor.direction(DIRECTION)
module.encoder.direction(DIRECTION)

# Create PID object for position control
pos_pid = PID(POS_KP, POS_KI, POS_KD, UPDATE_RATE)

module.enable() # Enable the motor driver on the BigMotorModule
module.motor.enable() # Enable the motor to get started

pos_pid.setpoint = POSITION_EXTENT # Set the initial setpoint position

current_time = ticks_ms() # Record the start time of the program loop

# Loop until the BOOT/USER button is pressed
while not yukon.is_boot_pressed():

# Capture the state of the encoder
capture = module.encoder.capture()

# Calculate the velocity to move the motor closer to the position setpoint
vel = pos_pid.calculate(capture.degrees, capture.degrees_per_second)

# Set the new motor driving speed
module.motor.speed(vel)

# Print out the current motor values and their setpoints,
# but only for the first few updates and only every multiple
if update < (PRINT_WINDOW * UPDATES) and print_count == 0:
print("Pos =", capture.degrees, end=", ")
print("Pos SP =", pos_pid.setpoint, end=", ")
print("Speed = ", module.motor.speed() * SPD_PRINT_SCALE)

# Increment the print count, and wrap it
print_count = (print_count + 1) % PRINT_DIVIDER

update += 1 # Move along in time

# Have we reached the end of this time window?
if update >= (MOVEMENT_WINDOW * UPDATES):
update = 0 # Reset the counter

# Set the new position setpoint to be the inverse of the current setpoint
pos_pid.setpoint = 0.0 - pos_pid.setpoint

# Advance the current time by a number of seconds
current_time = ticks_add(current_time, int(1000 * UPDATE_RATE))

# Monitor sensors until the current time is reached, recording the min, max, and average for each
# This approach accounts for the updating of the rainbows taking a non-zero amount of time to complete
yukon.monitor_until_ms(current_time)

# Disable the motor
module.motor.disable()

finally:
# Put the board back into a safe state, regardless of how the program may have ended
yukon.reset()

0 comments on commit 5e395cf

Please sign in to comment.