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
  • 2
    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
up vote 13 down vote accepted

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
  • 1
    @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:

S curve

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

The Excel formula I used was this:

=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

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

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 )
  {
    av = pgm_read_word_near( ANTI_LOG+y );
    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 );
}

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.

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

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.