Skip to content

5. Controlling a Positional Servo Motor with PWM (Basic)

This guide explains how to control a servo motor using Pulse Width Modulation (PWM) on the Raspberry Pi. We'll use a C++ program that interfaces with the Linux PWM subsystem to precisely position a servo motor.

Required Hardware

  • Raspberry Pi (any model with hardware PWM support)
  • Standard servo motor (Here, we use FS90 from FEETECH)
  • Jumper wires
  • Breadboard (optional, but recommended)

Hardware Setup

This guide uses GPIO 12 (PWM Channel 0) for the servo connection. Connect the components as follows:

  1. Connect the signal wire (orange for FS90) to GPIO 12
  2. Connect the ground wire (brown for FS90) to GND
  3. Connect the power wire (red for FS90) to 5V
graph LR
    subgraph RPi["Raspberry Pi"]
        GPIO12["GPIO 12 (PWM)"]
        V5["5V"]
        GND["GND"]
    end

    subgraph Servo["Servo Motor"]
        Signal["Signal (Orange)"]
        Power["Power (Red)"]
        Ground["Ground (Brown)"]
    end

    GPIO12 --- Signal
    V5 --- Power
    GND --- Ground

    style Servo fill:#50c878

Note: For larger servos or when controlling multiple servos, use an external 5V power supply to prevent overloading the Raspberry Pi's power regulator.

Servo Motor Basics

Servo motors are controlled by sending PWM (Pulse Width Modulation) signals:

  • Pulse width determines position for FS90:
    • 0.5ms pulse (~500μs): 0 degrees
    • 1.5ms pulse (~1500μs): 90 degrees
    • 2.5ms pulse (~2500μs): 180 degrees

PWM Setup Configuration

Before writing any code, you need to activate the PWM functionality on your Raspberry Pi. By default, the PWM channels are not activated on the GPIO pins.

Enabling PWM on GPIO Pins

The Raspberry Pi has two PWM channels (PWM0 and PWM1) that can be mapped to specific GPIO pins. You'll need to activate the appropriate channel for servo control.

Available PWM Channel Options

You have four possible configurations:

PWM Channel GPIO Pin Function Alt Mode dtoverlay Configuration
PWM0 12 4 Alt0 dtoverlay=pwm,pin=12,func=4
PWM0 18 2 Alt5 dtoverlay=pwm,pin=18,func=2
PWM1 13 4 Alt0 dtoverlay=pwm,pin=13,func=4
PWM1 19 2 Alt5 dtoverlay=pwm,pin=19,func=2

For this tutorial, we'll use GPIO 12 mapped to PWM0.

Configuration Steps

  1. Edit the configuration file:

    /boot/firmware/config.txt
    
  2. Add the following line at the end of the file:

    dtoverlay=pwm,pin=12,func=4
    
  3. Reboot your Raspberry Pi:

After rebooting, your Raspberry Pi will have PWM0 activated on GPIO 12, ready for connecting a servo motor.

Code Explanation

This program uses the Linux sysfs interface to control hardware PWM on the Raspberry Pi to position a servo motor:

#include <atomic>
#include <csignal>
#include <fstream>
#include <iostream>
#include <string>
#include <thread>

using namespace std::chrono_literals;

// Atomic boolean for safe access between main thread and signal handler
std::atomic<bool> keep_running(true);

// Signal handler function that sets the flag to false when Ctrl+C is pressed
void signal_handler(int)
{
    keep_running = false;
}

bool write_sysfs(const std::string& path, const std::string& value)
{
    std::ofstream ofs(path);  // Open file for writing
    if (!ofs)
    {
        std::cerr << "Failed to open " << path << std::endl;
        return false;
    }
    ofs << value;  // Write the value to the file
    if (!ofs)
    {
        std::cerr << "Failed to write to " << path << std::endl;
        return false;
    }
    return true;
}

