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.
| Option | GPIO Pin | PWM Slice |
|---|---|---|
| External LED | GPIO 16 | PWM_SLICE0, Channel A |
| Pico 2 W Onboard | GPIO 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.
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.
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
#![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.
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
# Create project
cargo generate --git https://github.com/ImplFerris/pico2-template.git
# Build and run
cargo run --release
# With debug probe
cargo embed --releaseYou should see your LED smoothly fade in and out, demonstrating how PWM can create analog-like effects with digital signals.