Raspberry Pi Servo Pan-Tilt Project with Remote Control

I recently purchased a small servo pan-tilt module, which is a mechanical structure or device used to control servos. It is typically employed for directing cameras, sensors, or other devices. In this project, we will integrate it with the Raspberry Pi and achieve remote control. Below is the final outcome.

Raspberry Pi Servo Pan-Tilt
Table of Contents

About Servo

Let’s first delve into the basic principles and operational processes of a servo motor.

Although there are various types of servos, essentially, they are motors designed to rotate to a specific angle. This project utilizes the SG90 micro servo, featuring three wires: red for power, which can be connected to a 5V voltage; brown for ground; and orange as the signal wire for control.

Internally, the servo has a reference pulse with a period of 20 milliseconds and a pulse width of 1.5 milliseconds, corresponding to a reference voltage V0. To control the servo, we input a control pulse with a 20-millisecond period and a certain duty cycle through the signal wire. This pulse undergoes processing by the modulation chip, becoming a biased voltage V.

Internally, the servo first decides whether to rotate in the forward or reverse direction by comparing the polarity of ΔV = V − V0. Simultaneously, the servo contains a balance potentiometer. As the internal gears rotate, the potentiometer gradually reduces the voltage difference ΔV. When the motor reaches the specified angle, ΔV becomes precisely 0, and the servo ceases rotation.

Pulse Width/msDuty Cycle / %Angle / °
0.52.50
1.0545
1.57.590
2.010135
2.512.5180

Extrapolation of Key Data

Building on the aforementioned data, we need to perform a series of calculations for key parameters.

Firstly, based on the provided table, if we want to adjust the servo to a specific angle θ (0≤θ≤180), the corresponding pulse duty cycle can be calculated using the following formula:

Pulse duty cycle formula

Additionally, to avoid potential conflicts between commands, we must consider the rotation time of the servo. The servo’s rotation speed is approximately 0.2 seconds per 60 degrees, i.e., 0.003 seconds/degree. The servo accuracy is 180°/1024 ≈ 0.18°, and the corresponding pulse duty cycle accuracy is (12.5 – 2.5)/1024 ≈ 0.01. Therefore, during the incremental rotation process (every 0.18°, practically taken as 0.2°), the time interval for issuing servo commands should not be less than 0.2 × 0.003 = 0.0006 seconds, i.e., 0.001 seconds. For any specified angle, the interval between two commands should be longer than 180 × 0.2/60 = 0.4 seconds, which is the time to rotate from 0 degrees to 180 degrees.

Furthermore, it is necessary to consider the potential limitations imposed by the fixed board structure on the servo. The servo may not be able to rotate fully to 180 degrees due to structural constraints. For example, certain servos may only rotate between 90 and 180 degrees in the vertical direction, or they could get stuck. To prevent damage to the servo, it is crucial to be aware of its rotation range and reflect it in the program.

Python Control of Servo Motors

In this Raspberry Pi project, we control a servo motor through Python programming. To achieve precise control of the servo motor, we utilize the PWM method from the RPi.GPIO library. Below is a simple yet powerful GPIO control logic example demonstrating how to initialize, start, adjust duty cycle, and stop PWM output:

				
					import RPi.GPIO as GPIO
# Set BCM encoding mode
GPIO.setmode(GPIO.BCM)
# Turn off warnings
GPIO.setwarnings(False)
# Set the channel to output mode
GPIO.setup(channel, GPIO.OUT)
# Create a new PWM object and specify the frequency
pwm = GPIO.PWM(channel, frequency)
# Start PWM output and specify the initial duty cycle
pwm.start(dutycycle)
# Change the duty cycle
pwm.ChangeDutyCycle(dutycycle)
# Stop PWM
pwm.stop()
# Clean up GPIO settings
GPIO.cleanup()
				
			

To modularize the servo motor Pan-Tilt, we have designed two classes to encapsulate GPIO operations. The Rotation class represents a single SG90 servo motor, encapsulating all GPIO operations. The Steering class represents the entire Pan-Tilt, and it contains two Rotation objects. In the Rotation class, we distinguish between stepwise rotation and specified rotation.

