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
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 Color | Function | Pico 2 Connection |
|---|---|---|
| Brown/Black | Ground | GND |
| Red | Power (4.8-6V) | VBUS (5V) |
| Orange/Yellow | Signal (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.

Pulse width determines servo angle
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.
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.
// 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
#![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;
}
}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.