Brightness is a duty cycle
A digital pin is only ever on or off. analogWrite switches it on and off hundreds of times a second and varies how long it stays on in each cycle. The LED follows too quickly for the eye, so you see the average.
On and off, very quickly
A digital pin has two states: HIGH, the board's logic voltage, and LOW, 0 V.
There is no setting in between. So analogWrite(pin, 128) does not put 2.5 V
on the pin. It switches the pin fully on and fully off, over and over, and
makes the on part of each cycle half as long as the whole.
That fraction is the duty cycle. It runs from 0, always off, to 255, always on, and the name for the technique is PWM, pulse-width modulation.
How fast
On an Uno's pin 9 the cycle repeats 490 times a second, so each one lasts about 2 ms. The ESP32, ESP32-S3 and Pico cores use 1000 times a second by default. Either is far too fast for an eye to follow. The LED genuinely flashes, but what you see is the average.
The LED keeps up easily. Nothing on this block slows it down: no capacitor, no driver, just the LED and its resistor. Each time the pin goes HIGH, the full 13 mA flows at once (on an Uno), and each time it goes LOW, it stops.
The average is what counts
At a duty of 64, a quarter of 255, the pin is HIGH a quarter of the time, so the average current is a quarter of 13 mA, about 3.3 mA. That is also what a battery powering the project would see.
The ceiling is the same as for digitalWrite. PWM can make the LED dimmer than full, never brighter. On a 3.3 V board the full is about 2 mA, so the whole range is squeezed into what the previous article worked out.
Which pins
On an Uno only six pins can do this: 3, 5, 6, 9, 10 and 11, each marked with a tilde (~) on the board. On the ESP32 boards and the Pico, nearly every output pin can. The sketch uses D9 on an Uno for that reason, and the wiring from the first blink already works.
The code
Ramps analogWrite from 0 to 255 and back, one step every 8 ms. Change LED_PIN to the PWM pin you wired SIGNAL to.
/*
XL LED - fade with analogWrite TK01 / /p/tk01
Wiring. Count from the square pad on the TinkerBlock board, LED side
up, header at the bottom:
GND -> GND
NC -> nothing (both NC pins are unconnected on the board)
SIGNAL -> a PWM pin: D9 on an Uno, GPIO 4 on an ESP32 or ESP32-S3,
GP15 on a Raspberry Pi Pico
Arduino IDE
Tools > Board your board, e.g. ESP32S3 Dev Module
Tools > Port the one that appears when you plug in
Tools > USB CDC On Boot Enabled (ESP32-S3 only)
No library needed. ESP32 boards need board package 2.0 or later.
*/
// A PWM pin. Uno: 9. ESP32, ESP32-S3: 4. Pico: 15.
const int LED_PIN = 4;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
// 0 is always LOW, 255 is always HIGH; in between, the pin spends
// that fraction of every PWM period HIGH.
for (int duty = 0; duty <= 255; duty++) {
analogWrite(LED_PIN, duty);
delay(8);
}
for (int duty = 255; duty >= 0; duty--) {
analogWrite(LED_PIN, duty);
delay(8);
}
}The fade takes about two seconds each way, and it will look wrong: quick to brighten, then almost no change over the top half. That is not the sketch. It is how the eye works, and a fade that looks even is in the next article.
The same fade in MicroPython. machine.PWM runs the pin at 1 kHz, and duty_u16 takes 0 to 65535 rather than analogWrite's 0 to 255.
"""
XL LED - fade with PWM, MicroPython TK01 / /p/tk01
Wiring. Count from the square pad on the TinkerBlock board, LED side
up, header at the bottom:
GND -> GND
NC -> nothing (both NC pins are unconnected on the board)
SIGNAL -> GPIO 4 on an ESP32 or ESP32-S3, GP15 on a Raspberry Pi Pico
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine and time are built in.
"""
from machine import Pin, PWM
import time
# A PWM pin. ESP32, ESP32-S3: 4. Pico: 15.
LED_PIN = 4
led = PWM(Pin(LED_PIN), freq=1000)
while True:
# 0 is always LOW, 65535 is always HIGH; in between, the pin spends
# that fraction of every PWM period HIGH.
for duty in range(256):
led.duty_u16(duty * 257)
time.sleep_ms(8)
for duty in range(255, -1, -1):
led.duty_u16(duty * 257)
time.sleep_ms(8)duty * 257 turns the 0 to 255 steps into the 0 to 65535 that duty_u16 wants, so the ramp is the same as the Arduino sketch's, and looks just as lopsided. The next article fixes that.
When it does not work
The pin may not be PWM-capable. On an Uno only pins 3, 5, 6, 9, 10 and 11 are, marked with a tilde. On an ESP32, ESP32-S3 and Pico nearly every output pin is. Wire SIGNAL to D9 on an Uno and the sketch works unchanged.
Update the ESP32 board package. analogWrite arrived in the ESP32 core's 2.0 releases and is standard in 3.x. On an old core, use the ledc functions instead; the ESP32 PWM article linked below shows both.
That is the PWM itself. A phone camera samples the light far more often than an eye does and catches the off parts of each cycle. It does not mean anything is wrong, and it is not visible in the room.
Correct, and it is not a fault. Brightness as the eye sees it is not proportional to the duty cycle, so 128 looks much more than half bright. The next article corrects it with a small table.
Why the ramp above looks lopsided, and the ten-line table that fixes it.
A fade that looks even →Edit this page — content/books/xl-led/brightness-is-a-duty-cycle.mdx
Questions about this product
See what other owners have asked, and read their solutions.
XL LED
Loading discussions…
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.