Logarithmic scaling for LED dimming?

Hi all,

I'm writing some code to control TLC5940's via the internet. Currently in my code, I'm simply subtracting a value (something like 128 or 256) from the TLC cycle of 4096 which reduces the brightness in those steps.

I have heard that brightness is not perceived in this linier fashion so I'm wondering how I would go about implementing it?

the reason I want to do this is because I hardly notice it dimming when the values are high but when they get lower the dimming steps are to great and it wont dim as low as I want it too (only 20 or 30).

Thanks

A simple approach would be to have a table (array) of values which you subtract (you could make them logarithmic).

eg.

int vals [] = { 256, 230, 200, 150, 100, 50, 10 };

So each time you plan to reduce the brightness you subtract a different amount. Getting the values right might require some trial and error.

Hi Nick,

Thanks, simple is good where I'm concerned :slight_smile:

So I guess something like the following could work?

int vals [] = { 256, 230, 200, 150, 100, 50, 10 };
int numVal = 7; //number of values in array


for (int i = 0; i < numVal; i++){ //for the amount of values in the array..
brightness = (brightness - vals[i]); //set new brightness level
}

That's what I had in mind. And you can get the compiler to work out the number of items in the array:

// number of items in an array
#define NUMITEMS(arg) ((unsigned int) (sizeof (arg) / sizeof (arg [0])))

int vals [] = { 256, 230, 200, 150, 100, 50, 10 };
int numVal = NUMITEMS (vals); //number of values in array

I have heard that brightness is not perceived in this linier fashion

That's interesting.... worth reading up on just for the hell of it....

if you need more steps you can use multimap() to interpolate between the values of the array.

multimap() approximates a non linear function with linear pieces. The more points you take the smaller the average/max error.
And be aware that the points do not need to be equidistant. This allows you to add points where the function has the most "dynamics" and to leave out points where the function is "boring linear"

Hi all.

I've got the code running but I'm noticing some strange behavior, the values in the array (vals) is being added up, and the total value is being subtracted from the brightness each time - (1500 in this case)?

Here is the code, very experimental at the moment and a lot will change. The array is defined in the DEFINES section and the dimming is happening in the void down() function.

//////////////////////
//INCLUDES
//////////////////////
#include <SPI.h>
#include <Ethernet.h>
#include "Tlc5940.h"
//////////////////////
//END OF INCLUDES
//////////////////////


//////////////////////
//DEFINES
//////////////////////
int TLCDelay = 50;  //Delay after setting TLC
int brightness = 4095;  //Initial brightness to display
int brightnessStep = 128;  //Dimming steps in

// number of items in an array
#define NUMITEMS(arg) ((unsigned int) (sizeof (arg) / sizeof (arg [0])))

int vals [] = { 1000, 500 };
int numVal = NUMITEMS (vals); //number of values in array


//////////////////////
//END OF DEFINES
//////////////////////


