Servo Motors

Servo motors let you control position accurately. Unlike regular DC motors that spin continuously, a servo moves to a specific angle and stays there. Perfect for robotics, camera gimbals, and automated mechanisms.

Hardware

We'll use the SG90 micro servo motor - small, cheap, and commonly found in electronics kits. It can rotate from 0° to 180° based on the PWM signal you send it.

SG90 Micro Servo Motor

SG90 Micro Servo Motor

Servo Basics

A typical hobby servo has three wires: Ground (usually brown or black), Power (usually red), and Signal (usually orange or yellow). The signal wire expects a PWM signal that tells the servo which position to move to.

Wire ColorFunctionPico 2 Connection
Brown/BlackGroundGND
RedPower (4.8-6V)VBUS (5V)
Orange/YellowSignal (PWM)GPIO 15

How Servo Control Works

Servos operate on a 50Hz frequency, meaning they expect a control pulse every 20 milliseconds. The width of that pulse determines the servo's position.

Servo PWM position diagram

Pulse width determines servo angle

Important

Standard vs Reality: You'll often see "standard" values like 1.0ms for 0°, 1.5ms for 90°, and 2.0ms for 180°. However, cheap servos rarely follow these exactly.

My servo required 0.5ms for minimum position, 1.5ms for center, and 2.4ms for maximum position. This is normal and expected.

Always calibrate your specific servo. Treat published values as starting points, not absolutes.

Calculating Duty Cycle

The duty cycle represents the percentage of time the signal stays high during each 20ms cycle. For my servo:

0° Position

0.5ms pulse width = (0.5 / 20) × 100 = 2.5% duty cycle

90° Position

1.5ms pulse width = (1.5 / 20) × 100 = 7.5% duty cycle

180° Position

2.4ms pulse width = (2.4 / 20) × 100 = 12% duty cycle

PWM Configuration for 50Hz

To generate a 50Hz PWM signal, we need to configure the TOP and divider values. For the RP2350 running at 150MHz, we can use TOP = 46,874 with divider = 64.

rust
use embassy_rp::pwm::{Config as PwmConfig, Pwm, SetDutyCycle};

let mut servo_config: PwmConfig = Default::default();
servo_config.top = 46_874;
servo_config.divider = 64.into();

let mut pwm = Pwm::new_output_a(
    p.PWM_SLICE7,
    p.PIN_15,
    servo_config
);

Setting Servo Position

Since we need fractional percentages (2.5%, 7.5%, 12%), we use set_duty_cycle_fraction which accepts a numerator and denominator.

rust
// 0 degrees: 2.5% = 25/1000
let _ = pwm.set_duty_cycle_fraction(25, 1000);
Timer::after_secs(2).await;

// 90 degrees: 7.5% = 75/1000
let _ = pwm.set_duty_cycle_fraction(75, 1000);
Timer::after_secs(2).await;

// 180 degrees: 12% = 120/1000
let _ = pwm.set_duty_cycle_fraction(120, 1000);
Timer::after_secs(2).await;

Complete Servo Code

rust
#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp::{self as hal, block::ImageDef};
use embassy_rp::pwm::{Config as PwmConfig, Pwm, SetDutyCycle};
use embassy_time::Timer;

use panic_probe as _;
use defmt_rtt as _;

#[unsafe(link_section = ".start_block")]
#[used]
pub static IMAGE_DEF: ImageDef = hal::block::ImageDef::secure_exe();

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());
    
    // Configure PWM for 50Hz
    let mut servo_config: PwmConfig = Default::default();
    servo_config.top = 46_874;
    servo_config.divider = 64.into();
    
    let mut pwm = Pwm::new_output_a(
        p.PWM_SLICE7,
        p.PIN_15,
        servo_config
    );
    
    loop {
        // 0 degrees
        let _ = pwm.set_duty_cycle_fraction(25, 1000);
        Timer::after_secs(2).await;
        
        // 90 degrees
        let _ = pwm.set_duty_cycle_fraction(75, 1000);
        Timer::after_secs(2).await;
        
        // 180 degrees
        let _ = pwm.set_duty_cycle_fraction(120, 1000);
        Timer::after_secs(2).await;
    }
}
Tip

If your servo jitters or doesn't move to the correct positions, you need to calibrate it. Start with these values and adjust the numerators up or down until you find the exact pulse widths that work for your specific servo.