XL LED/Brightness/07. A fade that looks even
Brightness · 07 of 9

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

Two fades
Step
0 of 10
Step 1 looks, equal
35 %
Step 1 looks, corrected
11 %
Both LEDs are off. Press Fade both and watch where each one seems to get bright.

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_even_fade.ino
/*
  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.

When it does not work

The bottom of the fade still looks steppy.

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.

Why 2.2 and not some other number?

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.

Can I compute the curve with pow() instead of a table?

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.

Where this goes next

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

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

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.

Browse Modules and blocks on the forum