Below is the code for the Rotation class, which includes handling parameters such as servo signal line connection, rotation angle range, and initial angle:

				
					# -*- coding: UTF-8 -*-
import RPi.GPIO as GPIO
import time
class Rotation:
    # Constants for servo control
    frequency = 50      # Pulse frequency (Hz)
    delta_theta = 0.2   # Stepwise rotation interval (degrees)
    min_delay = 0.0006  # Theoretical time consumption to rotate delta_theta (s)
    max_delay = 0.4     # Time consumption to rotate from 0 to 180 (s)
    def __init__(self, channel, min_theta, max_theta, init_theta=0):
        '''
        Constructor:
            channel: Raspberry Pi pin number to which the servo signal line is connected (BCM encoding)
            min_theta: Minimum angle of servo rotation
            max_theta: Maximum angle of servo rotation
            init_theta: Initial angle of the servo
        '''
        self.channel = channel
        self.min_theta = max(0, min(min_theta, 180))
        self.max_theta = max(0, min(max_theta, 180))
        self.init_theta = max(self.min_theta, min(init_theta, self.max_theta))
        # Calculate duty cycle for minimum and maximum angles
        self.min_dutycycle = 2.5 + self.min_theta * 10 / 180
        self.max_dutycycle = 2.5 + self.max_theta * 10 / 180
    def setup(self):
        '''
        Initialization: Set up GPIO, PWM, and initial servo position
        '''
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        GPIO.setup(self.channel, GPIO.OUT)
        self.pwm = GPIO.PWM(self.channel, Rotation.frequency)  # PWM
        self.dutycycle = 2.5 + self.init_theta * 10 / 180        # Initial value of pulse duty cycle
        self.pwm.start(self.dutycycle)  # Rotate the servo to the initial position
        time.sleep(Rotation.max_delay)
    def positiveRotation(self):
        '''
        Positive stepwise rotation: Rotate delta_theta degrees each time it is called
        '''
        self.dutycycle = min(self.dutycycle + Rotation.delta_theta * 10 / 180, self.max_dutycycle)
        self.pwm.ChangeDutyCycle(self.dutycycle)
        time.sleep(Rotation.min_delay)
    def reverseRotation(self):
        '''
        Reverse rotation: Rotate delta_theta degrees each time it is called
        '''
        self.dutycycle = max(self.dutycycle - Rotation.delta_theta * 10 / 180, self.min_dutycycle)
        self.pwm.ChangeDutyCycle(self.dutycycle)
        time.sleep(Rotation.min_delay)
    def specifyRotation(self, theta):
        '''
        Rotate to the specified angle
        '''
        if 0 <= theta <= 180:
            self.dutycycle = 2.5 + theta * 10 / 180
            self.pwm.ChangeDutyCycle(self.dutycycle)
            time.sleep(Rotation.max_delay)
    def cleanup(self):
        '''
        Cleanup: Stop PWM and clean up GPIO
        '''
        self.pwm.stop()
        time.sleep(Rotation.min_delay)
        GPIO.cleanup()
				
			

The code for the Steering class is provided below. Its two Rotation attributes represent the horizontal and vertical servo motors:

				
					import Rotation