//////////////////////
//ETHERNET SETUP
//////////////////////
byte mac[] = { 
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; //physical mac address
byte ip[] = { 
  192, 168, 0, 177 }; // ip in lan
byte gateway[] = { 
  192, 168, 1, 1 }; // internet access via router
byte subnet[] = { 
  255, 255, 255, 0 }; //subnet mask
EthernetServer server(80); //server port
String readString;
//////////////////////
//END OF ETHERNET SETUP
//////////////////////


//////////////////////
//START OF MODES
//////////////////////
void allOn(){
  Tlc.clear();
  digitalWrite(7, HIGH);
  Tlc.setAll(brightness);
  Tlc.update();
  delay(TLCDelay);
}

void sides(){
  Tlc.clear();
  digitalWrite(7, HIGH);
  for (int i = 4; i < 12; i++)
  {
    Tlc.set(i, brightness);
  }
  Tlc.update();
  delay(TLCDelay);
}

void gradient(){
  Tlc.clear();
  for (int i = 0; i < 4; i++)
    Tlc.set(i, 4095);

  for (int i = 4; i < 8; i++)
    Tlc.set(i, 1000);

  for (int i = 8; i < 12; i++)
    Tlc.set(i, 250);

  for (int i = 12; i < 16; i++)
    Tlc.set(i, 100);

  Tlc.update();
  delay(TLCDelay);
}

void frontRow(){
  Tlc.clear();
  Tlc.set((1, 5, 9), brightness);
  Tlc.update();
  delay(TLCDelay);
}

void middle(){
  Tlc.clear();
  Tlc.set(6, brightness);
  Tlc.set(7, brightness);
  Tlc.set(8, brightness);
  Tlc.update();
  delay(TLCDelay);

}

//////////////////////
//END OF MODES
//////////////////////


//////////////////////
//START OF DIMMING CONTROL
//////////////////////
void up(){
  if ((brightness + brightnessStep) < 4095)
    brightness = brightness + brightnessStep; 

  Serial.println(brightness);
  Tlc.setAll(brightness);
  Tlc.update();
  delay(TLCDelay);
}

void down(){


  for (int i = 0; i < numVal; i++){ //for the amount of values in the array..
    brightness = (brightness - vals[i]); //set new brightness level
  }



  Serial.println(brightness);
  Tlc.setAll(brightness);
  Tlc.update();
  delay(TLCDelay);
}
//////////////////////
//END OF DIMMING CONTROL
//////////////////////


void setup(){

  Tlc.init(0);

  pinMode(7, OUTPUT); //pin selected to control
  //start Ethernet
  Ethernet.begin(mac, ip, gateway, subnet);
  server.begin();
  Serial.begin(9600);

}



void loop(){
  // Create a client connection
  EthernetClient client = server.available();
  if (client) {
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        //read char by char HTTP request
        if (readString.length() < 100) {

          //store characters to string
          readString += c;
          //Serial.print(c);
        }

        //if HTTP request has ended
        if (c == '\n') {

          ///////////////

          client.println("HTTP/1.1 200 OK"); //send new page
          client.println("Content-Type: text/html");
          client.println();

          client.println("<HTML>");
          client.println("<HEAD>");
          client.println("<meta name='apple-mobile-web-app-capable' content='yes' />");
          client.println("<meta name='apple-mobile-web-app-status-bar-style' content='black-translucent' />");
          client.println("<link rel='stylesheet' type='text/css' href='http://homeautocss.net84.net/a.css' />");
          client.println("<TITLE>Home Automation</TITLE>");

          client.println("<meta name=\"viewport\" content=\"user-scalable = yes\" />"); //?
          client.println("<meta name=\"viewport\" content=\"width=device-width, height=device-height, initial-scale=1, maximum-scale=1\" />");


          client.println("</HEAD>");
          client.println("<BODY>");
          client.println("<H1>Home Automation</H1>");
          client.println("<hr />");
          client.println("
");

          client.println("<a href=\"/?lighton\"\">All On</a>");
          client.println("<a href=\"/?lightoff\"\">All Off</a>
");
          client.println("
");
          client.println("<hr />");
          client.println("
");
          client.println("<a href=\"/?sides\"\">Sides</a>");  
          client.println("<a href=\"/?gradient\"\">Gradient</a>
");  
          client.println("
");
          client.println("
");
          client.println("<a href=\"/?up\"\">Up</a>");
          client.println("<a href=\"/?down\"\">Down</a>
");
          client.println("
");
          client.println("
");
          client.println("<a href=\"/?frontRow\"\">Front Row</a>");
          client.println("<a href=\"/?middle\"\">Middle</a>
");


          client.println("</BODY>");
          client.println("</HTML>");

          delay(1);
          //stopping client
          client.stop();

          ///////////////////// control arduino pin
          if(readString.indexOf("?lighton") >0)//checks for on
          {
            allOn();
          }
          else if(readString.indexOf("?lightoff") >0)//checks for off
          {
            Tlc.setAll(0);
            Tlc.update();
            delay(TLCDelay);
          }
          else if(readString.indexOf("?sides") >0)//checks for off
          {
            sides();
          }
          else if(readString.indexOf("?gradient") >0)//checks for off
          {
            gradient();
          }
          else if(readString.indexOf("?up") >0)
          {
            up();
          }
          else if(readString.indexOf("?down") >0)
          {
            down();
          }
          else if(readString.indexOf("?frontRow") >0)
          {
            frontRow();
          }
          else if(readString.indexOf("?middle") >0)
          {
            middle();
          }



          //clearing string for next read
          readString="";

        }
      }
    }
  }
}


/*
            digitalWrite(7, LOW);
 //Tlc.set(1, 0);
 //Tlc.set(15, 0);
 for (int i = 4095; i > (0-1) ; i--)
 {
 Tlc.set(15, i);
 Tlc.set(10, i);
 Tlc.update();
 delay(1);
 }
 Tlc.set(9, 0);
 Tlc.update();
 delay(100);
 
 */

You may be running out of RAM. Try using the F macro, eg. change:

         client.println("<HTML>");
          client.println("<HEAD>");
          client.println("<meta name='apple-mobile-web-app-capable' content='yes' />");
          client.println("<meta name='apple-mobile-web-app-status-bar-style' content='black-translucent' />");
          client.println("<link rel='stylesheet' type='text/css' href='http://homeautocss.net84.net/a.css' />");
          client.println("<TITLE>Home Automation</TITLE>");

          client.println("<meta name=\"viewport\" content=\"user-scalable = yes\" />"); //?
          client.println("<meta name=\"viewport\" content=\"width=device-width, height=device-height, initial-scale=1, maximum-scale=1\" />");

to:

          client.println(F("<HTML>"));
          client.println(F("<HEAD>"));
          client.println(F("<meta name='apple-mobile-web-app-capable' content='yes' />"));
          client.println(F("<meta name='apple-mobile-web-app-status-bar-style' content='black-translucent' />"));
          client.println(F("<link rel='stylesheet' type='text/css' href='http://homeautocss.net84.net/a.css' />"));
          client.println(F("<TITLE>Home Automation</TITLE>"));

          client.println(F("<meta name=\"viewport\" content=\"user-scalable = yes\" />")); //?
          client.println(F("<meta name=\"viewport\" content=\"width=device-width, height=device-height, initial-scale=1, maximum-scale=1\" />"));

And so on.


the values in the array (vals) is being added up, and the total value is being subtracted from the brightness each time

Well that's what your code is told to do:

  for (int i = 0; i < numVal; i++){ //for the amount of values in the array..
    brightness = (brightness - vals[i]); //set new brightness level
  }

You are subtracting every value.

Thanks Nick, I will use the F macro option!

I'm confused as to how I pull the values to subtract from the 'val' array each time through the loop?

You probably don't want to do it each time. You would have a (global) variable that says what part of the array you are up to. After a certain time elapsed (use millis() to find when that is), increment the variable to get the next number from the array to subtract.

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: Stevens's power law - Wikipedia - 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:

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 - Weber–Fechner law - Wikipedia - 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

/*
 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 
delay(500);
}

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

Grumpy_Mike:
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...

#define CIELPWM(a) (pgm_read_word_near(CIEL8 + a))

It would be...

#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!