# Correcting for non-linear brightness in LEDs when using PWM

When driving an LED with PWM, the brightness (as I perceive it) does not scale linearly with duty cycle. The brightness is slow to ramp up, then increases exponentially with duty cycle.

Can anyone suggest a rule of thumb to use as a correction factor, or other workaround?

• When I made a pair of Knight Rider cuff links, I had to use x^10 to make the fade off look nice! – Rocketmagnet Sep 9 '13 at 20:05
• Are you sure it's not "the brightness initially increases exponentially, and then is slow to ramp up"? – Dmitry Grigoryev Dec 22 '15 at 8:17
• I believe our eyes respond logarithmically to brightness. – DKNguyen Jun 14 '19 at 22:24

For 16 levels it's easy to do a simple look-up table "by hand" and convert the 4 bit value in an 8 bit value to pass to PWM controller: this is the component i've used in my FPGA led array driver. For an 8 bit level controller, you'll need at least 11-12 bit output from the look-up table.

library IEEE;
use IEEE.Std_logic_1164.all;

entity Linearize is
port ( reqlev : in std_logic_vector (3 downto 0) ;
pwmdrive : out std_logic_vector (7 downto 0) );
end Linearize;

architecture LUT of Linearize is
begin
with reqlev select
pwmdrive <= "00000000" when "0000",
"00000010" when "0001",
"00000100" when "0010",
"00000111" when "0011",
"00001011" when "0100",
"00010010" when "0101",
"00011110" when "0110",
"00101000" when "0111",
"00110010" when "1000",
"01000001" when "1001",
"01010000" when "1010",
"01100100" when "1011",
"01111101" when "1100",
"10100000" when "1101",
"11001000" when "1110",
"11111111" when "1111",
"00000000" when others;
end LUT;

• I'm trying to figure out exactly what your formula is. It's remarkably close to f(x)=x^2, but the curve isn't quite deep enough. f(x)=x^3/13 gets me a lot closer. – ajs410 Sep 27 '10 at 19:30
• It's no formula (not intentionally)... I've trown in the linearizer initial values just guessing:-). I've then powered the array, driving led columns in brightness order, and tweaked the values to get an even ramp. It's really easy with only 16 levels. – Axeman Oct 2 '10 at 10:58
• @ajs410 - looks more like $2^n$ to me: the first 1 bit more or less shifts left 1 position to the left with each step. – stevenvh Jul 16 '11 at 6:16

In theory it should be exponential, but I've got best results for fading by using a quadratic function.

I also think you got it backwards. At low duty cycle the perceived increase in brightness is much bigger than at almost full duty cycle, where the increase in brightness is almost imperceivable.

I've been looking in to this subject over the last few days as I have the same problem... trying to dim LEDs using PWM in a visibly linear way, but I want full 256 step resolution. Trying to guess 256 numbers to manually create a curve is not an easy task!

I'm not an expert mathematician, but I know enough to generate some basic curves by combining a few functions and formulas without really knowing how they work. I find that using a spreadsheet (I used Excel) you can play around with a set of numbers from 0 to 255, put a few formulas in the next cell, and graph them.

I'm using pic assembler to do the fading, and so you can even get the spreadsheet to generate the assembler code with a formula (="retlw 0x" & DEC2HEX(A2)). This makes it very quick and easy to try out a new curve.

After a bit of playing around with LOG and SIN functions, the average of the two, and a few other things, I couldn't really get the right curve. What is happening is that the middle part of the fade was happening slower than the lower and higher levels. Also, if a fade-up is immediately followed by a fade-down there was a sharp noticeable spike in the intensity. What is needed (in my opinion) is an S curve.

A quick search on Wikipedia came up with the formula needed for an S curve. I plugged this into my spreadsheet, and made a few adjustments to make it multiply across my range of values, and came up with this:

I tested it on my rig, and it worked beautifully.

=1/(1+EXP(((A2/21)-6)*-1))*255


where A2 is the first value in column A, which increases A3, A4, ..., A256 for each value.

I have no idea if this is mathematically correct or not, but it produces the desired results.

