Day 2: (Wed Sep 1, Week 1) Hobby Servo Motion¶
Notes for 2021-09-01.
New Assignments¶
New assignment, due Wed Sep 8: Exercise: Servo Motion with the Pico
Before the next class, please arrange access to SolidWorks 2021-2022 (the version now in the clusters). Here are several options:
by installing on your personal Windows laptop (notes)
by installing in personal Windows virtual machine (on any platform)
via the https://virtual.andrew.cmu.edu/ Engineering cluster
by borrowing an IDeATe Windows laptop
If you are new to SolidWorks 3D modeling, I highly recommend you go through the beginner tutorial available via CMU LinkedIn Learning (formerly Lynda). (Note that it does appear possible to use this without a LinkedIn account.) Here’s a starting point: Learning SOLIDWORKS. (two hours).
Administrative¶
Please note that Monday September 6 is Labor Day, no class.
Please test your ID card on the door lock and let me know if it doesn’t work.
Reminder: please stay on top of laser cutter qualification. We’re working on opening up more training sessions.
Agenda¶
Introduction to hobby servos.
Programming movement trajectories.
Soldering tutorial.
Hands-on experimentation.
Figures¶
Lecture code samples¶
See also Hobby Servo Examples - Raspberry Pi Pico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | # servo_step.py
#
# Raspberry Pi Pico - hobby servo motion demo
#
# Demonstrates stepping a hobby servo back and forth.
#
# This assumes a tiny 9G servo has been wired up to the Pico as follows:
# Pico pin 40 (VBUS) -> servo red (+5V)
# Pico pin 38 (GND) -> servo brown (GND)
# Pico pin 1 (GP0) -> servo orange (SIG)
# links to CircuitPython module documentation:
# time https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/index.html
# math https://circuitpython.readthedocs.io/en/latest/shared-bindings/math/index.html
# board https://circuitpython.readthedocs.io/en/latest/shared-bindings/board/index.html
# pwmio https://circuitpython.readthedocs.io/en/latest/shared-bindings/pwmio/index.html
################################################################################
# load standard Python modules
import math, time
# load the CircuitPython hardware definition module for pin definitions
import board
# load the CircuitPython pulse-width-modulation module for driving hardware
import pwmio
#--------------------------------------------------------------------------------
# Define a function to issue a servo command by updating the PWM signal output.
# This function maps an angle specified in degrees between 0 and 180 to a servo
# command pulse width between 1 and 2 milliseconds, and then to the
# corresponding duty cycle fraction, specified as a 16-bit fixed-point integer.
# The optional pulse_rate argument specifies the pulse repetition rate in Hz
# (pulses per second).
def servo_write(servo, angle, pulse_rate=50, debug=False):
# calculate the desired pulse width in units of seconds
pulse_width = 0.001 + angle * (0.001 / 180.0)
# calculate the duration in seconds of a single pulse cycle
cycle_period = 1.0 / pulse_rate
# calculate the desired ratio of pulse ON time to cycle duration
duty_cycle = pulse_width / cycle_period
# convert the ratio into a 16-bit fixed point integer
duty_fixed = int(2**16 * duty_cycle)
# limit the ratio range and apply to the hardware driver
servo.duty_cycle = min(max(duty_fixed, 0), 65535)
# print some diagnostics to the console
if debug:
print(f"Driving servo to angle {angle}")
print(f" Pulse width {pulse_width} seconds")
print(f" Duty cycle {duty_cycle}")
print(f" Command value {servo.duty_cycle}\n")
#--------------------------------------------------------------------------------
# Create a PWMOut object on Pin GP0 to drive the servo.
servo = pwmio.PWMOut(board.GP0, duty_cycle=0, frequency=50)
# Begin the main processing loop.
while True:
servo_write(servo, 0.0, debug=True)
time.sleep(2.0)
servo_write(servo, 180.0, debug=True)
time.sleep(2.0)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | # servo_sweep.py
#
# Raspberry Pi Pico - hobby servo motion demo
#
# Demonstrates smoothly moving a hobby servo back and forth.
#
# This assumes a tiny 9G servo has been wired up to the Pico as follows:
# Pico pin 40 (VBUS) -> servo red (+5V)
# Pico pin 38 (GND) -> servo brown (GND)
# Pico pin 1 (GP0) -> servo orange (SIG)
# links to CircuitPython module documentation:
# time https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/index.html
# math https://circuitpython.readthedocs.io/en/latest/shared-bindings/math/index.html
# board https://circuitpython.readthedocs.io/en/latest/shared-bindings/board/index.html
# pwmio https://circuitpython.readthedocs.io/en/latest/shared-bindings/pwmio/index.html
################################################################################
# print a banner as reminder of what code is loaded
print("Starting servo_sweep script.")
# load standard Python modules
import math, time
# load the CircuitPython hardware definition module for pin definitions
import board
# load the CircuitPython pulse-width-modulation module for driving hardware
import pwmio
#--------------------------------------------------------------------------------
# Class to represent a single hardware hobby servo. This wraps up all the
# configuration information and current state into a single object. The
# creation of the object also initializes the physical pin hardware.
class Servo():
def __init__(self, pin, pulse_rate=50):
# Create a PWMOut object on the desired pin to drive the servo.
# Note that the initial duty cycle of zero generates no pulses, which
# for many servos will present as a quiescent low-power state.
self.pwm = pwmio.PWMOut(board.GP0, duty_cycle=0, frequency=pulse_rate)
# Save the initialization arguments within the object for later reference.
self.pin = pin
self.pulse_rate = pulse_rate
# Initialize the other state variables.
self.target = None # target angle; None indicates the "OFF" state
self.debug = False # flag to control print output
def write(self, angle):
# Object method to issue a servo command by updating the PWM signal
# output. This function maps an angle specified in degrees between 0
# and 180 to a servo command pulse width between 1 and 2 milliseconds,
# and then to the corresponding duty cycle fraction, specified as a
# 16-bit fixed-point integer. As a special case, an angle value of None
# will turn off the output; many servos will then become backdrivable.
# calculate the desired pulse width in units of seconds
if angle is None:
pulse_width = 0.0
else:
pulse_width = 0.001 + angle * (0.001 / 180.0)
# calculate the duration in seconds of a single pulse cycle
cycle_period = 1.0 / self.pulse_rate
# calculate the desired ratio of pulse ON time to cycle duration
duty_cycle = pulse_width / cycle_period
# convert the ratio into a 16-bit fixed point integer
duty_fixed = int(2**16 * duty_cycle)
# limit the ratio range and apply to the hardware driver
self.pwm.duty_cycle = min(max(duty_fixed, 0), 65535)
# save the target value in the object attribute (i.e. variable)
self.target = angle
# if requested, print some diagnostics to the console
if self.debug:
print(f"Driving servo to angle {angle}")
print(f" Pulse width {pulse_width} seconds")
print(f" Duty cycle {duty_cycle}")
print(f" Command value {self.pwm.duty_cycle}\n")
#--------------------------------------------------------------------------------
# Movement primitive to smoothly move from a start to end angle at a constant rate.
# It does not return until the movement is complete.
# The angles are specified in degrees, the speed in degrees/second.
def linear_move(servo, start, end, speed=60, update_rate=50):
# Calculate the number of seconds to wait between target updates to allow
# the motor to move.
# Units: seconds = 1.0 / (cycles/second)
interval = 1.0 / update_rate
# Compute the size of each step in degrees.
# Units: degrees = (degrees/second) * second
step = speed * interval
# Output the start angle once before beginning the loop. This guarantees at
# least one angle will be output even if the start and end are equal.
angle = start
servo.write(angle)
# Loop once for each incremental angle change.
while angle != end:
time.sleep(interval) # pause for the sampling interval
# Update the target angle. The positive and negative movement directions
# are treated separately.
if end >= start:
angle += step; # movement in the positive direction
if angle > end:
angle = end # end at an exact position
else:
angle -= step # movement in the negative direction
if angle < end:
angle = end # end at an exact position
servo.write(angle) # update the hardware
#--------------------------------------------------------------------------------
# Movement primitive to generate a smooth oscillating movement by simulating a
# spring-mass-damper system. It does not return until the movement is complete.
# This is an example of simple harmonic motion and uses a differential equation
# to specify a motion implicitly.
# The q_d parameter specifies the center angle of the oscillation, conceptually
# is the angle of the simulated spring anchor point.
# The default parameters were selected as follows:
# q = 0.0 initial position
# qd = 0.0 initial velocity
# k = 4*pi*pi spring constant for 1 Hz: freq = (1/2*pi) * sqrt(k/m); k = (freq*2*pi)^2
# b = 2.0 damping constant
def ringing_move(servo, q_d, q=0.0, qd=0.0, k=4*math.pi*math.pi, b=2.0,
update_rate=50, duration=2.0, debug=False):
# Calculate the number of seconds to wait between target updates to allow
# the motor to move.
# Units: seconds = 1.0 / (cycles/second)
interval = 1.0 / update_rate
while duration > 0.0:
# Calculate the acceleration.
# qdd acceleration in angles/sec^2
# k spring constant relating the acceleration to the angular displacement
# b damping constant relating the acceleration to velocity
qdd = k * (q_d - q) - b * qd
# integrate one time step using forward Euler integration
q += qd * interval # position changes in proportion to velocity
qd += qdd * interval # velocity changes in proportion to acceleration
# update the servo command with the new angle
servo.write(q)
# print the output for plotting if requested
if debug:
print(q)
# Delay to control timing. This is an inexact strategy, since it doesn't account for
# any execution time of this function.
time.sleep(interval)
duration -= interval
#--------------------------------------------------------------------------------
# Create an object to represent a servo on the given hardware pin.
print("Creating servo object.")
servo = Servo(board.GP0)
# Enable (voluminous) debugging output.
# servo.debug = True
# Begin the main processing loop. This is structured as a looping script, since
# each movement primitive 'blocks', i.e. doesn't return until the action is
# finished.
print("Starting main script.")
while True:
# initial pause
time.sleep(2.0)
# begin the movement sequence, starting with some slow sweeps
print("Starting linear motions.")
linear_move(servo, 0.0, 180.0, speed=45)
linear_move(servo, 180.0, 0.0, speed=22.5)
# brief pause; stillness is the counterpoint
time.sleep(1.0)
# start bouncy oscillation movements
print("Starting ringing motions.")
ringing_move(servo, 90.0, duration=1.5)
ringing_move(servo, 45.0, duration=1.5)
ringing_move(servo, 90.0, duration=1.5)
# Final oscillation into a a grand pause at the end, but never actually
# stopping; stillness isn't necessarily stationary. This also initializes
# the generator to begin the movement at the target angle, but with positive
# velocity.
print("Starting final ringing motion.")
ringing_move(servo, 90.0, q=90.0, qd=400, b=1.0, duration=12.0)
|