6.14. LED dimming with PWM¶
The Generating analog with PWM and an RC filter page used an RC low-pass filter to extract a DC voltage from a PWM signal. For an LED, the filter is not needed – the human eye itself does the averaging.
When an LED is switched on and off faster than about 60 Hz, the visual system stops resolving individual pulses and perceives a steady brightness equal to the average light output. A 50 % duty cycle reads as roughly half brightness; 25 % as a quarter; 10 % as dim.
The wiring is the same as for a static external LED on
GPIO output – a current-limiting
resistor in series with the LED, sized using the rules from
Electronics basics. The change is only in
software: the pin runs as a PWM output
instead of a plain Pin.OUT.
6.14.1. Picking the frequency¶
For LED dimming the PWM frequency only has to clear the eye’s flicker threshold:
Below ~60 Hz the eye sees the pulses outright.
Below ~200 Hz peripheral vision and rapid eye motion can still reveal flicker.
1 kHz is comfortably above all of that and is a typical default.
There is no upper bound that matters for a small LED on a GPIO; anything from 1 kHz to 10 kHz behaves the same to the eye.
6.14.2. Fading¶
A fade-in / fade-out loop sweeps the duty cycle from off to fully on and back, dwelling briefly at each step:
import time
from machine import PWM, Pin
led = PWM(Pin("P7"), freq=1000, duty_u16=0)
while True:
for d in range(0, 65535, 256):
led.duty_u16(d)
time.sleep_ms(5)
for d in range(65535, 0, -256):
led.duty_u16(d)
time.sleep_ms(5)
At 1 kHz PWM and 5 ms steps the eye sees a smooth fade in both directions, with the apparent brightness tracking the duty value.
Perceived brightness is not strictly linear in duty cycle –
the eye’s response follows roughly a square or cube law – so
a linear sweep of duty_u16 does not look like a linear
sweep of brightness. For a perceptually smoother fade, step
the duty on a curve.
A convenient integer-only trick is to step an 8-bit counter
and use its square as the duty cycle. 255 × 255 = 65025 is
within rounding of full scale, so the sweep covers the whole
range:
import time
from machine import PWM, Pin
led = PWM(Pin("P7"), freq=1000, duty_u16=0)
while True:
for step in range(256):
led.duty_u16(step * step) # 0..65025, roughly quadratic
time.sleep_ms(5)
for step in range(255, -1, -1):
led.duty_u16(step * step)
time.sleep_ms(5)
The fade now feels roughly even in apparent brightness from off to full.