Experimenting with LED Dimming Functions - Smoothing Functions Arduino Sketch

In this post, I wrote a sketch to try out different smothing functions for fading LEDs.

Choosing a smoothing function that spends less time on the low end of the transition has the same effect as reducing the overall transition time. We are essentially allocating more steps to the low end of the brightness scale. (The x axis can be described as “time” and the y axis as “steps”). Choosing a smoothing function that spends less time on the low end of the transition has the same effect as reducing the overall transition time. We are essentially allocating more steps to the low end of the brightness scale. (The x axis can be described as “time” and the y axis as “steps”).

Use the graph below to compare different smoothing functions and their behaviour across the domain 0-255.

Smoothing Functions

The graph above shows a few smoothing functions. In order, following the line y=150, we have:

  • a logarithmic function, y=46 \ln x
  • the logistic function (s-curve), y=\frac {L}{1+e^{-k(x-c)}}
  • linear, y=x
  • and quadratic, y= x^2

Arduino Fading Function Sketch

The following Arduino Program cycles through 3 different dimming functions: logistic, quadratic and linear.


#include <Arduino.h>

#define SIGNAL_PIN 4
#define DELAY 50

// function domain space
#define MAX 1000
#define MIN 0
#define INCREMENT 20 // step through domain space in increments of ~

// for logistic function
#define L 1000.0
#define e 2.71828
#define c 500.0
#define k 1.14
#define BRIGHTNESS_MAX 1800 // maximum output signal (function output mapped to values from 0 to ~)



int inc; // +/- INCREMENT
int x; // function input value
int func; // function output value

int mode; // which function to use (implementation specific)
int counter; //track how many iterations per mode (implementation specific)

void setup() {

    counter = 0;
    mode = 0;
    inc = INCREMENT;
    x = 0;
    pinMode(SIGNAL_PIN, OUTPUT);
    Serial.begin(115200);
}

int logistic(float x);
int quadratic(int x);

void loop() {
    // put your main code here, to run repeatedly:
    // fade in from min to max in increments of 5 points:

      int b;
      // sets the value (range from 0 to 255):
      switch (mode) {
        case 0:
          func=logistic(x);
          b = map(func, MIN,logistic(MAX),0,BRIGHTNESS_MAX );
          break;
        case 1:
          func=quadratic(x);
          b = map(func,0,quadratic(MAX),0,BRIGHTNESS_MAX);
          break;
        case 2:
          func = x; // linear i.e. f(x) = x
          b = map(func,MIN,MAX,0,BRIGHTNESS_MAX);
          break;
      }

      analogWrite(SIGNAL_PIN, b); // set signal voltage

      // wait for 30 milliseconds to see the dimming effect
      Serial.print("mode: ");
      Serial.print(mode);
      Serial.print("\t\tmapped: ");
      Serial.print(b);
      Serial.print("\t\tfunc: ");
      Serial.print(b);
      Serial.print("\t\tBrightness: ");
      Serial.print(x);


    if (x + inc > MAX) // check if adding inc will overshoot our max
    {

      inc = -INCREMENT;
      Serial.print("\t\tInc: ");
      Serial.print(inc);
    }

    if (x + inc < MIN) //inc itself is negative
    {
      inc = INCREMENT;
      Serial.print("\t\tInc: ");
      Serial.print(inc);
      counter++;
      if (counter > 3)
      {
        counter = 0;
        mode = (mode+1)%3; // cycle between values of 0, 1, 2
        analogWrite(SIGNAL_PIN, 0);
        delay(2000);
        analogWrite(SIGNAL_PIN, 0);
      }
    }
    x = x + inc;
    delay(DELAY);
    Serial.println();
}

int quadratic(int x)
{
  return x*x;
}

int logistic(float x)
{
  float power = -0.01*k*(x-c);
  return (int) floor( L/(1+pow(e,power)));
}

Unfortunately, all these smoothing functions exhibit choppy fading on the dim end. It really should have come at no surprise because we are not increasing the resolution at all. Choppiness is especially noticeable for long transitions. Given a hardware-limited resolution of 256 PWM levels, the longer we have to stretch these levels the more time will be spent on each level. As the frames per second go below 24fps, it is very noticable.

  • 256 levels / 1s = 256fps (very smooth, even at low brightness)
  • 256 / 10s = 25fps (choppiness visible at low brightness)
  • 256 / 20s = 12fps (not smooth at all, clear brightness level difference is visible every time the brightness is reduced by one level.)

If you are looking for a way to fix the choppyness, I would suggest use a low pass filter as discussed this the main post.