LED brightness setting with Arduino PWM

If you have ever tried to dim a white LED using a PWM signal you must have experienced that it is not as easy as it first seems. A 10% duty cycle will never result in a 10% brightness. The relationship between the duty cycle and the perceived brightness is not linear at all. In this article I’ll explain my practical method of solving this problem.In this experiment, I’m using an Arduino Nano clone as the PWM source, a 4W LED lamp from IKEA as the light source and an IRF540n mosfet for switching the high current.

schematic

You might notice that I’m connecting the gate of the mosfet to PB3 on the atmega328p which is labeled D9 on an Arduino Nano. I’m doing this because this pin “OC1A” can produce a high frequency 10-bit PWM which is far more useful than the 8-bit PWM that the analogWrite function can produce. I recommend this article for more details.

Let’s create a real simple Arduino sketch for our PWM experiment:

void setup() {
  // PB2 as output
  DDRB = 0x02;

  // setting up mode of operation, prescaling, etc for timer1
  TCCR1A = (1 << COM1A1) | (1 << WGM11) | (1 << WGM10);
  TCCR1B = (1 << CS10);
}

void loop() {
  for (uint16_t i=0; i <= 1023; i++) {
    // this is setting the duty cycle of the PWM (1023 is 100%)
    OCR1A = i;
    delay(5);
  }
}

If we run this sketch we see our LEDs brightness increasing from zero to full brightness in about 5 seconds. But the rate of increase is much higher at the beginning than at 50%. The first idea that comes to one’s mind is to try some logarithmic function but unfortunately that only solves the problems with the low duty cycles. After some googling I found that the needed function is an S-curve like this:

s-curve-1023

Using this curve we have 256 levels of brightness and the output is very close to linear. Here’s how to use this curve in code:

// first add this at the beginning of the sketch
#include <avr/pgmspace.h>

// then define the brightness levels like this
const uint16_t br[] PROGMEM = {
  0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3,
  4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9,
  9, 10, 10, 11, 12, 12, 13, 13, 14, 15, 16, 17, 17, 18, 19,
  20, 21, 22, 24, 25, 26, 28, 29, 30, 32, 34, 35, 37, 39, 41,
  43, 46, 48, 50, 53, 55, 58, 61, 64, 67, 71, 74, 78, 82, 86,
  90, 94, 99, 104, 109, 114, 119, 125, 130, 137, 143, 149,
  156, 163, 170, 178, 186, 194, 202, 210, 219, 228, 238, 247,
  257, 267, 278, 288, 299, 310, 322, 333, 345, 357, 369, 382,
  394, 407, 420, 433, 446, 459, 472, 485, 499, 512, 525, 539,
  552, 565, 578, 591, 604, 617, 630, 642, 655, 667, 679, 691,
  702, 714, 725, 736, 746, 757, 767, 777, 786, 796, 805, 814,
  822, 830, 838, 846, 854, 861, 868, 875, 881, 887, 894, 899,
  905, 910, 915, 920, 925, 930, 934, 938, 942, 946, 950, 953,
  957, 960, 963, 966, 969, 971, 974, 976, 978, 981, 983, 985,
  987, 989, 990, 992, 994, 995, 996, 998, 999, 1000, 1002,
  1003, 1004, 1005, 1006, 1007, 1007, 1008, 1009, 1010, 1011,
  1011, 1012, 1012, 1013, 1014, 1014, 1015, 1015, 1016, 1016,
  1016, 1017, 1017, 1017, 1018, 1018, 1018, 1019, 1019, 1019,
  1019, 1020, 1020, 1020, 1020, 1020, 1021, 1021, 1021, 1021,
  1021, 1021, 1022, 1022, 1022, 1022, 1022, 1022, 1022, 1022,
  1022, 1022, 1023, 1023
};

// finally set the brightness with this line:
OCR1A = pgm_read_word(&br[value_0_to_255]);

If you want to use analogWrite() and only 8-bit PWM, here’s your array of values. I must admit that I didn’t test these though.

const uint8_t br[] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
  3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7,
  7, 8, 8, 8, 9, 9, 10, 10, 11, 11, 12, 13, 13, 14, 15, 15,
  16, 17, 18, 19, 19, 20, 21, 22, 23, 25, 26, 27, 28, 30, 31,
  32, 34, 36, 37, 39, 41, 42, 44, 46, 48, 50, 52, 55, 57, 59,
  62, 64, 67, 69, 72, 75, 77, 80, 83, 86, 89, 92, 95, 98, 101,
  105, 108, 111, 114, 118, 121, 124, 128, 131, 134, 137, 141,
  144, 147, 150, 154, 157, 160, 163, 166, 169, 172, 175, 178,
  180, 183, 186, 188, 191, 193, 196, 198, 200, 203, 205, 207,
  209, 211, 213, 214, 216, 218, 219, 221, 223, 224, 225, 227,
  228, 229, 230, 232, 233, 234, 235, 236, 236, 237, 238, 239,
  240, 240, 241, 242, 242, 243, 244, 244, 245, 245, 246, 246,
  247, 247, 247, 248, 248, 248, 249, 249, 249, 250, 250, 250,
  250, 251, 251, 251, 251, 251, 252, 252, 252, 252, 252, 252,
  253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 254, 254,
  254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254,
  254, 254, 254, 254, 254, 254, 254, 255, 255, 255, 255, 255,
  255, 255, 255
};

That’s it! If you have any comments please don’t hesitate to share!

Update 2016.02.20.

Here’s a full example sketch to use with analogWrite. Notice the “(int)” type-conversion that is needed for the analogWrite:

#include <avr/pgmspace.h>

const uint8_t br[] PROGMEM = {
  0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
  3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7, 7,
  7, 8, 8, 8, 9, 9, 10, 10, 11, 11, 12, 13, 13, 14, 15, 15,
  16, 17, 18, 19, 19, 20, 21, 22, 23, 25, 26, 27, 28, 30, 31,
  32, 34, 36, 37, 39, 41, 42, 44, 46, 48, 50, 52, 55, 57, 59,
  62, 64, 67, 69, 72, 75, 77, 80, 83, 86, 89, 92, 95, 98, 101,
  105, 108, 111, 114, 118, 121, 124, 128, 131, 134, 137, 141,
  144, 147, 150, 154, 157, 160, 163, 166, 169, 172, 175, 178,
  180, 183, 186, 188, 191, 193, 196, 198, 200, 203, 205, 207,
  209, 211, 213, 214, 216, 218, 219, 221, 223, 224, 225, 227,
  228, 229, 230, 232, 233, 234, 235, 236, 236, 237, 238, 239,
  240, 240, 241, 242, 242, 243, 244, 244, 245, 245, 246, 246,
  247, 247, 247, 248, 248, 248, 249, 249, 249, 250, 250, 250,
  250, 251, 251, 251, 251, 251, 252, 252, 252, 252, 252, 252,
  253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 254, 254,
  254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254,
  254, 254, 254, 254, 254, 254, 254, 255, 255, 255, 255, 255,
  255, 255, 255
};

void setup() {
  pinMode(9, OUTPUT);
}

void loop() {
  for (uint16_t i = 0; i <= 255; i++) {
    analogWrite(9, (int)&br[i]);
    delay(5);
  }
}

3 CommentsLeave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *