Skill Level: Intermediate
Table Of Contents
- Introduction
- What Is Needed
- Background Information
- Connecting The Servo
- Installing The Servo Library
- Basic Servo Operation
- Calibrating Servo Positions
- Sweeping The Servo Through Given Angles
- Additional Resources
- Summary
Introduction
This tutorial will show you how to connect, configure, calibrate, and control a servo motor with a CircuitPython compatible microcontroller board. It will also demonstrate how to sweep a servo through specified angles.
A basic understanding of electronics and programming is expected along with some familiarity with the CircuitPython ecosystem. If you are new to CircuitPython, or would just like to refresh your knowledge, please see our Getting Started With CircuitPython On Compatible Microcontroller Boards and Getting Started With CircuitPython On Raspberry Pi With Blinka tutorials before proceeding with this one.
The resources created for this tutorial are available on GitHub for your reference.
What Is Needed
- Linux, macOS, Or Windows Based Computer With A USB Port
- Mu Python Editor (recommended)
- 5-6 V DC / 2 A External Power Supply (recommended)
- Alligator Clip Test Leads (recommended, available on Adafruit and SparkFun)
- CircuitPython Compatible Microcontroller Board With Compatible USB Cable (available on Adafruit)
- 3 x Male/Female Jumper Wires (available on Adafruit and Arrow)
- Small Servo Motor (available on Adafruit and SparkFun)
Background Information
The typical small servo motor, or simply servo, is an electromechanical device that translates electronic signals into distinct angular positions of 0-180 degrees on a motor’s shaft. It is constructed with a small DC motor, a gearing system that reduces the motor’s speed and increases torque, position control circuitry, and a potentiometer to provide position feedback. Servos are often used in radio-controlled vehicles and small robots for steering and other functions. The shaft of the servo motor is toothed so that items, such as gears, wheels, levers (horns), etc. can be attached to it.
Servos come in a variety of sizes. The larger the size, the more torque it produces, along with the associated extra power consumption. The standard sizes are generally categorized as micro, standard, and large, but some manufacturers provide additional sizes.
Three color coded wires are used to power and control a servo: power (red or brown), ground (black or brown), and signal (white, orange, or yellow). The signal wire is fed a pulse every 20 milliseconds with the width of that pulse, normally 1-2 ms, used to determine the position of the servo. Typically, 1 ms corresponds to the 0° position (farthest most counterclockwise), 2 ms for 180° (farthest most clockwise), and 1.5 ms for the center of rotation at 90°. However, these values can vary greatly by manufacturer and adjustments may need to be made with the control software or circuity to obtain better precision. Some servos only provide 90 degrees of rotation and others even allow for more than 180 degrees. There are even continuous rotation servos, but the pulse width for those is used to control their speed instead of position as is the case for standard servos.
I am using the TowerPro SG-5010 standard sized servo with Adafruit’s Feather M4 Express microcontroller board connected to a macOS based computer with the Mu Python editor for this tutorial. I also verified that the CircuitPython program created in this tutorial works on the Raspberry Pi Pico, although the servo’s pin name must be changed from D5 to GP5. If you are using a different CircuitPython compatible board, computer setup, or code editor, the vast majority of this tutorial should still apply, however, some minor changes may be necessary.
If you need assistance with your particular setup, post a question in the comments section below and I, or someone else, can try to help you.
Connecting The Servo
Before connecting any power or circuitry to your CircuitPython compatible microcontroller board, connect your board to your computer, replace and save (run) the code.py program on your CIRCUITPY drive with the following bare minimum program,
while True: pass
and then disconnect your board from your computer. This resets all of the board’s GPIO pins back to their default states and ensures no outputs are being driven that may damage your board or connected electronics during wiring and power-up operations.
Attach one of the horns, or levers, that came with your servo to the motor shaft of the servo itself. This will help you visually see the position (angle) to which the servo is set.
WARNING:
Before connecting a servo to your microcontroller board, consult the specifications for both the servo and the microcontroller board you plan to use in order to understand the servo’s operating limits and control parameters along with the microcontroller board’s 5 V supply capabilities. If you try to drive the servo to a position outside of its range, it can stall and cause a current spike that may damage your servo or microcontroller board, or at the very least, cause your board to reset. In addition, the current consumed by a servo increases with increasing loads and hits its maximum when the servo stalls due to too much load. For these reasons, I strongly recommend using an external power supply capable of providing 5-6 V DC @ 2A to power your servo until you understand its operation in your project. Once you understand the servo’s current needs (no pun intended), you can probably power a single small servo under light loads from the microcontroller board directly. Using large or multiple servos usually requires external power even during normal operation.
Consult the specifications of your microcontroller board to determine which GPIO pins support pulse width modulation (PWM) as it is required to control a servo motor. Most CircuitPython compatible microcontroller boards provide PWM support on at least some, if not all, of their GPIO pins. I am using GPIO pin D5 for the Feather M4 Express in this tutorial, but you may need to select and use a different pin depending on your board’s capabilities.
Recommended Hook Up
If you are using an external power supply, use some combination of power supply cables, jumper wires, alligator clip test leads, etc. to
- Connect the servo’s power wire to the power supply’s power terminal,
- Connect the servo’s ground wire to both the power supply’s ground terminal and the GND pin of the microcontroller board, and
- Connect the servo’s signal wire to GPIO pin D5 of the microcontroller board.
Alternate Hook Up
If you are powering the servo directly from a microcontroller board capable of supplying 5 V, use jumper wires to
- Connect the servo’s power wire to the 5V, VUSB, etc. pin of the microcontroller board,
- Connect the servo’s ground wire to the GND pin of the microcontroller board, and
- Connect the servo’s signal wire to GPIO pin D5 of the microcontroller board.
Once the servo is attached, connect your microcontroller board to your computer with the USB cable and power it up.
Installing The Servo Library
Before installing any new libraries, make sure you are running the latest stable release of CircuitPython on your compatible microcontroller board.
Although you can drive a servo directly with a PWM capable GPIO pin directly with CircuitPython, we will take advantage of the servo class within the existing Adafruit_CircuitPython_Motor library to control our servo motor within this tutorial.
Download the latest stable CircuitPython libraries bundle and copy the adafruit_motor library directory to the lib directory of your board’s CIRCUITPY drive.
Basic Servo Operation
Now that our servo is connected and our library is installed, let’s see how we can make the servo move.
Open Mu or your favorite code editor and create a CircuitPython program with the code shown below.
from time import sleep import board import pwmio from adafruit_motor import servo DEBUG = True servo_a_pin = pwmio.PWMOut(board.D5, frequency=50) servo_a = servo.Servo(servo_a_pin, min_pulse=1000, max_pulse=2000) def basic_operations(): if DEBUG: print("Setting angle to 90 degrees.") servo_a.angle = 90 sleep(5) if DEBUG: print("Setting angle to 0 degrees.") servo_a.angle = 0 sleep(5) if DEBUG: print("Setting angle to 90 degrees.") servo_a.angle = 90 sleep(5) if DEBUG: print("Setting angle to 180 degrees.") servo_a.angle = 180 sleep(5) while True: basic_operations()
We begin by importing our necessary libraries, including the adafruit_motor
library on line 4.
Line 6 defines the debugging mode of the program. If enabled (DEBUG
is set to True
), then debugging messages are printed to the serial console. Set DEBUG
to False
for normal operation.
Line 8 defines the PWM pin we are using to control the servo. Again, D5
is used here, but you may need a different PWM capable pin name for your particular board, e.g. GP5
for the Raspberry Pi Pico. This definition also specifies the PWM frequency that will be utilized. Since servos require a pulse of a particular width to be provided every 20 ms, this corresponds to a 50 Hz (1 / 0.02) PWM frequency.
The instance of the servo (servo_a
), using the servo_a_pin
pin, is then created and initialized on line 9. The optional minimum and maximum pulse width arguments of 1000
and 2000
specify the pulse widths (in microseconds) of the 0° and 180° positions (angles) for the attached servo motor respectively. If they were not specified, the library’s default values of 750 and 2250 would have been used instead. Since these values determine the endpoints of the servo’s range, I chose to be conservative in setting their values so as not to push the servo beyond its natural range. We will be adjusting these values later for the specific servo in use.
Since we will be covering a couple of examples of functionality, I separated them into their own distinct functions. Therefore, the basic_operations()
function, this first example, is the only function I am currently calling within the program’s endless loop (beginning on line 25). This example function contains the most basic operations of telling the servo to move to its center point along with both endpoints with 5 second delays in between. The angles are also printed to the serial console if the debugging mode is enabled.
If there is something that needs further explanation, please let me know in the comment section and I will try to answer your question.
If you are using the Mu editor with a CircuitPython compatible microcontroller board, click the Serial icon within the menu bar at the top to open the serial console. It will appear at the bottom of the Mu editor’s window and will be used to view the program’s output. Save the program as code.py to the top level of the board’s CIRCUITPY drive and it will begin running automatically.
We now have a functional servo. You will probably notice that the 0° and 180° endpoints are a bit off, but we will fix that in the next section. You may also notice that the servo moved in the opposite direction than you expected. Some servos move clockwise for smaller angles while others move in the opposite direction.
Calibrating Servo Positions
In this section, we will fix the angles of the 0° and 180° endpoint positions.
WARNING:
The operations that we will be performing in this section could easily cause your servo to try to position itself outside of its natural operating range. This will usually cause the current spike we mentioned earlier that could damage your microcontroller board or servo. If you are not using an external power supply, you may want to skip this section, and instead, move ahead to the next section using the conservative 0° and 180° pulse width values we have used so far.
Remember the optional arguments of 1000
and 2000
that we used to create the servo_a
instance? As a reminder, these correspond to the minimum pulse width (0°) and maximum pulse width (180°) endpoint settings allowed by the adafruit_motor
library. We are about to adjust those in order to properly have the servo move to those positions with better accuracy while also giving us the full 0 to 180 degrees range.
Each servo will have a different full scale range that depends greatly on the manufacturer. This will be a trial-and-error process that could require quite a few attempts before settling on the right values.
Add the following constants just after the DEBUG
constant to define our endpoint values. Notice that we are beginning with the conservative 1000
and 2000
microsecond values.
SERVO_A_0_DEGREES_PULSE_WIDTH = 1000 SERVO_A_180_DEGREES_PULSE_WIDTH = 2000
Then replace the original servo_a
instance definition
servo_a = servo.Servo(servo_a_pin, min_pulse=1000, max_pulse=2000)
with the new definition that utilizes those constants.
servo_a = servo.Servo(servo_a_pin, actuation_range=180, min_pulse=SERVO_A_0_DEGREES_PULSE_WIDTH, max_pulse=SERVO_A_180_DEGREES_PULSE_WIDTH)
I also added the optional actuation_range
argument (with a default value of 180) to the definition. On the off chance that your servo only has a 90, 135, etc. degree actuation range, set the actuation_range
argument and the name of the SERVO_A_180_DEGREES_PULSE_WIDTH
constant accordingly. You would also need to adjust the basic_operations()
function to account for a more limited actuation range.
Save (run) the program and it should work exactly the same as it did before since we are still effectively using the same endpoint values.
Now let’s begin the iterative adjustment process. Every round involves performing each of the following steps.
- Slightly decrease the 0° position (from the initial
1000
value) and increase the 180° position (from the initial2000
value) constant values. - Rerun the program.
- When the program states it is setting the servo to 0 degrees, take note of how close the new position is to the expected 0° endpoint (perpendicular to the servo body). If the servo stalls or does not move, go back to the last setting.
- When the program states it is setting the servo to 180 degrees, take note of how close the new position is to the expected 180° endpoint (perpendicular to the servo body). If the servo stalls or does not move, go back to the last setting.
- If you need more time to inspect the servo positions (angles), increase the values of the
sleep()
delays. - Depending on how close the last set and expected endpoints are relative to each other, choose the next endpoint values to try. For instance, if the endpoints are still far away from each other, try changing the value by 100. If they are very close, try a value change of 5.
- Stop the process once you reach the full 0-180 degrees range.
The final values obtained for the TowerPro SG-5010 servo I am using ended up being 500 and 2468 for the 0° and 180° endpoints respectively. As you can see, these values are quite different from the standard 1000 and 2000 conventions. These values are unique to the specific servo you are using. You should have a distinct set of constant definitions for each servo you have in your project.
Sweeping The Servo Through Given Angles
This final example demonstrates how to sweep through positions on your servo.
Add the following function to your program, after the basic_operations()
function but before the program’s endless loop.
def servo_sweep(start_angle, stop_angle, step_angle=1, step_time=0.015): if start_angle != int(start_angle) or start_angle < 0 or start_angle > 180: print(f"ERROR: The start_angle value of {start_angle} is invalid.") return if stop_angle != int(stop_angle) or stop_angle < 0 or stop_angle > 180: print(f"ERROR: The stop_angle value of {stop_angle} is invalid.") return if step_angle != int(step_angle) or step_angle < 1 or step_angle > abs(stop_angle - start_angle): print(f"ERROR: The step_angle value of {step_angle} is invalid.") return if step_time < 0: print(f"ERROR: The step_time value of {step_time} is invalid.") return if DEBUG: print(f"Sweeping angle from {start_angle} to {stop_angle} degrees in increments of {step_angle} degree(s) with a {step_time} s step time.") if start_angle < stop_angle: for angle in range(start_angle, stop_angle + 1, step_angle): if DEBUG: print(f"Setting angle to {angle} degrees.") servo_a.angle = angle sleep(step_time) else: for angle in range(start_angle, stop_angle - 1, -step_angle): if DEBUG: print(f"Setting angle to {angle} degrees.") servo_a.angle = angle sleep(step_time)
The servo_sweep()
function takes two regular arguments and two optional arguments and will step the servo through the angular positions based on the specified arguments.
start_angle
is the value of the starting angle (position) with expected values of 0-180 degrees.stop_angle
is the value of the stopping angle (position) with expected values of 0-180 degrees.step_angle
is the value of the optional stepping angle with expected values of 1-180 degrees. It defaults to 1 degree.step_time
is the value of the optional stepping time. It defaults to 0.015 seconds.
The function begins by verifying the input arguments and notifying the user of any errors. It then proceeds to show the user the operation it is about to perform (line 14), if debugging is enabled, and then performs the actual stepping operation (beginning on line 15). The operation is split into two separate for-loops depending on whether the starting angle is smaller or larger than the ending angle. Each for-loop will step through the appropriate positions, print the angle that is about to be set (if debugging is enabled), move the servo to the new angle, and then delay for the appropriate amount of time.
Now, let’s utilize the above function within our program. Add the following sweep_operations()
function, after the servo_sweep()
function but before the endless loop, that demonstrates how to use the previous servo_sweep()
sweeping function with a few example sweeps.
def sweep_operations(): servo_sweep(0, 180) sleep(5) servo_sweep(180, 0) sleep(5) servo_sweep(45, 135, 15, 1) sleep(5) servo_sweep(180, 0, 45, 1) sleep(5)
servo_sweep(0, 180)
– Sweeps the servo from 0° to 180° in 1° (default) increments with 0.015 s (default) delays in between.
servo_sweep(180, 0)
– Sweeps the servo from 180° to 0° in 1° (default) increments with 0.015 s (default) delays in between.
servo_sweep(45, 135, 15, 1)
– Sweeps the servo from 45° to 135° in 15° increments with 1 second delays in between.
servo_sweep(180, 0, 45, 1)
– Sweeps the servo from 180° to 0° in 45° increments with 1 second delays in between.
Finally, replace the endless loop with the following to call the new sweep_operations()
demonstration function instead of the original basic_operations()
routine.
while True: # basic_operations() sweep_operations()
Save your program and you should see the servo stepping through the various angles.
Before we end, now would be a good time to reset all of the board’s GPIO pins back to their default states again. This ensures no outputs are being driven that may damage your board or connected electronics when plugging in your board for your next project. Copy the current code.py program to servo_basic.py and update code.py to contain only the following bare minimum program.
while True: pass
Additional Resources
The following is a list of additional resources you may find helpful.
- Servomotor on Wikipedia
- Servo (radio control) on Wikipedia
- Servos Explained by SparkFun
- CircuitPython Servo section of CircuitPython Essentials Adafruit Learning Guide
- Using Servos With CircuitPython and Arduino Adafruit Learning Guide
Summary
In this tutorial, we learned how to connect, configure, calibrate, and control a servo motor with a CircuitPython compatible microcontroller board.
Specifically, we learned
- how servos generally work and some of the differences you may find among the servos produced by different manufacturers,
- how to connect a servo to your microcontroller board,
- how to configure and set angular positions on a servo, and
- how to calibrate the endpoints of the servo’s range.
I also provided an example that demonstrates how to sweep the servo through given angles.
Hopefully, this tutorial provided you with a good understanding of how to incorporate servos into your own project.
The final source code used for this tutorial is available on GitHub. The GitHub version of the code is fully commented to include additional information, such as the program’s description, circuit connections, library references, code clarifications, and other details. The comments are also Sphinx compatible in case you want to generate the code documentation yourself.
Thank you for joining me on this journey and I hope you enjoyed the experience. Please feel free to share your thoughts or questions in the comments section below.
This tutorial is provided as a free service to our valued readers. Please help us continue this endeavor by considering a donation.
Leave a Comment