Here are the full set of 256 levels that I used:

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05,
0x05, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0B,
0x0C, 0x0C, 0x0D, 0x0D, 0x0E, 0x0F, 0x0F, 0x10, 0x11, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1F, 0x20, 0x21, 0x23, 0x24, 0x26, 0x27, 0x29, 0x2B, 0x2C,
0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x43, 0x45, 0x47, 0x4A, 0x4C, 0x4F,
0x51, 0x54, 0x57, 0x59, 0x5C, 0x5F, 0x62, 0x64, 0x67, 0x6A, 0x6D, 0x70, 0x73, 0x76, 0x79, 0x7C,
0x7F, 0x82, 0x85, 0x88, 0x8B, 0x8E, 0x91, 0x94, 0x97, 0x9A, 0x9C, 0x9F, 0xA2, 0xA5, 0xA7, 0xAA,
0xAD, 0xAF, 0xB2, 0xB4, 0xB7, 0xB9, 0xBB, 0xBE, 0xC0, 0xC2, 0xC4, 0xC6, 0xC8, 0xCA, 0xCC, 0xCE,
0xD0, 0xD2, 0xD3, 0xD5, 0xD7, 0xD8, 0xDA, 0xDB, 0xDD, 0xDE, 0xDF, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5,
0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xED, 0xEE, 0xEF, 0xEF, 0xF0, 0xF1, 0xF1, 0xF2,
0xF2, 0xF3, 0xF3, 0xF4, 0xF4, 0xF5, 0xF5, 0xF6, 0xF6, 0xF6, 0xF7, 0xF7, 0xF7, 0xF8, 0xF8, 0xF8,
0xF9, 0xF9, 0xF9, 0xF9, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFC,
0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD,
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFF

• This equation worked perfectly for me. – Ignacio Vazquez-Abrams Sep 19 '13 at 20:57

I was using an ATtiny for lighting my deck. Brightness is controlled using a pot connected to ADC pin.

Tried exponential function and the PWM output based on that seems to be giving linear increase in perceived brightness.

I was using this formulae:

out = pow(out_max, in/in_max)


Attiny85 @8MHz was taking about 210us to perform the above calculation. To improve the performance, made a lookup table. Since input was from 10-bit ADC and ATtiny memory is limited, I wanted to create a shorter table as well.

Instead of making lookup table with 1024 entries, made a reverse lookup table with 256 entries (512 bytes) in program memory (PGMEM). A function was written to perform binary search on that table. This method takes only 28uS for each lookup. If I use a direct lookup table, it would require 2kb memory, but lookup would take only 4uS or so.

Calculated values in lookup table uses input range 32-991 only, discarding lower/upper range of ADC, in case there is problem with circuit.

Below is what I have now.

// anti_log test program

/*LED connected to PIN6(PB1)*/
#define LED 1

