Lesson 16 — Servos and Variable Scope
Estimated time: 90 minutes
- Learning Objectives
- New Hardware: 9G Servo Motor
- Concepts
- Guided Walkthrough
- Challenges
- Common Mistakes & Debugging
- Key Vocabulary
Learning Objectives
By the end of this lesson you will be able to:
- Wire and control a 9G servo motor with PWM
- Understand local vs global variable scope
- Use the
globalkeyword when necessary - Build a button-controlled servo with LED feedback
New Hardware: 9G Servo Motor
A servo motor moves to a precise angle (0°–180°) and holds that position. It uses three wires: power, ground, and a signal wire that receives PWM pulses.
Wiring
| Servo Wire | Connect To |
|---|---|
| Red | 5V |
| Brown or Black (ground) | GND |
| Orange or Yellow (signal) | GPIO 13 |
Servos can draw significant current when moving. If your ESP32-S3 resets unexpectedly when the servo moves, power the servo from a separate 5V supply (connecting GND between both supplies).
How Servo Control Works — PWM
PWM (Pulse Width Modulation) is a way to communicate an analogue value using a digital pin. The signal alternates rapidly between HIGH and LOW. The proportion of time it’s HIGH (the duty cycle) carries the information.
For servos, we use 50Hz frequency (50 pulses per second). The width of each pulse determines the angle:
- ~0.5ms pulse → 0°
- ~1.5ms pulse → 90° (centre)
- ~2.5ms pulse → 180°
In MicroPython on ESP32, the duty() function takes a value 0–1023. For a 50Hz signal:
- 0° ≈ duty 40 (calibrate for your specific servo)
- 180° ≈ duty 115
Did you know? Servos are in RC cars and planes, robotic arms, camera gimbals (those systems that keep cameras steady), and even the tiny actuators inside automatic camera lenses. The servo principle was invented in the 1800s for controlling ship rudders.
Concepts
Local vs Global Scope
Scope is the region of code where a variable is visible and accessible.
Local variable — created inside a function. Only exists while the function is running. Disappears when the function returns.
Global variable — created outside any function. Exists throughout the whole program.
x = 10 # Global
def my_function():
y = 20 # Local — only exists inside this function
print(x) # Can READ global x ✓
print(y) # Can read local y ✓
my_function()
print(x) # ✓ Can access global x
# print(y) # ✗ ERROR — y doesn't exist here
Why scope? Functions get their own “workspace” so they don’t accidentally overwrite each other’s variables. This makes code safer and easier to reason about.
Modifying a Global Variable
If you want to change a global variable from inside a function, use the global keyword:
counter = 0
def increment():
global counter # Tell Python: use the GLOBAL counter
counter = counter + 1 # Now this modifies the global
increment()
increment()
increment()
print(counter) # 3
Without global, Python would create a new local variable called counter instead of modifying the global one.
Best practice: Use global sparingly. Prefer passing values in as parameters and getting results back via return. global is mainly useful for hardware objects and simple state variables that must be shared across many functions.
Guided Walkthrough
Step 1: Basic Servo Control
from machine import Pin, PWM
import time
servo = PWM(Pin(13), freq=50)
def set_angle(angle):
"""Move servo to specified angle (0-180 degrees)."""
# Map 0-180° to duty cycle
# Adjust min_duty and max_duty to calibrate your servo
min_duty = 40
max_duty = 115
duty = int(min_duty + (angle / 180) * (max_duty - min_duty))
servo.duty(duty)
# Test: move through key positions
for angle in [0, 45, 90, 135, 180]:
print(f"Moving to {angle}°")
set_angle(angle)
time.sleep(1)
set_angle(90) # Return to centre
time.sleep(0.5)
servo.deinit() # Release PWM when done
print("Done!")
Calibrating your servo: If 0° and 180° don’t match what you expect, adjust min_duty (for 0°) and max_duty (for 180°). Try values between 30–55 for min and 100–130 for max.
Step 2: Scope Demonstration
score = 100 # Global
def add_points(points):
# Can READ global score:
new_score = score + points
return new_score # Return instead of modifying global
def reset_score():
global score # Declare intent to modify global
score = 0
print("Score reset to 0")
print(f"Initial score: {score}")
result = add_points(50)
print(f"After adding 50: {result}")
print(f"Global score unchanged: {score}")
reset_score()
print(f"After reset: {score}")
Step 3: Servo + LED + Scope
from machine import Pin, PWM
import machine, neopixel, time
# Hardware (global objects — fine to be global)
servo = PWM(Pin(13), freq=50)
pin48 = machine.Pin(48, machine.Pin.OUT)
np = neopixel.NeoPixel(pin48, 1)
# State variable (global — shared across functions)
current_angle = 90
def set_angle(angle):
"""Set servo angle and update LED colour."""
global current_angle
angle = max(0, min(180, angle)) # Clamp to valid range
current_angle = angle
duty = int(40 + (angle / 180) * 75)
servo.duty(duty)
# LED shows angle: 0°=red, 90°=yellow, 180°=green
red = int(255 * (1 - angle / 180))
green = int(255 * (angle / 180))
np[0] = (red, green, 0)
np.write()
def sweep(start_angle, end_angle, step=5, delay=0.04):
"""Sweep servo from start to end angle."""
direction = 1 if end_angle > start_angle else -1
angle = start_angle
while (direction == 1 and angle <= end_angle) or \
(direction == -1 and angle >= end_angle):
set_angle(angle)
time.sleep(delay)
angle += direction * step
def centre():
"""Move to centre (90°)."""
set_angle(90)
# Demo sequence
print("Servo demo...")
centre()
time.sleep(1)
print("Sweeping 90° to 180°...")
sweep(90, 180)
time.sleep(0.5)
print("Sweeping 180° to 0°...")
sweep(180, 0, step=3, delay=0.03)
time.sleep(0.5)
print("Sweeping back to centre...")
sweep(0, 90)
print(f"Final angle: {current_angle}°")
servo.deinit()
Step 4: Button-Controlled Servo
from machine import Pin, PWM
import machine, neopixel, time
servo = PWM(Pin(13), freq=50)
btn_up = Pin(0, Pin.IN, Pin.PULL_UP)
btn_down = Pin(14, Pin.IN, Pin.PULL_UP)
pin48 = machine.Pin(48, machine.Pin.OUT)
np = neopixel.NeoPixel(pin48, 1)
current_angle = 90
STEP = 10
def set_angle(angle):
global current_angle
angle = max(0, min(180, angle))
current_angle = angle
servo.duty(int(40 + (angle / 180) * 75))
np[0] = (int(255 * (1 - angle/180)), int(255 * angle/180), 0)
np.write()
set_angle(90)
print("Button controls:")
print(" Button A (GPIO 0) = increase angle")
print(" Button B (GPIO 14) = decrease angle")
print("Ctrl+C to stop.")
while True:
if btn_up.value() == 0:
set_angle(current_angle + STEP)
print(f"Angle: {current_angle}°")
time.sleep(0.25) # Debounce + prevent rapid firing
elif btn_down.value() == 0:
set_angle(current_angle - STEP)
print(f"Angle: {current_angle}°")
time.sleep(0.25)
time.sleep(0.05)
Challenges
⭐ Core
Write a servo_wave(speed) function that sweeps the servo from 0° to 180° and back three times. speed should be a delay value (small = fast, large = slow). Test it with servo_wave(0.02) and servo_wave(0.1).
⭐⭐ Extension
Write map_distance_to_angle(distance, min_dist, max_dist) that maps a distance value linearly to 0°–180°. Connect the ultrasonic sensor and use this function to make the servo track detected distance — move the servo to show how close the object is.
⭐⭐⭐ Stretch
Create a “servo recorder”: use a global list angle_history = []. Write log_angle(angle) that calls set_angle() AND appends the angle to the history list. Write replay() that moves through the history list, calling set_angle() for each stored angle. Record a sequence using the buttons, then replay it. Print the history before replaying.
Common Mistakes & Debugging
Servo jitters or vibrates Often a power issue — servo draws more current than the ESP32 can supply. Try a separate 5V supply for the servo.
UnboundLocalError: local variable 'x' referenced before assignment You have a global variable x and tried to use it inside a function, but also assigned to it in the same function without declaring global x. Either add global x at the top of the function, or pass the value as a parameter.
Servo only moves to extremes Duty cycle range needs calibrating. Test values like servo.duty(30) through servo.duty(130) manually to find the range.
Forgetting servo.deinit() If you restart your script without running servo.deinit(), the PWM channel may not reinitialise correctly. Press Ctrl+C, reset the board, and try again.
Key Vocabulary
| Term | Definition |
|---|---|
| servo | A motor with a built-in control circuit that moves to and holds a precise angle |
| PWM | Pulse Width Modulation — varying the pulse width to communicate a value |
| duty cycle | The proportion of time a PWM signal is HIGH |
| local variable | A variable created inside a function — only exists within that function |
| global variable | A variable created outside functions — accessible everywhere |
| scope | The region of code where a variable is visible |
| global keyword | Declares that a variable inside a function refers to the global variable of that name |