Dimming LED with PWM

Let's create a dimming effect for an LED using PWM on the Raspberry Pi Pico 2 W. We'll gradually increase and decrease the brightness to create a smooth fading effect.

How It Works

We gradually increase the PWM duty cycle from a low value to a high value, creating a fade-in effect. Then we decrease it back down for a fade-out effect. A small delay between each change makes the transition smooth and visible.

The Eye and PWM

When PWM switching happens super quickly, our eyes can't keep up. Instead of seeing the blinking, it just looks like the brightness changes. The longer the LED stays ON (higher duty cycle), the brighter it seems. The shorter it's ON (lower duty cycle), the dimmer it looks.

Hardware Setup

You can use an external LED on any PWM-capable GPIO pin, or use the onboard LED. For the Pico 2 W, the onboard LED requires the cyw43 driver since GPIO25 is controlled by the wireless chip.

OptionGPIO PinPWM Slice
External LEDGPIO 16PWM_SLICE0, Channel A
Pico 2 W OnboardGPIO 25 (via cyw43)PWM_SLICE4, Channel B

Initialize PWM

Embassy makes PWM configuration simple. We create a PWM output with default settings, which gives us control over the duty cycle.

rust
use embassy_rp::pwm::{Pwm, SetDutyCycle};

// For external LED on GPIO 16
let mut pwm = Pwm::new_output_a(p.PWM_SLICE0, p.PIN_16, Default::default());

// For Pico 2 W onboard LED (requires cyw43 setup)
// let mut pwm = Pwm::new_output_b(p.PWM_SLICE4, p.PIN_25, Default::default());

Main Dimming Logic

We create two loops: one that increases the duty cycle from 0% to 100%, and another that decreases it back to 0%. The 8ms delay makes the transition smooth and visible to the eye.

rust
loop {
    // Fade in: 0% to 100%
    for i in 0..=100 {
        Timer::after_millis(8).await;
        let _ = pwm.set_duty_cycle_percent(i);
    }
    
    // Fade out: 100% to 0%
    for i in (0..=100).rev() {
        Timer::after_millis(8).await;
        let _ = pwm.set_duty_cycle_percent(i);
    }
    
    // Pause before repeating
    Timer::after_millis(500).await;
}

Complete Code

rust
#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp::{self as hal, block::ImageDef};
use embassy_rp::pwm::{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());
    
    // External LED on GPIO 16
    let mut pwm = Pwm::new_output_a(p.PWM_SLICE0, p.PIN_16, Default::default());
    
    loop {
        // Fade in
        for i in 0..=100 {
            Timer::after_millis(8).await;
            let _ = pwm.set_duty_cycle_percent(i);
        }
        
        // Fade out
        for i in (0..=100).rev() {
            Timer::after_millis(8).await;
            let _ = pwm.set_duty_cycle_percent(i);
        }
        
        Timer::after_millis(500).await;
    }
}

Understanding set_duty_cycle_percent

The set_duty_cycle_percent method accepts a u8 value from 0 to 100, making it perfect for percentage-based control. This is a convenience function from embedded-hal that internally calculates the correct duty cycle value based on the PWM's maximum duty cycle.

Note

For more precise control: If you need fractional percentages (like 2.5% or 7.5%), you can use set_duty_cycle_fraction which accepts a numerator and denominator. This is useful for servo control where precise pulse widths matter.

For simple LED dimming, set_duty_cycle_percent with whole numbers works perfectly.

Running the Program

bash
# Create project
cargo generate --git https://github.com/ImplFerris/pico2-template.git

# Build and run
cargo run --release

# With debug probe
cargo embed --release

You should see your LED smoothly fade in and out, demonstrating how PWM can create analog-like effects with digital signals.