Updated code for simple PWM LED controller

I’ve updated the code to my LED strip driver and included a few new features.

Here’s a quick demo video.
[UPDATE 7 Mar 2011] Added input/output graph plus link to download LUT calc excel file.
[UPDATE : DEC 6 2010] I’ve had some proper PCB’s made for this circuit. More info here.

Intensity memory.

Now the code stores the current LED intensity (PWM duty) in EEPROM so that it can “remember” it after you turn it off and on again.  It actually stores the lookup table index (See below for explanation) rather than the intensity, but the end result is the same. It’s quite handy as it means the system works much more like how you expect lights to work.  It’s done with a simple test in the main loop that checks to see if the current value is different to the one it last stored.  If it’s different then it stores the current value.  The main loop contains a delay of 100mS so this runs about 10 times a second. (But only updates the EEPROM if the value has changed)

Reading and writing to EEPROM is pretty easy.  There’s a couple of things to be observed and a specific set of instructions to activate a write. It’s all in the datasheet.  There’s a great set of BoostC libraries by Lieven Hollevoet here. I modified his eeprom library by to work with the PIC12f683. I’ve included my modified library in the zip file with the latest version of the code. (Thanks Lieven!)

Input debouncing.

I added some very simple de-bouncing to the input from the rotary encoder.  I was getting a lot of jittering due to the mechanical nature of the contacts in the encoder.  The de-bouncing is really simple.  When the IOC interrupt is called I store the current value of the rotary encoder inputs. I then wait 1mS and see if the value is the same. If it is then I process the input, if not I ignore it.

The length of the delay is important and depends on the type of input being de-bounced. Faster changing inputs require better solutions. If the delay is too long then you’ll miss catching some valid state changes. Too short and you’ll still get caught out by bouncing contacts. There’s some great and really detailed info here.

The short version in this case is that allowing around 1 to 5 ms of “settle” time is sufficient for this sort of application.  My delay of 1mS limits my maximum counts per second to around 1000 give or take a bit of overhead in the interrupt routine.

My encoder has 20 detents of a cycle (4 states) each so that’s 80 state changes per revolution.  1000 / 80 = 12.5 revolutions per second… More than enough for an encoder being turned by hand!

In practice it works perfectly, giving stable and precise control at just about any speed I can manage to twiddle at.

There’s more good rotary encoders info here.

Intensity mapping.

I’ve created a “mapping” function in between the input and output to give a more linear feel to the way the LED intensity changes as you turn the knob.   The human eye has a more or less logarithmic response to light. It is more sensitive to changes at low light levels than it is to changes at high intensity. With direct relationship between input and output the perceived light level changes in a non-linear fashion; all the “action” is at the lower end, with the upper end tailing off into “bright”.

By creating an exponential mapping between the input and output the perception is that the light intensity changes in a more linear fashion. This gives good resolution of brightness at low levels where you can tell the difference at the cost of loss of resolution at high intensity – where you can’t tell the difference anyway.

It’s done like this.

The input is limited to between 0 and 127, the output uses the full 10bits of available PWM resolution.  The relationship is exponential, at least I think that’s what it is, using the following formula;

Round(((x^(10/input_limit)-1)/(x-1)) * output_limit)

Trial and error gave x a value of 36. With the rounding in place it gives a nice linear start to the curve up until around point 15 where it starts to grow exponentially.

Round(((36^(10/127)-1)/(36-1)) * 1024)

A graph of input/output looks like this.

Input / Output mapping

You can download the Excel document that I created and have a play for yourself.

The lookup table is as follows (128 values). I tweaked it slightly to add 9 back in sacrificing a duplicate 3.  (I’ve marked them in blue in the excel file)

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 21, 22, 24, 25, 27, 28, 30, 32, 33, 35, 37, 39, 41, 43, 45, 47, 49, 52, 54, 56, 59, 61, 64, 66, 69, 72, 75, 78, 81, 84, 87, 91, 94, 98, 101, 105, 109, 113, 117, 121, 125, 130, 134, 139, 144, 149, 154, 159, 165, 170, 176, 182, 188, 194, 200, 207, 214, 221, 228, 235, 243, 250, 258, 267, 275, 284, 293, 302, 311, 321, 331, 342, 352, 363, 374, 386, 398, 410, 422, 435, 449, 462, 476, 491, 506, 521, 537, 553, 570, 587, 605, 623, 641, 661, 680, 701, 721, 743, 765, 788, 811, 835, 860, 885, 912, 939, 966, 995, 1023]

The lookup table array (LUT) needs to be stored in program memory as it’s a constant and we don’t have enough EEPROM anyway.  The only problem is that my lookup table contains values up to 1023 which would require an INT datatype (two bytes) and program memory can only store unsigned chars (single bytes).  I could store each int as two bytes and always remember to increment my counter by two, but that wastes a lot of space.

So to work around that I only store the lower byte in the LUT.  There’s not much going on in the upper byte as we’re only using the first two bits so I just store the index of the LUT at which these bits change in another 4 value array.  When I need to retrieve the full value (all 16 bits) I get the lower byte directly and find the upper byte by finding the index of the highest value that is less than the LUT position that I’m asking for.

Does that even make sense?

