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