// Anti-Log (reverse) lookup table
// y = 0-255 (pwm output), y_range=256
// x = 0-1023 (10-bit ADC input);
// assuming lower/higher end of ADC out values cannot be used
// discarding first 32 and last 32 values.
// min_x = 32, max_x = 1023-min_x, x_range=1024-2*min_x
// ANTI_LOG[y] = round( x_range*log(y, base=y_range) + min_x )
// given a value of x, perform a binary lookup on below table
// takes about 28uS for Attiny85 @8MHz clock
PROGMEM prog_uint16_t ANTI_LOG[] = {
0x0000, 0x0020, 0x0098, 0x00de, 0x0110, 0x0137, 0x0156, 0x0171, 0x0188, 0x019c, 0x01af, 0x01bf, 0x01ce, 0x01dc, 0x01e9, 0x01f5,
0x0200, 0x020a, 0x0214, 0x021e, 0x0227, 0x022f, 0x0237, 0x023f, 0x0246, 0x024d, 0x0254, 0x025b, 0x0261, 0x0267, 0x026d, 0x0273,
0x0278, 0x027d, 0x0282, 0x0288, 0x028c, 0x0291, 0x0296, 0x029a, 0x029f, 0x02a3, 0x02a7, 0x02ab, 0x02af, 0x02b3, 0x02b7, 0x02bb,
0x02be, 0x02c2, 0x02c5, 0x02c9, 0x02cc, 0x02cf, 0x02d3, 0x02d6, 0x02d9, 0x02dc, 0x02df, 0x02e2, 0x02e5, 0x02e8, 0x02eb, 0x02ed,
0x02f0, 0x02f3, 0x02f5, 0x02f8, 0x02fa, 0x02fd, 0x0300, 0x0302, 0x0304, 0x0307, 0x0309, 0x030b, 0x030e, 0x0310, 0x0312, 0x0314,
0x0317, 0x0319, 0x031b, 0x031d, 0x031f, 0x0321, 0x0323, 0x0325, 0x0327, 0x0329, 0x032b, 0x032d, 0x032f, 0x0331, 0x0333, 0x0334,
0x0336, 0x0338, 0x033a, 0x033c, 0x033d, 0x033f, 0x0341, 0x0342, 0x0344, 0x0346, 0x0347, 0x0349, 0x034b, 0x034c, 0x034e, 0x034f,
0x0351, 0x0352, 0x0354, 0x0355, 0x0357, 0x0358, 0x035a, 0x035b, 0x035d, 0x035e, 0x0360, 0x0361, 0x0363, 0x0364, 0x0365, 0x0367,
0x0368, 0x0369, 0x036b, 0x036c, 0x036d, 0x036f, 0x0370, 0x0371, 0x0372, 0x0374, 0x0375, 0x0376, 0x0378, 0x0379, 0x037a, 0x037b,
0x037c, 0x037e, 0x037f, 0x0380, 0x0381, 0x0382, 0x0383, 0x0385, 0x0386, 0x0387, 0x0388, 0x0389, 0x038a, 0x038b, 0x038c, 0x038e,
0x038f, 0x0390, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e,
0x039f, 0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7, 0x03a8, 0x03a9, 0x03aa, 0x03ab, 0x03ab, 0x03ac, 0x03ad,
0x03ae, 0x03af, 0x03b0, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b4, 0x03b5, 0x03b6, 0x03b7, 0x03b8, 0x03b9, 0x03ba, 0x03ba, 0x03bb,
0x03bc, 0x03bd, 0x03be, 0x03bf, 0x03bf, 0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7, 0x03c7, 0x03c8,
0x03c9, 0x03ca, 0x03ca, 0x03cb, 0x03cc, 0x03cd, 0x03cd, 0x03ce, 0x03cf, 0x03d0, 0x03d0, 0x03d1, 0x03d2, 0x03d3, 0x03d3, 0x03d4,
0x03d5, 0x03d6, 0x03d6, 0x03d7, 0x03d8, 0x03d8, 0x03d9, 0x03da, 0x03db, 0x03db, 0x03dc, 0x03dd, 0x03dd, 0x03de, 0x03df, 0x03df
};

// Binary lookup using above table.
byte antilog( int x )
{
byte y = 0x80;
int av;
for( int i=0x40; i>0; i>>=1 )
{
if ( av > x )
{
y -= i;
}
else if ( av < x )
{
y |= i;
}
else
{
return y;
}
}
if ( pgm_read_word_near( ANTI_LOG+y ) > x )
{
y -= 1;
}
return y;
}

void setup()
{
pinMode( LED, OUTPUT );
digitalWrite( LED, LOW );
}

#define MIN_X 0
#define MAX_X 1024

void loop()
{
int i;
// antilog_drive
for( i=MIN_X; i<MAX_X; i++ )
{
analogWrite( LED, antilog( i ) );
delay( 2 );
}
for( --i; i>=MIN_X; i-- )
{
analogWrite( LED, antilog( i ) );
delay( 2 );
}
delay( 1000 );
// Linear drive
for( i=MIN_X; i<MAX_X; i++ )
{
analogWrite( LED, i>>2 );
delay( 2 );
}
for( --i; i>=MIN_X; i-- )
{
analogWrite( LED, i>>2 );
delay( 2 );
}
delay( 2000 );
}


I found this guy who uses a method he calls "Anti-Log Drive". Here is the direct download link for his info.

This PDF explains the curve needed, apparently a logarithmic one. If you have a linear dimmer (your PWM value) then the function should be logarithmic.

Here you can find a lookup table for 32 steps of brightness for 8 bit PWM.

Here for 16 steps.

Here is what I have done based on that arduino forum response. I have computed the values from 0 to 255 so it's easy to use with pwm on arduino