The data for the lower byte looks like this.

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 21, 22, 24, 25, 27, 28, 30, 32, 33, 35, 37, 39, 41, 43, 45, 47, 49, 52, 54, 56, 59, 61, 64, 66, 69, 72, 75, 78, 81, 84, 87, 91, 94, 98, 101, 105, 109, 113, 117, 121, 125, 130, 134, 139, 144, 149, 154, 159, 165, 170, 176, 182, 188, 194, 200, 207, 214, 221, 228, 235, 243, 250, 2, 11, 19, 28, 37, 46, 55, 65, 75, 86, 96, 107, 118, 130, 142, 154, 166, 179, 193, 206, 220, 235, 250, 9, 25, 41, 58, 75, 93, 111, 129, 149, 168, 189, 209, 231, 253, 20, 43, 67, 92, 117, 144, 171, 198, 227, 255]

The array for the upper byte just stores 4 values.

[0, 81, 104, 118]

To get the full value for LUT position 110 I first get the lower byte directly (111) then starting from the right hand side, I look for the highest value less than 110 (104) and store its index (2) in to the upper byte.  In binary that gives a 16 bit integer of 0b10,0110,1111 which in decimal is 623!

Working with integers and bytes is made a lot easier by using a union data-type that contains both an int and two bytes.

union int_or_bytes {
unsigned int ui;  //unsigned int
unsigned char ub[2]; //array of bytes. lowest byte first.

Anyway the end result is that when you turn the knob the light intensity grows in a nice linear manner, plus now it takes far fewer rotations to reach full intensity (around 3 1/2).

The latest code, including the eeprom library and a HEX file is here.
The circuit remain the same as before.

13 thoughts on “Updated code for simple PWM LED controller

  1. Hey Matt: I built version one and was thrilled at how well it worked, but felt that it needed intensity memory. I guess you read my mind; now I can’t wait to try out V2…sounds excellent! Thanks Matt!


  2. @Richard.
    Hey great to hear you built one. hope it all worked out ok. Version 2 uses the same circuit just with better code so you shouldn’t have any problem getting it going. There’s a #define at the top of main.c (line 57) that sets the maximum LUT index to limit the average current. Depending on your LED strip and the supply you are using you might want to change this to increase or decrease the maximum PWM duty. I have it at 109 which gives me around 210mA for a single strip or 250mA if I have two in series.
    The line looks like this.

    #define INPUT_MAX 109 //around 240mA

    I could have modified the LUT to set the limit lower than 1023 but I thought it better to keep the LUT the same so that the complete range could be used if needed. Plus it’s quicker to change the limit than it is to recalculate the LUT and enter in all the values again… 🙂

    Thanks Lieven for all the libraries you made. Whilst BoostC comes with many libraries I find it annoying that they don’t come with the source as I can never be sure exactly what they are doing. Part of the fun is working out how things are done so I was really pleased to find your great collection.
    Many thanks again.

  3. I made the change to 1023, but in the higher ranges of brightness, the system seems to lose the count. It gets brighter with more turns, until it gets dimmer (a little) then brighter. Then it will actually crash, to a point where I have to reprogram the chip. I’m hoping to rig a setup where the PWM values get written out to a 2 wire lcd that I already have on the breadboard. This is now stretching my abilities to where I need to learn more! 🙂 That really is the point, isn’t it?

    • Hi Richard.
      Not sure if I’m reading you right, but…
      Don’t forget that the value in INPUT_MAX represents the LUT index not the actual intensity. So the maximum value for INPUT_MAX in our case is 127 as that’s how many entries we have in our LUT. If you assign INPUT_MAX = 1023 then you’ll have problems, plus it’s a INPUT_MAX is a Char not an Int so the compiler won’t be happy about that either.

      • This is where I say duhhhhhh, of course. If that table had 1024 entries it would be rather large, wouldn’t it? It rarely pays to rush through things…

  4. Thanks a lot for this post.
    While I could just use as it is the table of PWM values you provide I really try to get the formula you came up with and the magic number 36, but I am or feel quite dumb for now. Can I ask you kindly to elaborate a little on the formula, so I can play with less steps and or higher PWM resolution?
    Thank you!

  5. Thank you!
    I was trying exactly that – to come up with an excel file where I can play with the formula.
    I think you just climbed yourself up the google ladder for the specific phrase – LED exponential PWM formula. For years to come that is!
    My regards!

  6. Pingback: Simple PWM control for LED strip « Catmacey's stuff

  7. Great article.

    How do i implement a softstart and a softdim with in the ledstrip, in case the ledstrip is been powered up or down ?


    • Hi Samuel,

      To implement a soft-start wouldn’t be too much hassle.
      Assuming you’re using a microcontroller (mcu) then all you’d need to do is hold the PWM output low (off) on startup then build it up to full intensity over period of time.

      Soft-off could be a bit harder as you’d need to control the power to the whole ciruit rather than just having an off switch. One option is to have the MCU always on so that your power button is actually just a signal to the MCU to control the power to the LED rather than actually being a real power switch. When the “off” button is pressed your MCU starts ramping down the PWM until the LED is off. That’d leave the powersupply and MCU still running of course, which might not be what you want, so you could optionlly have the MCU cut it’s own power at that point using a “soft power switch” idea either with a relay or a triac or one of these from Pololu which look perfect for the task.

      If you can’t be bothered with all that then a rough and ready approach to soft-off might be to have a large-ish capacitor on the circuit supply so that when the power is cut the capacitor will slowly drain. If your MCU can work down to low voltages then you might find that it’ll carry on PWM’ing whilst the capacitor drains and the LEDs dim giving you pretty much what you want. Not very classy but worth a try.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s