Jared Sanson


Linear LED PWM

Feb 03, 2013 electronics, leds Article

In this blog post, I will show you how to properly control an LEDs brightness using PWM. It isn't as simple as you think!

PWM works by varying the duty cycle of a square wave, like so:

The average voltage of the output is equal to VCC * k, thus changing the duty cycle will change the brightness of an LED connected to output. Pretty much every microcontroller supports PWM, either through a dedicated peripheral, or you can do it yourself in software!

However, there is a problem with the common approach of PWMing an LED. Take this Arduino code for example:

void loop() {
    byte i = 0;
    while (1) {
        analogWrite(2, i);

This will fade an LED on pin 2 of the arduino, from dark to bright. But you may notice the LEDs don't fade very nicely. They will appear to fade very quickly at first, and then spend a long time at full brightness:

The reason behind this is because the human eye doesn't respond to light linearly, but logarithmically. That sure complicates things!

To fix it, we have to correct the PWM values to make them appear linear to the human eye.

A common misconception is that gamma correction should be used, as it does have a very similar response to the eye. However, gamma correction has nothing to do with how humans perceive light, it is just coincidence that it appears to work. The CIE 1931 lightness formula is what actually describes how we perceive light:

L* = 903.3 ∙ Y,            if Y ≤ 0.008856
L* = 116   ∙ Y^1/3 – 16,   if Y > 0.008856

Where Y is the luminance (output) between 0.0 and 1.0, and L* is the lightness (input) between 0 and 100

EDIT: I mixed up Y and L*, and thus the relationship was wrong. the article has been updated to fix this

The formula needs to be rearranged in terms of L*:

Y = (L* / 902.3)           if L* ≤ 8
Y = ((L* + 16) / 116)^3    if L* > 8

Of course, this formula would be too slow to implement on a microcontroller due to the power and division, so you should use a look-up table instead. I created a simple python script to generate a C header file that can be simply included into the project:


INPUT_SIZE = 255       # Input integer size
OUTPUT_SIZE = 255      # Output integer size
INT_TYPE = 'const unsigned char'
TABLE_NAME = 'cie';

def cie1931(L):
    L = L*100.0
    if L <= 8:
        return (L/902.3)
        return ((L+16.0)/116.0)**3

x = range(0,int(INPUT_SIZE+1))
y = [round(cie1931(float(L)/INPUT_SIZE)*OUTPUT_SIZE) for L in x]

f = open('cie1931.h', 'w')
f.write('// CIE1931 correction table\n')
f.write('// Automatically generated\n\n')

f.write('%s %s[%d] = {\n' % (INT_TYPE, TABLE_NAME, INPUT_SIZE+1))
for i,L in enumerate(y):
    f.write('%d, ' % int(L))
    if i % 10 == 9:

Which produces a table that looks like this:

const unsigned char cie[256] = { 0, 0, 0, 0, 0, 1, 1, ..., 247, 250, 252, 255 };

Depending on the microcontroller being used, you may want to change the type so it stores the values in ROM instead of RAM.

The constants at the top can be changed to suit the microcontroller. For instance, if you wanted to use a 10 bit PWM, you could set INT_SIZE=1024. This would still generate a table with 256 entries, but the output will be 10 bits.

Because this conversion reduces the resolution of the PWM, it may indeed be wise to use a 10 bit PWM. This is exactly what I did in my LED Coffee Table Project.

Finally, using the lookup table to fade LEDs:

#include "cie1931.h"

void loop() {
    byte i = 0;
    while (1) {
        analogWrite(2, cie[i]);

And now we have LEDs that fade linearly!

View Comments...


A friend asked me if I could turn a damaged samsung LED TV into a coffee table, so I created this project. The TV was damaged in the Christchurch Earthquake a year or so ago, but only the front LCD panel was damaged. The backlight was still intact, so the TV made a really nice coffee table!


The first task was to disassemble the panel, which wasn't too hard to do. The circuitry was separated into the Power Supply and Computer board, and it turns out the Computer board isn't necessary to control the backlight.

Disassembled PanelDisassembled Panel

The next thing to do was to figure out how to control the backlight. After some experimentation shorting out various wires, I figured out how to turn on the PSU and LEDs.

It turns out the backlight is separated into 4 rows of LEDs, and I realised I could make some really cool patterns! You can see two of the rows active in one of the photos above.

Once I had figured out how to drive it, I started prototyping a circuit.


I decided to use a dsPIC30F because I had one on hand, and I didn't want to waste one of my more powerful dsPIC33F chips. The project greatly benefited from the extra power, since it allowed me to have 4x 10 bit PWM channels. (much better than an Arduino!)

Prototype CircuitPrototype Circuit


The firmware supports 4 animation channels, and a brightness channel. 

To make sure the animations look as smooth as possible, I used gamma correction to make it appear linear, and 10 bit PWM to give the widest dynamic range possible.

I discovered at 8 bits, the resolution of animations at low brightness was very poor, especially since I had applied gamma correction. After expanding it to 10 bit resolution, the animations were very nice.

I used inline assembly in parts to make use of the dsPIC instruction set. While not necessary, I wanted to learn it.

There are 8 modes that I added:

  1. Fully on
  2. Middle rows on
  3. Outer rows on
  4. Animated waves, from inner to outer rows
  5. Animated waves, from one side to the other
  6. Pulsing animation, varies from half to full brightness, kind of like a heartbeat 7. Slow flash mode
  7. Fast flash mode (aka. Seizure mode!)

The screen is really really bright at full brightness, and it's very weird to look at since it's spread over such a large surface.

I am happy to provide source code on request!


After I had gotten the firmware working how I wanted it to, I built a rough frame using spare lumber. It's a little rough around the edges, but it's not bad for a first try!

DIY Wood FrameDIY Wood Frame

Final Product

All that was left was to add some buttons and mount it into its frame:

Sorry, no video of the animations yet.

View Comments...

I recently ordered some PIC24F32KA302 microcontrollers, but found out that my PicKit 2 wouldn't detect them!

Luckily, I found a way to add them to the software. First you need to download the PicKit 2 Device File Editor.

Open the PicKit 2 device file, usually located at
C:\Program Files\Microchip\PicKit 2\PK2DeviceFile.dat

I started by duplicating a similar device, the PIC24F16KA102. Most of the settings appear to be the same for that particular family, so by looking at the differences you can work out what needs to be modified.

But where do you get the missing numbers from? Eventually I stumbled across a datasheet by Microchip: PIC24FXXKA1XX/FVXXKA3XX Flash Programming Specifications

The fields that need to be updated are:

  • PartName
  • DeviceID
  • ProgramMem

All the other fields appear to be the same.

Device ID

The Device ID is in Section 6 near the bottom, and for the PIC24F32KA302 it is 0x4502.


The Program Memory Size is a little trickier. For example, the PIC24F08 family reports 0x0B00 in the device file, but 0x2BFE in the datasheet. I found the formula to be (0x2BFE + 2) / 2.

Thus for the PIC24F32KA302, the program memory field should be set to 0x2C00


Save the data file to the desktop, then copy & replace the original.

Now start PicKit 2, select PIC24 from the Device Family menu, and...


View Comments...

Playing with RF

Jan 23, 2013 electronics, rf Uncategorized


I recently ordered two RFM23BP radio modules, which transmit at 434MHz. They have a built in amplifier that can produce a 0dBW (1000mW) signal, so I'm hoping that they can reach a few km or so.

Because I live in New Zealand, I have to conform to the radio standards, and thus I decided that it would be better to operate on the 458MHz band where I'm allowed a maximum of -3dBW (500mW).

The RFM23 module supports three modulation modes: OOK (On-Off Keying), FSK (Frequency Shift Keying), and GFSK (Gaussian Frequency Shift Keying). I wanted to know what these modulation modes looked like, so I pulled out my handy RTL-SDR compatible dongle:

458MHz Spectrum458MHz Spectrum

This is the total viewable bandwidth around the 458 MHz band. The red line is centered on that band, and the maximum allowable bandwidth around the band is 70kHz (Contrasted with a total viewable bandwidth of approx 2MHz)

You can also see a strong signal on the left, which appears to be some sort of coded signal.  The faint bands on each side are just artefacts.


OOK ModulationOOK Modulation

The first mode I tried is OOK, where the signal is simply turned on and off to code the data. You can see faint bands on either side of the signal, this is spectral splatter from the sharp signal.

Unlike the next two modes, OOK operates at only one frequency, at 458.58MHz.


FSK ModulationFSK Modulation

The next mode I tried was FSK, where the coded data switches between two frequencies on either side of the center frequency. As with the OOK modulation, there is unwanted spectral splatter on each frequency band, though not as visible here.

Incidentally, the bandwidth of 50kHz is just within the allowable frequency band for 458MHz.


GFSK ModulationGFSK Modulation

GFSK is the module's most efficient mode, as it reduces spectral splatter.  As you can see, it spreads the frequencies out using some sort of gaussian function, which reduces the harshness of the signal.


The above screenshots were captured without an antenna on the RFM23 module, with the power level set to only 1.3mW! So obviously it will have very good range once I add a proper antenna.

My next task will be to test the maximum range of this module, and hopefully it will extend far enough for my needs!

View Comments...