class Steering:
    def __init__(self, channelH, min_thetaH, max_thetaH,
                 channelV, min_thetaV, max_thetaV, init_thetaH=0, init_thetaV=0):
        '''
        Constructor:
            channelH: Signal channel for the horizontal servo motor
            min_thetaH: Minimum angle of rotation for the horizontal servo motor
            max_thetaH: Maximum angle of rotation for the horizontal servo motor
            channelV: Signal channel for the vertical servo motor
            min_thetaV: Minimum angle of rotation for the vertical servo motor
            max_thetaV: Maximum angle of rotation for the vertical servo motor
            init_thetaH: Initial angle for the horizontal servo motor
            init_thetaV: Initial angle for the vertical servo motor
        '''
        # Create instances of the Rotation class for horizontal and vertical servos
        self.hRotation = Rotation(channelH, min_thetaH, max_thetaH, init_thetaH)
        self.vRotation = Rotation(channelV, min_thetaV, max_thetaV, init_thetaV)
    def setup(self):
        '''
        Setup: Initialize GPIO and PWM for both horizontal and vertical servos
        '''
        self.hRotation.setup()
        self.vRotation.setup()
    def Up(self):
        '''
        Stepwise upward rotation: Rotates 0.2 degrees each time it is called
        '''
        self.vRotation.positiveRotation()
    def Down(self):
        '''
        Stepwise downward rotation: Rotates 0.2 degrees each time it is called
        '''
        self.vRotation.reverseRotation()
    def Left(self):
        '''
        Stepwise leftward rotation: Rotates 0.2 degrees each time it is called
        '''
        self.hRotation.positiveRotation()
    def Right(self):
        '''
        Stepwise rightward rotation: Rotates 0.2 degrees each time it is called
        '''
        self.hRotation.reverseRotation()
    def specify(self, thetaH, thetaV):
        '''
        Rotate to the specified angles
        '''
        self.hRotation.specifyRotation(thetaH)
        self.vRotation.specifyRotation(thetaV)
    def cleanup(self):
        '''
        Cleanup: Stop PWM and clean up GPIO for both servos
        '''
        self.hRotation.cleanup()
        self.vRotation.cleanup()
				
			

Test

The code and testing results for controlling the servo motors are provided below:

				
					# This is a test program for controlling servo motors
from Steering_Module.Steering import Steering
import time
# Initialize the Pan-Tilt object
steer = Steering(14, 0, 180, 15, 90, 180, 36, 160)  # Initial position at 36 and 160
# Set up the Pan-Tilt
steer.setup()
time.sleep(2)
# Continuous rotation
for _ in range(0, 900):
    steer.Up()
for _ in range(0, 900):
    steer.Down()
for _ in range(0, 900):
    steer.Left()
for _ in range(0, 900):
    steer.Right()
# Rotate to specified angles
steer.specify(80, 120)
steer.specify(20, 100)
steer.specify(170, 180)
# Clean up resources
steer.cleanup()

				
			

Remote Control Principle

To simplify the data transmission process, we adopt the UDP protocol to send remote control commands to the Raspberry Pi. In this communication method, the computer continuously sends remote control commands through the UDP protocol’s socket, and the Raspberry Pi continuously receives these commands in a loop. Unlike the TCP protocol, UDP is connectionless, so there is no need to establish or maintain a connection during the communication. Given that this scenario involves close-range testing and UDP emphasizes fast transmission and low latency, it is well-suited for such simple applications with high real-time requirements.

Now, we are moving on to implement the remote control program, which means continuously turning the Pan-Tilt to a specified area when the arrow key is held down and stopping the rotation when released. To achieve this functionality, we set a status variable on the computer side. It is set to 1 when the key is pressed and 0 when released. Simultaneously, we continuously send this status through the socket. On the Raspberry Pi side, we loop to receive commands and read the status. Based on the status value, we make corresponding movements with the Pan-Tilt.

Since the Raspberry Pi side needs to constantly call the socket’s receive function to receive commands and control the Pan-Tilt simultaneously, the default recvfrom function is blocking. To prevent the program from getting “stuck,” we use the setblocking() method to set the socket to non-blocking mode. We set the parameter to 0, so when calling recvfrom, if there is no data in the buffer, it throws a socket.error exception. We simply ignore this exception to ensure the normal operation of the program.

PC Program Settings

				
					using System;