int main()
{
    // Define paths to PWM sysfs interfaces
    const std::string pwm_chip = "/sys/class/pwm/pwmchip0";
    const std::string pwm_channel = pwm_chip + "/pwm0";

    // Register signal handler for graceful termination
    std::signal(SIGINT, signal_handler);

    // Export PWM channel 0 for use
    if (!write_sysfs(pwm_chip + "/export", "0"))
    {
        std::cerr
            << "PWM channel 0 might already be exported or error occurred."
            << std::endl;
    }
    std::this_thread::sleep_for(1s);  // Wait for export to complete

    // Configure PWM parameters
    // Set period to 20ms (20,000,000 nanoseconds) - standard for servo control
    if (!write_sysfs(pwm_channel + "/period", "20000000"))
        return 1;

    // Initialize duty cycle to 0
    if (!write_sysfs(pwm_channel + "/duty_cycle", "0"))
        return 1;

    // Enable PWM output
    if (!write_sysfs(pwm_channel + "/enable", "1"))
        return 1;

    // Main control loop - continues until Ctrl+C is pressed
    while (keep_running)
    {
        // Set servo to 0 degrees position (0.5ms pulse)
        std::cout << "Setting 0 degrees (0.5ms pulse)..." << std::endl;
        if (!write_sysfs(pwm_channel + "/duty_cycle", "500000"))
            break;
        std::this_thread::sleep_for(2s);  // Hold position for 2 seconds
        if (!keep_running)
            break;

        // Set servo to 90 degrees position (1.5ms pulse)
        std::cout << "Setting 90 degrees (1.5ms pulse)..." << std::endl;
        if (!write_sysfs(pwm_channel + "/duty_cycle", "1500000"))
            break;
        std::this_thread::sleep_for(2s);  // Hold position for 2 seconds
        if (!keep_running)
            break;

        // Set servo to 180 degrees position (2.5ms pulse)
        std::cout << "Setting 180 degrees (2.5ms pulse)..." << std::endl;
        if (!write_sysfs(pwm_channel + "/duty_cycle", "2500000"))
            break;
        std::this_thread::sleep_for(2s);  // Hold position for 2 seconds
        if (!keep_running)
            break;

        // Return to 90 degrees position
        std::cout << "Setting 90 degrees (1.5ms pulse)..." << std::endl;
        if (!write_sysfs(pwm_channel + "/duty_cycle", "1500000"))
            break;
        std::this_thread::sleep_for(2s);  // Hold position for 2 seconds
        if (!keep_running)
            break;
    }

    // Cleanup operations before exit

    // Return servo to 0 degrees position
    std::cout << "Setting 0 degrees (0.5ms pulse) before exit..." << std::endl;
    write_sysfs(pwm_channel + "/duty_cycle", "500000");
    std::this_thread::sleep_for(1s);  // Wait for servo to reach position

    // Disable PWM output
    std::cout << "Disabling PWM output." << std::endl;
    write_sysfs(pwm_channel + "/enable", "0");

    // Unexport the PWM channel
    if (!write_sysfs(pwm_chip + "/unexport", "0"))
    {
        std::cerr << "Failed to unexport PWM channel 0. You can ignore this if "
                     "you want to keep it enabled."
                  << std::endl;
    }

    return 0;
}

Key Program Elements

  1. Signal Handling:

    • The program sets up a signal handler to catch Ctrl+C (SIGINT)
    • This allows for clean shutdown when the user terminates the program
  2. PWM Configuration:

    • The program uses GPIO 12, which is connected to hardware PWM channel 0
    • PWM is accessed through the Linux sysfs interface at /sys/class/pwm/pwmchip0
    • Period is set to 20,000,000 nanoseconds (20ms or 50Hz)
  3. Servo Position Control:

    • 0 degrees: 500,000 nanoseconds (0.5ms) pulse width
    • 90 degrees: 1,500,000 nanoseconds (1.5ms) pulse width
    • 180 degrees: 2,500,000 nanoseconds (2.5ms) pulse width
  4. Cleanup Process:

    • Returns servo to 0 degrees position before exit
    • Disables PWM output
    • Unexports the PWM channel
  5. Helper Functions:

    • write_sysfs: Utility function to write values to sysfs files
    • signal_handler: Handles SIGINT to enable clean program termination

Compiling and Running

  1. Save the code to a file, e.g., servo_control.cpp

  2. Compile the program:

    g++ -o servo_control servo_control.cpp -std=c++17 -pthread
    
  3. Run the program with root privileges (needed for PWM access):

    sudo ./servo_control
    
  4. The program will cycle the servo through 0°, 90°, 180°, and back to 90° positions. Press Ctrl+C to exit the program.

How It Works in Detail

  1. PWM Export: The program exports PWM channel 0 to make it available for use.

  2. Setting Period: The period is set to 20ms (20,000,000 nanoseconds), which is the standard for most servo motors.

  3. Setting Duty Cycle: The duty cycle is what controls the servo position:

    • 0° position: 2.5% duty cycle (0.5ms pulse in a 20ms period)
    • 90° position: 7.5% duty cycle (1.5ms pulse in a 20ms period)
    • 180° position: 12.5% duty cycle (2.5ms pulse in a 20ms period)
  4. Enable PWM: Writing "1" to the enable file starts the PWM output.

  5. Position Loop: The program cycles through different positions, waiting 2 seconds at each position.

  6. Graceful Shutdown: When Ctrl+C is pressed, the program:

    • Returns the servo to the 0° position
    • Disables the PWM output
    • Unexports the PWM channel

Troubleshooting

  1. Servo Not Moving:

    • Check connections, especially the signal wire to GPIO 12
    • Ensure your servo is receiving adequate power
    • Some servos may need slightly different pulse widths for the same angles
  2. Permission Issues:

    • The "Failed to open" error usually means the program needs administrator privileges
    • In that case, run with sudo
  3. Jittery Movement:

    • This could indicate insufficient power
    • Use an external 5V power supply for the servo
  4. PWM Already in Use:

    • If another process is using the PWM channel, you might get errors
    • The program handles this gracefully and continues execution

Taking It Further

You can expand this program to:

  1. Create a servo controller that accepts angle input from the user
  2. Implement smooth transitions between positions
  3. Control multiple servos for more complex motion
  4. Build a web interface to control servo positions remotely
  5. Use the servo for robotics projects like robotic arms or pan-tilt mechanisms

Safety Considerations

  1. Power Requirements: Servos can draw substantial current. For larger servos or multiple servos, use an external power supply.

  2. Mechanical Limits: Respect the mechanical limits of your servo. Some servos have physical stops and forcing them beyond these limits can damage gears.

  3. Initial Position: It's good practice to set a known initial position (as this program does) to prevent unexpected movement.