A fade that looks even
Equal steps of duty cycle do not look like equal steps of brightness. A fade has to take tiny steps where the LED is dim and big steps where it is bright, and a small table of values does that.
Two fades
Both LEDs start dark, finish at full and take ten steps. The top one sends equal steps of duty cycle: 0, 26, 51, 77 and so on. The bottom one sends steps chosen to look equal.
The curves under them are how bright each step looks. The top one leaps up at the first step and then flattens: most of the change you can see happens in the first third of the ramp. The bottom one climbs steadily.
Why the eye does this
The eye judges light by ratios, not differences. Going from 26 to 51 doubles the light, and it looks like a big change. Going from 230 to 255 adds the same 25 steps, only about 10 % more light, and you can barely see it. A ramp of equal steps is therefore mostly doubling at the start and mostly nothing at the end.
The fix is to make each step a similar ratio instead: very small steps near the bottom, larger steps near the top. The usual curve is brightness to the power 2.2. The number is a rule of thumb, not a measurement of this LED, but it looks right in most rooms.
Doing it with a table
The sketch keeps 32 precomputed values in an array and sends those instead of the loop counter. The table starts 0, 0, 1, 1, 3, 5 and ends with steps of more than fifteen at a time. Looked at as numbers it is lopsided. Seen on the LED, it is even.
A table is cheaper than computing the curve each time. On an Uno, pow() on
a float takes a noticeable amount of time and a few kilobytes of program
space. Thirty-two bytes of table takes neither.
The limit at the bottom
With 256 levels, the corrected curve spends only a handful of them on the
dimmest part, and 1 out of 255 is already a visible glow in a dark room. That
is where a fade still looks steppy. On an ESP32 you can ask for 12-bit PWM with
analogWriteResolution(LED_PIN, 12), which gives 4096 levels and a smooth
start; scale the table to 4095 to match.
The code
The same fade as before, but each of 32 brightness steps is looked up in a table rather than sent straight to analogWrite. Change LED_PIN to your PWM pin.
/*
XL LED - a fade that looks even 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;
// 32 steps that look equally spaced: 255 * (i / 31) ^ 2.2, rounded.
const uint8_t LOOKS_EVEN[32] = {
0, 0, 1, 1, 3, 5, 7, 10,
13, 17, 21, 26, 32, 38, 44, 52,
60, 68, 77, 87, 97, 108, 120, 132,
145, 159, 173, 188, 204, 220, 237, 255
};
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
for (int i = 0; i < 32; i++) {
analogWrite(LED_PIN, LOOKS_EVEN[i]);
delay(60);
}
for (int i = 31; i >= 0; i--) {
analogWrite(LED_PIN, LOOKS_EVEN[i]);
delay(60);
}
}The table is 255 times step to the power 2.2, for 32 equal steps from 0 to 1, rounded. It starts 0, 0, 1, 1 and ends 220, 237, 255: tiny steps where the eye is sensitive, big ones where it is not. Compare it with the previous article's fade and the difference is plain.
The corrected fade in MicroPython. The table is computed once at start-up rather than typed out, and in 16 bits, so the dim end is smoother than the Arduino version's.
"""
XL LED - a fade that looks even, 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)
# 32 steps that look equally spaced: 65535 * (i / 31) ** 2.2, rounded.
LOOKS_EVEN = [round(65535 * (i / 31) ** 2.2) for i in range(32)]
while True:
for duty in LOOKS_EVEN:
led.duty_u16(duty)
time.sleep_ms(60)
for duty in reversed(LOOKS_EVEN):
led.duty_u16(duty)
time.sleep_ms(60)65535 levels instead of 255: the first step above zero is 34 out of 65535, about an eighth of the Arduino table's smallest step of 1 in 255, so the bottom of the fade no longer looks steppy. Change 2.2 to 2.8 if the top still seems to hang.
When it does not work
Eight bits of PWM is only 256 levels, and the corrected curve spends most of them near the top. The first few non-zero steps are 1, 2 and 3 out of 255, and the jump between them is visible in a dark room. On an ESP32, analogWriteResolution(LED_PIN, 12) gives 4096 levels and a smoother start.
It is a rule of thumb, borrowed from how screens encode brightness, and it looks right for most LEDs in most rooms. Anything from about 2 to 3 is reasonable. Try 2.8 if the fade still seems to hang at the top.
Yes, and on an ESP32 it costs nothing. On an Uno, pow() on floats is slow and pulls in a few kilobytes of code, which is why the sketch uses a precomputed table. The table is also easier to tweak by eye.
A blink that leaves the rest of the sketch free to do its job.
Blinking without stopping →Edit this page — content/books/xl-led/a-fade-that-looks-even.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.