How to Calibrate Servo Motors for Precise Control with Raspberry Pi
If you’ve ever tried to make a micro servo motor turn to exactly 90 degrees—only to watch it overshoot by 10 degrees and jitter like a caffeine-addicted robot—you know the pain. Micro servo motors are the unsung heroes of countless Raspberry Pi projects, from robotic arms and camera gimbals to animatronic eyes and tiny drawing machines. But here’s the dirty secret: out of the box, most cheap micro servos (like the ubiquitous SG90 or MG90S) are not precise. They drift, they jitter, and their “90-degree” position is anyone’s guess.
This blog post is your hands-on guide to turning that sloppy servo into a precision instrument. We’ll dive into why micro servos misbehave, how to wire them properly to your Raspberry Pi, and—most importantly—how to write calibration routines that give you repeatable, sub-degree accuracy. No fluff, no theory without practice. Just real code, real measurements, and real results.
Why Micro Servos Need Calibration in the First Place
Before we touch a single wire, let’s understand the enemy. A typical micro servo motor is a small DC motor connected to a potentiometer via a gear train. The potentiometer provides position feedback. The servo’s internal controller compares the current position (from the pot) to the desired position (encoded by the PWM signal) and drives the motor to reduce the error.
Sounds simple, right? But here’s where things go sideways for cheap micro servos:
- Manufacturing tolerances: The potentiometer is rarely linear across its full range. Two servos from the same batch can have different center points.
- Pulse width range variation: The standard 1.0 ms to 2.0 ms pulse width range (for 0 to 180 degrees) is a guideline, not a guarantee. Many micro servos respond to a narrower or wider range. Some might start moving at 0.5 ms, others at 1.2 ms.
- Dead band: The “dead band” is the range of pulse width change that produces no motor movement. Cheap servos have a wider dead band, meaning you can send a command that should move the servo 1 degree, but nothing happens.
- Load sensitivity: A micro servo under load (e.g., holding a small camera) will behave differently than unloaded. Calibration under load is essential for real-world projects.
The bottom line: if you send a 1.5 ms pulse and expect exactly 90 degrees, you’re gambling. Calibration replaces gambling with measurement.
Hardware Setup: Wiring Your Micro Servo to a Raspberry Pi
Let’s get physical. You’ll need:
- A Raspberry Pi (any model with GPIO pins; I’m using a Pi 4 for this example)
- A micro servo motor (SG90 or MG90S are perfect for this tutorial)
- A 470 µF electrolytic capacitor (optional but highly recommended)
- A breadboard and jumper wires
- A small ruler or protractor (for visual verification)
- A multimeter or oscilloscope (optional, for advanced debugging)
Pin Connections
| Servo Wire | Color (typical) | Raspberry Pi GPIO | |------------|-----------------|-------------------| | Power (VCC) | Red | 5V pin (Pin 2 or 4) | | Ground (GND) | Brown/Black | GND pin (Pin 6, 9, 14, etc.) | | Signal (PWM) | Orange/Yellow | GPIO 18 (Pin 12) |
Critical warning: Do not power the servo directly from the Raspberry Pi’s 3.3V rail. Micro servos draw significant current (200-500 mA under load), and the Pi’s 3.3V regulator cannot supply that. Use the 5V rail. Even then, a single servo can cause voltage dips that crash your Pi. This is where the 470 µF capacitor comes in: connect it between the 5V and GND rails on your breadboard, as close to the servo as possible. It acts as a local energy reservoir, smoothing out current spikes.
Wiring Diagram (Text Version)
Raspberry Pi 5V (Pin 2) → Breadboard 5V rail → Servo Red wire Raspberry Pi GND (Pin 6) → Breadboard GND rail → Servo Brown wire Raspberry Pi GPIO 18 → Servo Orange wire Capacitor (+) → Breadboard 5V rail Capacitor (-) → Breadboard GND rail
Double-check your connections before powering on. A reversed power wire can destroy the servo’s internal electronics.
Software Foundation: Generating Accurate PWM on Raspberry Pi
The Raspberry Pi’s software PWM (using RPi.GPIO or gpiozero) is notoriously jittery because it relies on the Linux kernel scheduler. For servo control, jitter translates to position wobble. Instead, we’ll use the hardware PWM available on GPIO 18 (and a few other pins) via the pigpio daemon.
Installing pigpio
Open a terminal on your Raspberry Pi and run:
bash sudo apt update sudo apt install pigpio python3-pigpio sudo systemctl enable pigpiod sudo systemctl start pigpiod
The pigpiod daemon runs in the background and provides precise hardware-timed PWM signals. We’ll control it from Python using the pigpio library.
Basic Servo Sweep (Without Calibration)
Let’s write a quick test script to see where our servo thinks 0, 90, and 180 degrees are.
python import pigpio import time
SERVO_GPIO = 18 pi = pigpio.pi() # Connect to local pigpiod
Set PWM frequency to 50 Hz (standard for servos)
pi.setPWMfrequency(SERVO_GPIO, 50)
Enable PWM with a specific pulse width (in microseconds)
def setservoangle(pulsewidthus): pi.setservopulsewidth(SERVOGPIO, pulsewidth_us)
Test positions using "standard" pulse widths
print("Moving to 0 degrees (1000 us)...") setservoangle(1000) time.sleep(2)
print("Moving to 90 degrees (1500 us)...") setservoangle(1500) time.sleep(2)
print("Moving to 180 degrees (2000 us)...") setservoangle(2000) time.sleep(2)
Stop PWM
pi.setservopulsewidth(SERVO_GPIO, 0) pi.stop()
Run this script. Watch your servo. Does it hit 0, 90, and 180? Almost certainly not. On my SG90, 1500 µs gave me about 85 degrees, and 2000 µs overshot to 185 degrees. This is why we calibrate.
The Calibration Process: Finding the True Pulse Width Range
Calibration means finding the actual pulse widths that correspond to your servo’s mechanical limits and your desired angle range. We’ll do this in three phases: finding the endpoints, measuring linearity, and building a calibration curve.
Phase 1: Finding the Mechanical Endpoints
Micro servos have physical stops that prevent rotation beyond ~0° and ~180°. Sending a pulse width that tries to push past these stops can strip gears or burn out the motor. We need to find the pulse widths that just barely reach each stop without straining.
Method: Write a script that slowly increases the pulse width from a safe low value (say 500 µs) until the servo stops moving, then record that value. Do the same from a safe high value (2500 µs) downward.
python import pigpio import time
pi = pigpio.pi() SERVOGPIO = 18 pi.setPWMfrequency(SERVOGPIO, 50)
def findendpoint(startpw, step, direction): """ direction: 1 for increasing, -1 for decreasing Returns the pulse width where the servo stops moving. """ pw = startpw pi.setservopulsewidth(SERVOGPIO, pw) time.sleep(0.5) last_position = input(f"Current pulse width: {pw} us. Is the servo still moving? (y/n): ")
while last_position.lower() == 'y': pw += step * direction pi.set_servo_pulsewidth(SERVO_GPIO, pw) time.sleep(0.3) last_position = input(f"Current pulse width: {pw} us. Still moving? (y/n): ") # Step back one increment to get the last position that caused movement pw -= step * direction return pw print("Finding minimum endpoint (0 degrees)...") minpw = findendpoint(500, 10, 1) # Start at 500 us, increase by 10 us print(f"Minimum pulse width (0 degrees): {min_pw} us")
print("Finding maximum endpoint (180 degrees)...") maxpw = findendpoint(2500, 10, -1) # Start at 2500 us, decrease by 10 us print(f"Maximum pulse width (180 degrees): {max_pw} us")
pi.setservopulsewidth(SERVO_GPIO, 0) pi.stop()
This interactive script asks you to visually confirm movement. For a more automated approach, you could attach a potentiometer or encoder, but visual inspection works for most hobbyists. On my MG90S, I found endpoints at 520 µs and 2480 µs.
Phase 2: Measuring Linearity (Mapping Pulse Width to Angle)
Now we know the pulse widths for 0° and 180°. But is the relationship linear in between? Probably not. We need to measure the actual angle for several intermediate pulse widths.
Method: Attach a lightweight pointer (a paper clip or straw) to the servo horn. Place a protractor or printed angle template behind it. Write a script that steps through pulse widths and records the measured angle.
python import pigpio import time
pi = pigpio.pi() SERVOGPIO = 18 pi.setPWMfrequency(SERVOGPIO, 50)
Use the endpoints we found
minpw = 520 maxpw = 2480
Create 10 evenly spaced test points
testpoints = [minpw + i * (maxpw - minpw) / 9 for i in range(10)]
calibration_data = []
for pw in testpoints: pi.setservopulsewidth(SERVOGPIO, int(pw)) time.sleep(1) # Allow servo to settle measuredangle = float(input(f"Pulse width: {int(pw)} us. Enter measured angle (degrees): ")) calibrationdata.append((int(pw), measured_angle))
pi.setservopulsewidth(SERVO_GPIO, 0) pi.stop()
print("Calibration data (pulse width, measured angle):") for pw, angle in calibration_data: print(f"{pw} us -> {angle} degrees")
This data will look something like this (from a real SG90):
520 us -> 0.0° 740 us -> 22.5° 960 us -> 44.0° 1180 us -> 66.5° 1400 us -> 88.0° 1620 us -> 110.5° 1840 us -> 132.0° 2060 us -> 154.5° 2280 us -> 176.0° 2480 us -> 180.0°
Notice the nonlinearity: the first 220 µs span 22.5 degrees, but the last 200 µs span only 4 degrees. This is typical of cheap potentiometers.
Phase 3: Building a Calibration Curve (Linear Interpolation)
We now have a lookup table. To set the servo to any arbitrary angle, we need to find the corresponding pulse width by interpolating between our measured points. Linear interpolation is sufficient for most hobby projects.
Implementation: Create a function that takes a desired angle and returns the appropriate pulse width using the calibration data.
python def angletopulsewidth(desiredangle, calibrationdata): """ calibrationdata: list of (pulsewidth, measuredangle) tuples, sorted by angle. Returns interpolated pulse width for desiredangle. """ # Sort by angle calsorted = sorted(calibrationdata, key=lambda x: x[1])
# Clamp to valid range if desired_angle <= cal_sorted[0][1]: return cal_sorted[0][0] if desired_angle >= cal_sorted[-1][1]: return cal_sorted[-1][0] # Find surrounding points for i in range(len(cal_sorted) - 1): pw_low, angle_low = cal_sorted[i] pw_high, angle_high = cal_sorted[i+1] if angle_low <= desired_angle <= angle_high: # Linear interpolation fraction = (desired_angle - angle_low) / (angle_high - angle_low) return int(pw_low + fraction * (pw_high - pw_low)) # Fallback (shouldn't reach here) return cal_sorted[-1][0] Now we can command precise angles:
python
Example usage
desired = 90.0 pw = angletopulsewidth(desired, calibrationdata) pi.setservopulsewidth(SERVOGPIO, pw) print(f"To achieve {desired}°, sending {pw} us pulse.")
Testing the Calibration
Let’s verify. Command 0°, 45°, 90°, 135°, and 180° using our calibrated function. Measure the actual angle with a protractor. On my setup, the error dropped from ±5° (uncalibrated) to ±0.5° (calibrated). That’s the difference between a wobbly robot arm and one that draws straight lines.
Advanced Calibration: Dealing with Dead Band and Hysteresis
Micro servos suffer from two additional gremlins: dead band and hysteresis.
Dead band is the range of pulse width change that produces no movement. For example, changing from 1500 µs to 1510 µs might not move the servo at all, but 1500 µs to 1520 µs does. To compensate, you can implement a “dead band avoidance” strategy: when you want to move to a new position, always approach from the same direction (e.g., always increase pulse width to reach the target, or always decrease). This ensures you don’t get stuck in the dead band.
Hysteresis means the servo’s position depends on its previous position. If you approach 90° from 0°, you might get 89.5°, but approaching from 180° gives 90.5°. This is due to gear backlash and friction. To mitigate, always approach your target from the same direction (unidirectional approach). Here’s a simple implementation:
python def set_angle_unidirectional(pi, servo_gpio, target_pw, last_pw, approach_from='low'): """ Approach target_pw from a specific direction to reduce hysteresis. approach_from: 'low' means we first go to a pulse width lower than target, then increase. """ if approach_from == 'low': # First go 50 us below target (or to a known safe low) intermediate = target_pw - 50 pi.set_servo_pulsewidth(servo_gpio, intermediate) time.sleep(0.1) # Allow to settle pi.set_servo_pulsewidth(servo_gpio, target_pw) else: intermediate = target_pw + 50 pi.set_servo_pulsewidth(servo_gpio, intermediate) time.sleep(0.1) pi.set_servo_pulsewidth(servo_gpio, target_pw) return target_pw
This adds a small delay but dramatically improves repeatability.
Automating Calibration with a Feedback Sensor
For truly precise control, you can close the loop with an external angle sensor (e.g., a magnetometer or potentiometer). This turns your servo into a closed-loop system. The Raspberry Pi reads the actual angle and adjusts the PWM until the error is zero. This is overkill for most projects but essential for applications like a telescope focuser or medical pump.
Simplified closed-loop code:
python import pigpio import time import Adafruit_ADS1x15 # For reading an external potentiometer
pi = pigpio.pi() SERVOGPIO = 18 adc = AdafruitADS1x15.ADS1115()
def readanglefromsensor(): # Assume ADC value maps linearly to 0-180 degrees value = adc.readadc(0, gain=1) return (value / 32767.0) * 180.0
def setangleclosedloop(desiredangle, tolerance=0.5): currentangle = readanglefromsensor() error = desiredangle - currentangle
while abs(error) > tolerance: # Simple proportional control pw_adjustment = int(error * 10) # Tune this gain current_pw = pi.get_servo_pulsewidth(SERVO_GPIO) new_pw = max(500, min(2500, current_pw + pw_adjustment)) pi.set_servo_pulsewidth(SERVO_GPIO, new_pw) time.sleep(0.05) current_angle = read_angle_from_sensor() error = desired_angle - current_angle This loop runs until the servo is within 0.5° of the target. It compensates for load changes, temperature drift, and gear backlash in real time.
Putting It All Together: A Complete Calibration Script
Here’s a self-contained script that performs the full calibration (endpoint finding + linearity mapping) and saves the calibration data to a file for reuse.
python
!/usr/bin/env python3
import pigpio import time import json
class ServoCalibrator: def init(self, gpio=18): self.pi = pigpio.pi() self.gpio = gpio self.pi.setPWMfrequency(self.gpio, 50) self.calibrationdata = [] self.minpw = None self.max_pw = None
def find_endpoints(self, step=10): # Find minimum pw = 500 self.pi.set_servo_pulsewidth(self.gpio, pw) time.sleep(0.5) while True: response = input(f"PW {pw} us. Moving? (y/n): ") if response.lower() != 'y': break pw += step self.pi.set_servo_pulsewidth(self.gpio, pw) time.sleep(0.3) self.min_pw = pw - step # Find maximum pw = 2500 self.pi.set_servo_pulsewidth(self.gpio, pw) time.sleep(0.5) while True: response = input(f"PW {pw} us. Moving? (y/n): ") if response.lower() != 'y': break pw -= step self.pi.set_servo_pulsewidth(self.gpio, pw) time.sleep(0.3) self.max_pw = pw + step print(f"Endpoints: min={self.min_pw} us, max={self.max_pw} us") return self.min_pw, self.max_pw def measure_linearity(self, num_points=10): if self.min_pw is None or self.max_pw is None: raise ValueError("Run find_endpoints first") test_pws = [int(self.min_pw + i * (self.max_pw - self.min_pw) / (num_points-1)) for i in range(num_points)] self.calibration_data = [] for pw in test_pws: self.pi.set_servo_pulsewidth(self.gpio, pw) time.sleep(1) angle = float(input(f"PW {pw} us. Enter angle: ")) self.calibration_data.append((pw, angle)) return self.calibration_data def angle_to_pw(self, desired_angle): cal = sorted(self.calibration_data, key=lambda x: x[1]) if desired_angle <= cal[0][1]: return cal[0][0] if desired_angle >= cal[-1][1]: return cal[-1][0] for i in range(len(cal)-1): pw_low, ang_low = cal[i] pw_high, ang_high = cal[i+1] if ang_low <= desired_angle <= ang_high: frac = (desired_angle - ang_low) / (ang_high - ang_low) return int(pw_low + frac * (pw_high - pw_low)) return cal[-1][0] def save_calibration(self, filename='servo_cal.json'): data = { 'min_pw': self.min_pw, 'max_pw': self.max_pw, 'calibration_data': [(pw, ang) for pw, ang in self.calibration_data] } with open(filename, 'w') as f: json.dump(data, f) print(f"Calibration saved to {filename}") def load_calibration(self, filename='servo_cal.json'): with open(filename, 'r') as f: data = json.load(f) self.min_pw = data['min_pw'] self.max_pw = data['max_pw'] self.calibration_data = [(pw, ang) for pw, ang in data['calibration_data']] print(f"Calibration loaded from {filename}") def cleanup(self): self.pi.set_servo_pulsewidth(self.gpio, 0) self.pi.stop() Example usage
if name == "main": cal = ServoCalibrator(gpio=18) try: # First time: run calibration cal.findendpoints() cal.measurelinearity(numpoints=10) cal.savecalibration()
# Test a few angles for angle in [0, 45, 90, 135, 180]: pw = cal.angle_to_pw(angle) cal.pi.set_servo_pulsewidth(cal.gpio, pw) print(f"Commanded {angle}° -> PW {pw} us") time.sleep(2) finally: cal.cleanup() Run this once to calibrate your servo. On subsequent runs, you can skip the calibration and just load the saved data with cal.load_calibration().
Common Pitfalls and How to Avoid Them
Pitfall 1: Power starvation. If your servo jitters or moves erratically, it’s almost always power related. Add a larger capacitor (1000 µF or more) or power the servo from a separate 5V supply with a common ground.
Pitfall 2: Using software PWM. RPi.GPIO’s software PWM is fine for LEDs but terrible for servos. Always use hardware PWM via pigpio or a servo driver board like the PCA9685.
Pitfall 3: Ignoring load. Calibrate your servo under the load it will actually carry. A servo calibrated unloaded will behave differently when holding a weight. If your project involves a gripper or arm, attach the load before calibrating.
Pitfall 4: Forgetting to stop PWM. Leaving PWM active when your script exits can cause the servo to hold its last position indefinitely, draining the battery or overheating. Always call pi.set_servo_pulsewidth(gpio, 0) in a finally block.
Pitfall 5: Over-tightening the horn. If the servo horn is screwed on too tightly, it can bind against the case, causing erratic movement. Leave a tiny gap.
Real-World Example: A Calibrated Camera Pan-Tilt System
Let’s apply our calibration to a common project: a two-axis camera pan-tilt. You’ll need two micro servos (pan and tilt), each calibrated separately. The calibration process is identical for both, but you must save separate calibration files (e.g., pan_cal.json and tilt_cal.json).
Code snippet for pan-tilt control:
python class PanTilt: def init(self, pangpio=18, tiltgpio=19): self.pi = pigpio.pi() self.pan = ServoCalibrator(pangpio) self.tilt = ServoCalibrator(tiltgpio) self.pan.loadcalibration('pancal.json') self.tilt.loadcalibration('tiltcal.json')
def look_at(self, pan_angle, tilt_angle): pan_pw = self.pan.angle_to_pw(pan_angle) tilt_pw = self.tilt.angle_to_pw(tilt_angle) self.pi.set_servo_pulsewidth(self.pan.gpio, pan_pw) self.pi.set_servo_pulsewidth(self.tilt.gpio, tilt_pw) def cleanup(self): self.pi.set_servo_pulsewidth(self.pan.gpio, 0) self.pi.set_servo_pulsewidth(self.tilt.gpio, 0) self.pi.stop() Usage
pt = PanTilt() pt.lookat(90, 45) # Center both axes time.sleep(2) pt.lookat(45, 30) pt.cleanup()
With calibration, the camera will point exactly where you command, every time. Without calibration, you’d be guessing.
When Calibration Isn’t Enough: Upgrading Your Servo
If you’ve followed this guide and still can’t get better than ±1° accuracy, your servo hardware may be the limit. Consider upgrading to:
- Digital micro servos (e.g., Hitec HS-35HD): They have faster response, narrower dead bands, and better linearity.
- Metal gear servos (e.g., MG90S over SG90): Less gear backlash, especially under load.
- Servos with feedback (e.g., Feetech STS3215): These are serial bus servos that report their actual position, eliminating the need for calibration altogether.
But for 90% of Raspberry Pi projects, a well-calibrated SG90 or MG90S is more than adequate.
Final Thoughts on Servo Calibration
Calibrating a micro servo motor with a Raspberry Pi isn’t just about fixing a broken angle. It’s about transforming a cheap, mass-produced component into a reliable, repeatable actuator. The process—finding endpoints, measuring linearity, interpolating, and compensating for dead band—is a microcosm of what professional robotics engineers do with far more expensive hardware.
The scripts in this guide give you a reusable framework. Run the calibration once, save the data, and every future project that uses that servo will benefit. Your robot arm will draw straight lines. Your camera gimbal will track smoothly. Your animatronic eye will stop looking like it’s having a seizure.
And when a friend asks, “How did you get that servo so accurate?” you can smile and say, “I calibrated it. It’s not magic—it’s measurement.”
Copyright Statement:
Author: Micro Servo Motor
Source: Micro Servo Motor
The copyright of this article belongs to the author. Reproduction is not allowed without permission.
Recommended Blog
- Using Raspberry Pi to Control Servo Motors in Automated Packaging and Labeling Systems
- How to Control Servo Motors Using Raspberry Pi and the pigpio Library for Precision Robotics
- Implementing Servo Motors in Raspberry Pi-Based Automated Sorting and Packaging Systems
- Creating a Servo-Controlled Automated Trash Can Lid with Raspberry Pi
- How to Use Raspberry Pi to Control Servo Motors in Automated Assembly Lines
- Implementing Servo Motors in Raspberry Pi-Based Automated Sorting and Packaging Systems
- Implementing Servo Motors in Raspberry Pi-Based Automated Sorting Lines
- Building a Servo-Powered Automated Sorting Robot with Raspberry Pi and Sensors
- Integrating Multiple Servo Motors with Raspberry Pi
- Implementing Servo Motors in Raspberry Pi-Based Automated Warehouse Systems
About Us
- Lucas Bennett
- Welcome to my blog!
Hot Blog
- How to Choose the Right Motor for High-Temperature Applications
- The Role of Micro Servo Motors in Smart Farming
- Micro vs Standard Servo: Speed vs Torque Trade-Offs
- Micro Servos Integrated with Wireless RF Modules
- Implementing Servo Motors in Raspberry Pi-Based Automated Sorting and Packaging Systems
- Rozum Robotics' Micro Servo Motors: Advanced Features for Home Automation Projects
- How Gear Materials Affect Servo Motor Performance Under Varying Signal Resilience
- Vector's Micro Servo Motors: Compact and Lightweight for Pan-Tilt Systems
- How to Build a Remote-Controlled Car with a 3D-Printed Chassis
- The Impact of Gear Materials on Servo Motor Heat Generation
Latest Blog
- Voltage Drop at Wire Leads: Spec vs Real-World Conditions
- How to Calibrate Servo Motors for Precise Control with Raspberry Pi
- Micro Servo Motor Gear Types: Plastic vs Metal Gears
- Smart Micro Servo Motors: The Next Generation of Automation
- PWM Control in Power Systems: Applications and Design Considerations
- How to Build a Remote-Controlled Car with a Lightweight Body
- The Physics of Feedback in Micro Servo Systems
- The Technology That Makes Micro Servo Motors Work
- The Role of PCB Design in Home Automation
- How to Connect a Micro Servo Motor to Arduino MKR WAN 1300
- How to Use Thermal Management to Extend Motor Warranty
- Waterproof Micro Servo Types for Outdoor Use
- The Future of Micro Servo Motors: Insights from Leading Brands
- How to Implement Torque and Speed Control in Cranes
- Understanding the Basics of Radio Frequency Control in RC Cars
- How to Connect a Micro Servo Motor to Arduino MKR NB 1500
- Micro Servo Motor Integration into RC Car Cockpits & Mimic Movements
- Using Micro Servos in DIY Smart Locks with Bluetooth/WiFi Control
- Micro Servo Motor Price Comparison: Which Offers the Best Value?
- Diagnosing and Fixing RC Car Battery Overheating Issues