We'll be using CircuitPython for this project. Are you new to using CircuitPython? No worries, there is a full getting started guide here.
Adafruit suggests using the Mu editor to edit your code and have an interactive REPL in CircuitPython. You can learn about Mu and its installation in this tutorial.
Be sure to load CircuitPython on your board. See this tutorial for the process.
This project needs CircuitPython 4.0.0 or later. All modern versions should work.
Like the circuit, the code is also straight-forward.
It works as a simple linear, looping state machine that moves to the next state each time the encoder button is pressed:
- Mode 0 - adjust the servo angle
- Mode 1 - sweep the servo repeated between 0 and 180 and back, allowing the time to sweep to be adjusted
- Mode 2 - adjust the lower bound of the pulse width
- Mode 3 - adjust the upper bound of the pulse width
To start it initializes hardware and sets up some variables, including a debouncer for the encoder switch. There's nothing special about this and it can be seen in the full listing below.
The main loop is made up of 6 sections.
Housekeeping
We start by grabbing the current time. This is the elapsed time actually, as we just need to measure differences in time, not absolute time of day. We also update the debouncer now.
now = time.monotonic()
button.update()
Sweep
Next, if the current mode is the sweep mode, the servo angle will need to be updated if the time between steps has passed. If the angle reaches either extreme, the direction of rotation is reversed.
if mode == 1:
if now >= (last_movement_at + sweep_time / 36):
last_movement_at = now
angle += delta
if (angle > 180) or (angle < 0):
delta *= -1
angle += delta
Mode
If the button was just pressed, we change the mode. If we are switching into mode 0 the angle is set to 0. If we are going into mode 1 we set the time it takes to sweep and next time to step the servo. If we are going into mode 2 or 3, the angle is set to the appropriate extreme.
if button.fell:
servo.angle = 0
if mode == 0:
mode = 1
sweep_time = 1.0
last_movement_at = now
elif mode == 1:
mode = 2
angle = 0
elif mode == 2:
mode = 3
angle = 180
elif mode == 3:
mode = 0
angle = 0
Adjust
If the encoder switch wasn't just pressed, the encoder is use to adjust the relevant parameter (depending on the current mode).
else:
current_position, change = get_encoder_change(rotary_encoder, current_position)
if change != 0:
if mode == 0:
angle = min(180, max(0, angle + change * 5))
elif mode == 1:
sweep_time = min(5.0, max(1.0, sweep_time + change * 0.1))
elif mode == 2:
min_pulse_index = min(10, max(min_pulse_index + change, 0))
test_servo = servo.Servo(pwm,
min_pulse=min_pulses[min_pulse_index],
max_pulse=max_pulses[max_pulse_index])
test_servo.angle = 0
elif mode == 3:
max_pulse_index = min(10, max(max_pulse_index + change, 0))
test_servo = servo.Servo(pwm,
min_pulse=min_pulses[min_pulse_index],
max_pulse=max_pulses[max_pulse_index])
test_servo.angle = 180
Display
Once any mode change or adjustment has been made, the display is updated as appropriate for the mode.
oled.fill(0)
if mode == 0:
oled.text("Angle: {0}".format(angle), 0, 0)
elif mode == 1:
oled.text("Sweep time: {0}".format(sweep_time), 0, 0)
elif mode == 2:
oled.text("Min width: {0}".format(min_pulses[min_pulse_index]), 0, 0)
elif mode == 3:
oled.text("Max width: {0}".format(max_pulses[max_pulse_index]), 0, 0)
oled.show()
test_servo.angle = angle
And that's it. Below is the full code. You can download it by clicking "Download Project Bundle." You'll need the debouncer as well which can be found in the library bundle.
# SPDX-FileCopyrightText: 2018 Dave Astels for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Servo Tester
Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!
Written by Dave Astels for Adafruit Industries
Copyright (c) 2018 Adafruit Industries
Licensed under the MIT license.
All text above must be included in any redistribution.
"""
import time
import board
import busio
import digitalio
import rotaryio
import pwmio
import adafruit_ssd1306
from adafruit_motor import servo
from adafruit_debouncer import Debouncer
#--------------------------------------------------------------------------------
# Initialize Rotary encoder
button_io = digitalio.DigitalInOut(board.D12)
button_io.direction = digitalio.Direction.INPUT
button_io.pull = digitalio.Pull.UP
button = Debouncer(button_io)
rotary_encoder = rotaryio.IncrementalEncoder(board.D10, board.D11)
#--------------------------------------------------------------------------------
# Initialize I2C and OLED
i2c = busio.I2C(board.SCL, board.SDA)
oled = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)
oled.fill(0)
oled.show()
min_pulses = [ 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000]
max_pulses = [2000, 2050, 2100, 2150, 2200, 2250, 2300, 2350, 2400, 2450, 2500]
min_pulse_index = 10
max_pulse_index = 0
#-------------------------------------------------------------------------------
# Initialize servo
pwm = pwmio.PWMOut(board.D5, frequency=50)
test_servo = servo.Servo(pwm, min_pulse=1000, max_pulse=2000)
test_servo.angle = 0
current_position = None # current encoder position
change = 0 # the change in encoder position
angle = 0
mode = 0
sweep_time = 1.0
last_movement_at = 0.0
delta = 5
def get_encoder_change(encoder, pos):
new_position = encoder.position
if pos is None:
return (new_position, 0)
else:
return (new_position, new_position - pos)
#--------------------------------------------------------------------------------
# Main loop
while True:
now = time.monotonic()
button.update()
if mode == 1:
if now >= (last_movement_at + sweep_time / 36):
last_movement_at = now
angle += delta
if (angle > 180) or (angle < 0):
delta *= -1
angle += delta
if button.fell:
servo.angle = 0
if mode == 0:
mode = 1
sweep_time = 1.0
last_movement_at = now
elif mode == 1:
mode = 2
angle = 0
elif mode == 2:
mode = 3
angle = 180
elif mode == 3:
mode = 0
angle = 0
else:
current_position, change = get_encoder_change(rotary_encoder, current_position)
if change != 0:
if mode == 0:
angle = min(180, max(0, angle + change * 5))
elif mode == 1:
sweep_time = min(5.0, max(1.0, sweep_time + change * 0.1))
elif mode == 2:
min_pulse_index = min(10, max(min_pulse_index + change, 0))
test_servo = servo.Servo(pwm,
min_pulse=min_pulses[min_pulse_index],
max_pulse=max_pulses[max_pulse_index])
angle = 0
elif mode == 3:
max_pulse_index = min(10, max(max_pulse_index + change, 0))
test_servo = servo.Servo(pwm,
min_pulse=min_pulses[min_pulse_index],
max_pulse=max_pulses[max_pulse_index])
angle = 180
oled.fill(0)
if mode == 0:
oled.text("Angle: {0}".format(angle), 0, 0)
elif mode == 1:
oled.text("Sweep time: {0}".format(sweep_time), 0, 0)
elif mode == 2:
oled.text("Min width: {0}".format(min_pulses[min_pulse_index]), 0, 0)
elif mode == 3:
oled.text("Max width: {0}".format(max_pulses[max_pulse_index]), 0, 0)
oled.show()
test_servo.angle = angle
After you've done this, your CIRCUITPY drive should look like this:
Page last edited December 03, 2025
Text editor powered by tinymce.