Go Down

Topic: Logarithmic scaling for LED dimming? (Read 2 times) previous topic - next topic


Here's a method of calculating brightness levels that appear to be equally spaced.  It may be a bit late in the thread's life for this.  

Stevens' power law  - see it here: http://en.wikipedia.org/wiki/Stevens_power_law - gives a method of determining the relative perceived intensity of a stimulus - in this case, brightness of an LED - as a function of the physical magnitude of the stimulus.  Here's the general form:
P = k * Sa
where P is the perceived intensity, S is the magnitude, and k and a are constants that depend on the type of stimulus and the units of measurement.  The value of a is less than 1 for brightness and loudness; greater than 1 for electric current through fingertips.

The value of a isn't well-characterized, though.  For light intensity, it might be around 0.33, but the actual value will depend on the ambient lighting, color, the color and brightness of the background that it's seen against, and, to some extent, which one of us is looking at the LED.  It'll take some experiments to find an acceptable value.

Dealing with k is easier - in fact, we can forget about it.  Here's why:  the units of P, the perceived brightness, are arbitrary.  They're generic "perceived brightness units," and they don't correspond to any real physical quantity.  So, we can pick the units of P so that the value of k is exactly one, and then we can forget about k.  We'll also select the units of S as PWM ticks - the time-averaged illumination resulting from a PWM code of 1.  The amount of light that comes from the  LED varies nearly linearly with this quantity, so it's a reasonable unit to use.  And, doing so makes the math a lot easier.

To do the experiments, we can pick an a, and calculate an array defining n equal steps.  First, we calculate the maximum perceived brightness in arbitrary units:  
Pmax = Smaxa
Smax is the code that corresponds to the maximum brightness at which you want to operate an LED.  It could be anything, but it's easy to select 4095, since that's the maximum brightness code your LED driver IC will accept.  Then, for n=0 to N, where N is the number of steps, calculate Sn like this:
Sn = [Pmax * (n/N)](1/a)
or, in C, rounding the output codes to integers:
Code: [Select]
levels[n] = int(pow(Pmax * ((float)n/(float)N), 1/a) + 0.5);
The wiki article suggests that a is between 0.33 and 0.5.  My experiments say that might be true, but they also say that equal step-size is tricky to identify.

At the end, we have an array of codes that will ostensibly yield an equal change in brightness for each step.  That's true if you believe that Stevens' power law accurately describes perceived brightness.  Not everyone does.  The alternative is the Weber-Fechner law - http://en.wikipedia.org/wiki/Weber-Fechner_law - which describes a curve that's logarithmic, rather than a power function.  The math is about the same, except that it uses exp() rather than pow(), but the experimentation is harder - it requires you to find a stimulus magnitude that results in a perception of zero, and that stimulus magnitude can't itself be zero.  My rough tests suggest that a PWM code of 1 yields a perceptible brightness, so I think that we'd just be guessing about it.  The power law seems to be reasonably well-accepted, so it'll likely yield acceptable results.


Here is a simple example of using a look up table with the 256 levels given by the normal PWM

Code: [Select]
Change brightness of LED linearly to Human eye
32 step brightness using 8 bit PWM of Arduino
brightness step 24 should be twice bright than step 12 to your eye.

#include <avr/pgmspace.h>
#define CIELPWM(a) (pgm_read_word_near(CIEL8 + a)) // CIE Lightness loopup table function

5 bit CIE Lightness to 8 bit PWM conversion
L* = 116(Y/Yn)^1/3 - 16 , Y/Yn > 0.008856
L* = 903.3(Y/Yn), Y/Yn <= 0.008856

prog_uint8_t CIEL8[] PROGMEM = {
0,    1,    2,    3,    4,    5,    7,    9,    12,
15,    18,    22,    27,    32,    38,    44,    51,    58,
67,    76,    86,    96,    108,    120,    134,    148,    163,
180,    197,    216,    235,    255

int brightness = 0;    // initial brightness of LED
int fadeAmount = 1;

void setup()  {
// declare pin 9 to be an output:
pinMode(9, OUTPUT);

void loop()  {
// set the brightness of pin 9:, 0-31, 5 bit steps of brightness
analogWrite(9, CIELPWM(brightness));
// change the brightness for next time through the loop:
brightness = brightness + fadeAmount;
// reverse the direction of the fading at the ends of the fade:
if (brightness == 0 || brightness == 31) {
fadeAmount = -fadeAmount ;
// wait for 500 milliseconds to see the bightness change

Nick Gammon

It looks to me like Grumpy_Mike's example lists new values, not differences. That is probably much simpler anyway.


Here is a simple example of using a look up table with the 256 levels given by the normal PWM

Hey Mike!

I was just trying your code, and I've noticed something...
Shouldn't the macro read a byte, and not a word?

So instead of...
Code: [Select]
#define CIELPWM(a) (pgm_read_word_near(CIEL8 + a))

It would be...
Code: [Select]
#define CIELPWM(a) (pgm_read_byte_near(CIEL8 + a))

With that change it seems to work fine, and definitely looks more "natural", instead of a straight linear scale... thanks!

Go Up