byte ledLookupTable[] = {0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,7,7,7,8,8,8,9,9,9,10,10,11,11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,23,23,24,24,25,26,26,27,28,28,29,30,30,31,32,32,33,34,35,35,36,37,38,38,39,40,41,42,42,43,44,45,46,47,47,48,49,50,51,52,53,54,55,56,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,73,74,75,76,77,78,79,80,81,82,84,85,86,87,88,89,91,92,93,94,95,97,98,99,100,102,103,104,105,107,108,109,111,112,113,115,116,117,119,120,121,123,124,126,127,128,130,131,133,134,136,137,139,140,142,143,145,146,148,149,151,152,154,155,157,158,160,162,163,165,166,168,170,171,173,175,176,178,180,181,183,185,186,188,190,192,193,195,197,199,200,202,204,206,207,209,211,213,215,217,218,220,222,224,226,228,230,232,233,235,237,239,241,243,245,247,249,251,253,255};


Then to use on Arduino just do like that :

analogWrite(ledPin, ledLookupTable[brightness]); //with brighness between 0 and 255


Hope it is helpfull for some people ;)

I'm dealing with this now, and I'm taking a slightly different approach. I want 256 levels of brightness, but mapping a linear 0-255 range to a non-linear 0-255 range winds up, as you can see in some of the other answers, with a lot of duplicate entries. (I.e., several of your input values result in the same brightness level.)

I tried modifying the algorithm to map a 0-256 input range to a 0-1023 output range, but even that had several values mapping to 0. So I'm trying something a bit different - I'm using the 0-255 level to generate non-linear values in the range 0-769 (that's 1023 minus 255) using sin(), then add that to the input level to get an output in the range 0-1023 with no duplicates. I'll configure a timer to use a counter of 1023, and set the comparator for the PWM output to values from the lookup table based on what lighting level I want (0-255).

Here's the C program I used to generate my lookup table:

#include <stdio.h>
#include <math.h>

int main() {
int i;
double j;
int k;

printf( "int brightness[] = {\n" );
for( i=0; i<256; i++ ) {
// 0 - 255 => 1.0 - 0.0, multiply by 90 degrees (in radians)
j = (1 - (i / 255.0)) * M_PI / 2;
j = sin( j );
k = (1023-255) - j * (1023-255);
printf( "%s%d%s%s",
(((i % 8) == 0) ? "    " : " "), // leading space at start of line
k+i,
((i < 255) ? "," : ""),          // comma after all but last value
(((i % 8) == 7) ? "\n" : "")     // line break every 8 items
);
}
printf( "  };\n" );
}


And here's the table:

int brightness[] = {
0, 1, 2, 3, 4, 5, 6, 7,
8, 10, 11, 12, 14, 15, 16, 18,
19, 21, 22, 24, 25, 27, 29, 30,
32, 34, 35, 37, 39, 41, 43, 44,
46, 48, 50, 52, 54, 56, 58, 61,
63, 65, 67, 69, 72, 74, 76, 78,
81, 83, 86, 88, 91, 93, 96, 98,
101, 103, 106, 109, 111, 114, 117, 120,
122, 125, 128, 131, 134, 137, 140, 143,
146, 149, 152, 155, 158, 161, 164, 168,
171, 174, 177, 181, 184, 187, 191, 194,
198, 201, 205, 208, 212, 215, 219, 222,
226, 230, 233, 237, 241, 244, 248, 252,
256, 260, 263, 267, 271, 275, 279, 283,
287, 291, 295, 299, 303, 307, 312, 316,
320, 324, 328, 333, 337, 341, 345, 350,
354, 358, 363, 367, 372, 376, 381, 385,
390, 394, 399, 403, 408, 412, 417, 422,
426, 431, 436, 440, 445, 450, 455, 459,
464, 469, 474, 479, 484, 489, 493, 498,
503, 508, 513, 518, 523, 528, 533, 538,
543, 548, 554, 559, 564, 569, 574, 579,
584, 590, 595, 600, 605, 610, 616, 621,
626, 632, 637, 642, 647, 653, 658, 664,
669, 674, 680, 685, 690, 696, 701, 707,
712, 718, 723, 729, 734, 740, 745, 751,
756, 762, 767, 773, 778, 784, 790, 795,
801, 806, 812, 818, 823, 829, 834, 840,
846, 851, 857, 863, 868, 874, 880, 885,
891, 897, 902, 908, 914, 920, 925, 931,
937, 942, 948, 954, 960, 965, 971, 977,
982, 988, 994, 1000, 1005, 1011, 1017, 1023
};


I'll probably investigate other functions (like log()) once I've got this up and running.

For me this law seem to work pretty good: http://www.pyroelectro.com/tutorials/fading_led_pwm/theory2.html