PWM Exponential LED Fading on Arduino (or other platforms)

For a project I am working on I needed to dim a LED strip light using the PWM (pulse width modulated) outputs on an Arduino. The most straightforward way to do this would have been to linearly vary the output frequency. Shown below in an Arduino sketch:

// Use pin 9 as the PWM output
const int outputPin = 9;

void setup() {
  // Set the pin as a digital output
  pinMode(outputPin, OUTPUT);
}

void loop() {
  // Fade the brightness from 0 (min) to 255 (max) with 40ms delay per step
  for (int brightness = 0; brightness <= 255; brightness++) {
    analogWrite(outputPin, brightness);
    delay(40);
  }
}

However the light outputted by the LED does not scale linearly. If you were to use the very scientific metric of "Perceived Brightness" vs. time for the above sketch it would look something like the below for a linear PWM signal:

brightness_vs_time_linear

To the naked eye this looks like an exponential brightening; big jump in brightness at the start then no perceptible change for the last 7 seconds.

In order to make the LED look like it is fading linearly we need to set the input PWM signal to counteract the natural exponential nature of the LED.

power_vs_time_exp

From trial and error I've found the most pleasing input curve to use is generated by:

PWM_equation

Where x is the step (i.e. step 56 of the fade), y is the PWM value (0-255) and r is a factor that is calculated based on the number of steps and the required output.

r is calculated such that 2m/r=p. Where p is the maximum value of the PWM cycle (255 in Arduinos) and m is the number of steps the LED will fade over.

We can rearrange the equation so that it is expressed in terms of r:

r_equation

You can generate this in Arduino code using:

//Setup the output PWM pin
const int outputPin = 9;

// The number of Steps between the output being on and off
const int pwmIntervals = 100;

// The R value in the graph equation
float R;

void setup() {
  // set the pin connected to the LED as an output
  pinMode(outputPin, OUTPUT);

  // Calculate the R variable (only needs to be done once at setup)
  R = (pwmIntervals * log10(2))/(log10(255));

}

void loop() {
  int brightness = 0;

  for (int interval = 0; interval <= pwmIntervals; interval++) {
      // Calculate the required PWM value for this interval step
      brightness = pow (2, (interval / R)) - 1;
      // Set the LED output to the calculated brightness
      analogWrite(outputPin, brightness);
      delay(40);
  }
}

This will give you a nice natural fading of the light over as long and as many steps as you like.