using System.Net;
using System.Net.Sockets;
using System.Windows.Forms;
public partial class Form1 : Form
{
    // Byte array to represent the state of remote control commands
    private byte[] state = null;
    // UdpClient for sending remote control commands over UDP
    private UdpClient udpClient = null;
    // Constructor for the form
    public Form1()
    {
        InitializeComponent();
        // Initialize the state byte array with default values
        state = new byte[] { 0xff, 0x00, 0x00, 0x00, 0x00 };
        // Set the interval for the timer (in milliseconds)
        this.timer1.Interval = 50;
        // Enable the "Start" button and disable the "Stop" button by default
        this.button1.Enabled = true;
        this.button2.Enabled = false;
    }
    // Event handler for key press events
    private void Form1_KeyDown(object sender, KeyEventArgs e)
    {
        // Update the state byte array based on the pressed key
        switch (e.KeyCode)
        {
            case Keys.Up:
                state[1] = 0x01;
                break;
            case Keys.Down:
                state[2] = 0x01;
                break;
            case Keys.Left:
                state[3] = 0x01;
                break;
            case Keys.Right:
                state[4] = 0x01;
                break;
            default:
                break;
        }
    }
    // Event handler for key release events
    private void Form1_KeyUp(object sender, KeyEventArgs e)
    {
        // Update the state byte array based on the released key
        switch (e.KeyCode)
        {
            case Keys.Up:
                state[1] = 0x00;
                break;
            case Keys.Down:
                state[2] = 0x00;
                break;
            case Keys.Left:
                state[3] = 0x00;
                break;
            case Keys.Right:
                state[4] = 0x00;
                break;
            default:
                break;
        }
    }
    // Event handler for the "Start" button click
    private void button1_Click(object sender, EventArgs e)
    {
        // Start the timer for sending UDP packets
        timer1.Start();
        // Create a new UdpClient and connect to the specified IP address and port
        udpClient = new UdpClient();
        udpClient.Connect(IPAddress.Parse("192.168.191.2"), 9999);
        // Disable the "Start" button and enable the "Stop" button
        this.button1.Enabled = false;
        this.button2.Enabled = true;
    }
    // Event handler for the timer tick event
    private void timer1_Tick(object sender, EventArgs e)
    {
        // Send the current state byte array over UDP
        udpClient.Send(state, state.Length);
    }
}
				
			

Raspberry Pi Program

Here is the Python program on the Raspberry Pi, where the previously written Steering module needs to be imported:

				
					import socket
from Steering_Module.Steering import Steering
'''
Servo Control Program
'''
HOST = '192.168.191.2'
PORT = 9999
buffSize = 1024
closeNow = False
state = ['\xff', '\x00', '\x00', '\x00', '\x00']  # Control status variable
# Initialize socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind((HOST, PORT))
server.setblocking(0)  # Set to non-blocking mode
# Initialize servo module
steer = Steering(14, 0, 180, 15, 90, 180, 36, 160)
steer.setup()
print('Start!')
while closeNow is False:
    try:
        data, address = server.recvfrom(buffSize)  # Non-blocking mode, recvfrom returns immediately, throws socket.error exception if no data
        if (len(data) == 5 and data[0] == '\xff' and data[1] == '\xff' and  # End the program if it's a stop signal
                data[2] == '\xff' and data[3] == '\xff' and data[4] == '\xff'):
            print("Exit!")
            closeNow = True
        elif (len(data) == 5 and data[0] == '\xff'):  # Update the status variable based on the control signal received
            state[1] = data[1]
            state[2] = data[2]
            state[3] = data[3]
            state[4] = data[4]
    except socket.error:
        pass
    # Perform corresponding actions based on the values of the status variable
    if state[1] == '\x01':
        steer.Up()
        continue
    elif state[2] == '\x01':
        steer.Down()
        continue
    elif state[3] == '\x01':
        steer.Left()
        continue
    elif state[4] == '\x01':
        steer.Right()
        continue
    else:
        pass

				
			

You Might Be Interested

raspberry pi autostart
How to Auto Start Programs on Raspberry Pi

Automating program startup on the Raspberry Pi can be achieved through various methods. Editing the “/etc/rc.local” file or using desktop applications, while simpler, may not

raspberry pi
What is Raspberry Pi?

Raspberry Pi, a revolutionary single-board computer introduced by the Raspberry Pi Foundation, has become a global sensation, initially designed for educational purposes. With its integrated

Scroll to Top